前端之ES6核心语法

前端之ES6核心语法
墨颜丶文章声明:本文来自阮一峰老师发表在ES6 入门教程 - ECMAScript 6入门上的和自己阅读后自己的编撰
ES6新特性
| - | - |
|---|---|
| let和const命令 | 迭代器和生成器 |
| es6的模板字符串 | Promise对象 |
| 增强的函数 | Proxy对象 |
| 扩展的字符串、对象、数组功能 | async的用法 |
| 解构赋值 | 类class |
| Symbol | Map和Set |
| 模块化实现 |
let和const命令
let是一个块级作用域
ES6 新增了let命令,用来声明变量。它的用法类似于var,但是所声明的变量,只在let命令所在的代码块内有效。
1 | <script> |
上面代码在代码块之中,分别用let和var声明了两个变量。然后在代码块之外调用这两个变量,结果let声明的变量报错,var声明的变量返回了正确的值。这表明,let声明的变量只在它所在的代码块有效。
let声明变量,没有变量提升
1 | console.log(a); // undefined |
上述代码,为什么var不会报错,是因为编译器在编译的时候,看到关键词var时,会先提前声明
1 | console.log(a); // undefined |
let不能重复声明变量
1 | var a=2; |
const声明一个只读的常量。一旦声明,常量的值就不能改变。
1 | const PI = 3.1415; |
1 | const PI = 3.1415; |
上面代码表明改变常量的值会报错。
const声明的变量不得改变值,这意味着,const一旦声明变量,就必须立即初始化,不能留到以后赋值。
1 | const foo; |
上面代码表示,对于const来说,只声明不赋值,就会报错。
const的作用域与let命令相同:只在声明所在的块级作用域内有效。
1 | if (true) { |
const命令声明的常量也是不提升,同样存在暂时性死区,只能在声明的位置后面使用。
1 | if (true) { |
上面代码在常量MAX声明之前就调用,结果报错。
const声明的常量,也与let一样不可重复声明。
1 | var message = "Hello!"; |
const声明对象也一样,不可重复声明
1 | const person = { |
1 | const person = { |
要解决的问题
for循环经典例子:
1 | // 声明一个数组 |
上面代码中,变量i是var命令声明的,在全局范围内都有效,所以全局只有一个变量i。每一次循环,变量i的值都会发生改变,而循环内被赋给数组arr的函数内部的return i,里面的i指向的就是全局的i。也就是说,所有数组arr的成员里面的i,指向的都是同一个i,导致运行时输出的是最后一轮的i的值,也就是 10。
如果使用let,声明的变量仅在块级作用域内有效,最后输出的是 5。
1 | // 声明一个数组 |
不会污染全局变量
1 | var RegExp=10; |
总结
let是一个块级作用域
let声明变量,没有变量提升
let不能重复声明变量
const声明常量,一旦被声明无法修改,也没有变量提升,只在声明所在的块级作用域内有效
建议:在默认情况下用const,而只有在你知道变量值需要被修改的情况下使用let
模板字符串
之前
1 | <div class="box"></div> |
ES6
1 | <div class="box"></div> |
变量的解构赋值
数组的解构赋值
基本用法
ES6 允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构(Destructuring)。
以前,为变量赋值,只能直接指定值。
1 | let a = 1; |
ES6 允许写成下面这样。
1 | let [a, b, c] = [1, 2, 3]; |
上面代码表示,可以从数组中提取值,按照对应位置,对变量赋值。
本质上,这种写法属于“模式匹配”,只要等号两边的模式相同,左边的变量就会被赋予对应的值。下面是一些使用嵌套数组进行解构的例子。
1 | let [foo, [[bar], baz]] = [1, [[2], 3]]; |
如果解构不成功,变量的值就等于undefined。
1 | let [foo] = []; |
以上两种情况都属于解构不成功,foo的值都会等于undefined。
另一种情况是不完全解构,即等号左边的模式,只匹配一部分的等号右边的数组。这种情况下,解构依然可以成功。
1 | let [x, y] = [1, 2, 3]; |
上面两个例子,都属于不完全解构,但是可以成功。
如果等号的右边不是数组(或者严格地说,不是可遍历的结构,参见《Iterator》一章),那么将会报错。
1 | // 报错 |
上面的语句都会报错,因为等号右边的值,要么转为对象以后不具备 Iterator 接口(前五个表达式),要么本身就不具备 Iterator 接口(最后一个表达式)。
对于 Set 结构,也可以使用数组的解构赋值。
1 | let [x, y, z] = new Set(['a', 'b', 'c']); |
事实上,只要某种数据结构具有 Iterator 接口,都可以采用数组形式的解构赋值。
1 | function* fibs() { |
上面代码中,fibs是一个 Generator 函数(参见《Generator 函数》一章),原生具有 Iterator 接口。解构赋值会依次从这个接口获取值。
默认值
解构赋值允许指定默认值。
1 | let [foo = true] = []; |
对象的解构赋值
解构不仅可以用于数组,还可以用于对象。
1 | // ES5 |
对象的解构与数组有一个重要的不同。数组的元素是按次序排列的,变量的取值由它的位置决定;而对象的属性没有次序,变量必须与属性同名,才能取到正确的值。
1 | let { bar, foo } = { foo: 'aaa', bar: 'bbb' }; |
上面代码的第一个例子,等号左边的两个变量的次序,与等号右边两个同名属性的次序不一致,但是对取值完全没有影响。第二个例子的变量没有对应的同名属性,导致取不到值,最后等于undefined。
如果解构失败,变量的值等于undefined。
1 | let {foo} = {bar: 'baz'}; |
上面代码中,等号右边的对象没有foo属性,所以变量foo取不到值,所以等于undefined。
对象的解构赋值,可以很方便地将现有对象的方法,赋值到某个变量。
1 | // 例一 |
上面代码的例一将Math对象的对数、正弦、余弦三个方法,赋值到对应的变量上,使用起来就会方便很多。例二将console.log赋值到log变量。
如果变量名与属性名不一致,必须写成下面这样。
1 | let { foo: baz } = { foo: 'aaa', bar: 'bbb' }; |
这实际上说明,对象的解构赋值是下面形式的简写。
1 | let { foo: foo, bar: bar } = { foo: 'aaa', bar: 'bbb' }; |
也就是说,对象的解构赋值的内部机制,是先找到同名属性,然后再赋给对应的变量。真正被赋值的是后者,而不是前者。
1 | let { foo: baz } = { foo: 'aaa', bar: 'bbb' }; |
上面代码中,foo是匹配的模式,baz才是变量。真正被赋值的是变量baz,而不是模式foo。
与数组一样,解构也可以用于嵌套结构的对象。
1 | let obj = { |
注意,这时p是模式,不是变量,因此不会被赋值。如果p也要作为变量赋值,可以写成下面这样。
1 | let obj = { |
下面是另一个例子。
1 | const node = { |
上面代码有三次解构赋值,分别是对loc、start、line三个属性的解构赋值。注意,最后一次对line属性的解构赋值之中,只有line是变量,loc和start都是模式,不是变量。
下面是嵌套赋值的例子。
1 | let obj = {}; |
如果解构模式是嵌套的对象,而且子对象所在的父属性不存在,那么将会报错。
1 | // 报错 |
上面代码中,等号左边对象的foo属性,对应一个子对象。该子对象的bar属性,解构时会报错。原因很简单,因为foo这时等于undefined,再取子属性就会报错。
注意,对象的解构赋值可以取到继承的属性。
1 | const obj1 = {}; |
上面代码中,对象obj1的原型对象是obj2。foo属性不是obj1自身的属性,而是继承自obj2的属性,解构赋值可以取到这个属性。
默认值
对象的解构也可以指定默认值。
1 | var {x = 3} = {}; |
默认值生效的条件是,对象的属性值严格等于undefined。
1 | var {x = 3} = {x: undefined}; |
上面代码中,属性x等于null,因为null与undefined不严格相等,所以是个有效的赋值,导致默认值3不会生效。
字符串的解构赋值
字符串也可以解构赋值。这是因为此时,字符串被转换成了一个类似数组的对象。
1 | const [a, b, c, d, e] = 'hello'; |
类似数组的对象都有一个length属性,因此还可以对这个属性解构赋值。
1 | let {length : len} = 'hello'; |
数值和布尔值的解构赋值
解构赋值时,如果等号右边是数值和布尔值,则会先转为对象。
1 | let {toString: s} = 123; |
上面代码中,数值和布尔值的包装对象都有toString属性,因此变量s都能取到值。
解构赋值的规则是,只要等号右边的值不是对象或数组,就先将其转为对象。由于undefined和null无法转为对象,所以对它们进行解构赋值,都会报错。
1 | let { prop: x } = undefined; // TypeError |
函数参数的解构赋值
函数的参数也可以使用解构赋值。
1 | function add([x, y]){ |
上面代码中,函数add的参数表面上是一个数组,但在传入参数的那一刻,数组参数就被解构成变量x和y。对于函数内部的代码来说,它们能感受到的参数就是x和y。
下面是另一个例子。
1 | [[1, 2], [3, 4]].map(([a, b]) => a + b); |
函数参数的解构也可以使用默认值。
1 | function move({x = 0, y = 0} = {}) { |
上面代码中,函数move的参数是一个对象,通过对这个对象进行解构,得到变量x和y的值。如果解构失败,x和y等于默认值。
注意,下面的写法会得到不一样的结果。
1 | function move({x, y} = { x: 0, y: 0 }) { |
上面代码是为函数move的参数指定默认值,而不是为变量x和y指定默认值,所以会得到与前一种写法不同的结果。
undefined就会触发函数参数的默认值。
1 | [1, undefined, 3].map((x = 'yes') => x); |
用途
变量的解构赋值用途很多。
(1)交换变量的值
1 | let x = 1; |
上面代码交换变量x和y的值,这样的写法不仅简洁,而且易读,语义非常清晰。
(2)从函数返回多个值
函数只能返回一个值,如果要返回多个值,只能将它们放在数组或对象里返回。有了解构赋值,取出这些值就非常方便。
1 | // 返回一个数组 |
(3)函数参数的定义
解构赋值可以方便地将一组参数与变量名对应起来。
1 | // 参数是一组有次序的值 |
(4)提取 JSON 数据
解构赋值对提取 JSON 对象中的数据,尤其有用。
1 | let jsonData = { |
上面代码可以快速提取 JSON 数据的值。
(5)函数参数的默认值
1 | jQuery.ajax = function (url, { |
指定参数的默认值,就避免了在函数体内部再写var foo = config.foo || 'default foo';这样的语句。
(6)遍历 Map 结构
任何部署了 Iterator 接口的对象,都可以用for...of循环遍历。Map 结构原生支持 Iterator 接口,配合变量的解构赋值,获取键名和键值就非常方便。
1 | const map = new Map(); |
如果只想获取键名,或者只想获取键值,可以写成下面这样。
1 | // 获取键名 |
(7)输入模块的指定方法
加载模块时,往往需要指定输入哪些方法。解构赋值使得输入语句非常清晰。
1 | const { SourceMapConsumer, SourceNode } = require("source-map"); |
函数的扩展
函数参数的默认值
ES6 之前,不能直接为函数的参数指定默认值,只能采用变通的方法。
1 | // es5写法 |
默认的表达式也可以是一个函数
1 | // 2. a接受传参=10,b调用执行getVal(5) |
剩余参数
不具名参数的函数
1 | // ES5 写法 |
ES6 提供了一个方法rest 参数,(形式为...变量名),用于获取函数的多余参数,这样就不需要使用arguments对象了。rest 参数搭配的变量是一个数组,该变量将多余的参数放入数组中, 解决了arguments 的问题。
1 | // keys = ["author","title","year"] |
arguments对象不是数组,而是一个类似数组的对象。所以为了使用数组的方法,必须使用Array.from先将其转为数组。rest 参数就不存在这个问题,它就是一个真正的数组,数组特有的方法都可以使用。下面是一个利用 rest 参数改写数组push方法的例子。
1 | function checkArgs(...args){ |
知识点:
Object.create()
Object.create()是一个 JavaScript 的内置方法,它创建一个新对象,使用现有的对象作为新创建的对象的[[Prototype]]。这意味着新对象将继承现有对象的属性和方法。
这是 Object.create() 的基本语法:
1 | Object.create(proto, [propertiesObject]) |
参数:
proto:必需的参数,用作新创建的对象的[[Prototype]]。propertiesObject:可选的参数,一个 JavaScript 对象,定义了额外的属性及对应值。
arguments
arguments 是一个特殊的对象,它表示函数被调用时传递给该函数的参数
1
2
3
4
5
6
7
8
9
10// 比如上述代码中的arguments
{
"0": {
"title": "es6教程",
"author": "墨颜",
"year": "2019"
},
"1": "author",
"2": "year"
}arguments.length是一个 JavaScript 中的属性,它返回一个函数被调用时传递给该函数的参数的数量。
例如,如果你有一个函数 function myFunction() { console.log(arguments.length); },然后你调用 myFunction('one', 'two', 'three'),那么控制台将输出 3,因为 myFunction 被调用时传递了三个参数。
展开运算符
剩余运算符:把多个独立的合并到一个数组中
扩展运算符:将一个数组分割,并将各个项作为分离的参数传给函数
1 | const maxNum = Math.max(20,30,11,60) |
箭头函数
ES6 允许使用“箭头”(=>)定义函数,比如匿名函数function(){} 等于 ()=>{}。
1 | var f = v => v; |
如果箭头函数不需要参数或需要多个参数,就使用一个圆括号代表参数部分。
1 | var f = () => 5; |
如果箭头函数的代码块部分多于一条语句,就要使用大括号将它们括起来,并且使用return语句返回。
1 | var sum = (num1, num2) => { return num1 + num2; } |
由于大括号被解释为代码块,所以如果箭头函数直接返回一个对象,必须在对象外面加上括号,否则会报错。
1 | // 报错 |
下面是一种特殊情况,虽然可以运行,但会得到错误的结果。
1 | let foo = () => { a: 1 }; |
上面代码中,原始意图是返回一个对象{ a: 1 },但是由于引擎认为大括号是代码块,所以执行了一行语句a: 1。这时,a可以被解释为语句的标签,因此实际执行的语句是1;,然后函数就结束了,没有返回值。
如果箭头函数只有一行语句,且不需要返回值,可以采用下面的写法,就不用写大括号了。
1 | let fn = () => void doesNotReturn(); |
箭头函数可以与变量解构结合使用。
1 | const full = ({ first, last }) => first + ' ' + last; |
箭头函数使得表达更加简洁。
1 | const isEven = n => n % 2 === 0; |
上面代码只用了两行,就定义了两个简单的工具函数。如果不用箭头函数,可能就要占用多行,而且还不如现在这样写醒目。
箭头函数的一个用处是简化回调函数。
1 | // 普通函数写法 |
另一个例子是
1 | // 普通函数写法 |
下面是 rest 参数与箭头函数结合的例子。
1 | const numbers = (...nums) => nums; |
再比如一个例子
1 | // 普通函数写法 |
ES5中的this指向:取决于调用该函数的上下文对象
1 | let PageHandle = { |
ES6:箭头函数是没有this指向的,箭头函数内部this只能通过查找作用域链来确定,一旦使用箭头函数,当前就不存在作用域链
1 | let PageHandle = { |
上述代码中=>箭头函数是没有this指向的所以,所以他的作用域即为init: function (){},那么谁定义了init,this就指向了谁。
1 | let PageHandle = { |
如果init的方法也改成箭头函数,里边的this作用域即为PageHandle,整个window定义了PageHandle,所以即又会报错,所以给每个方法定义的时候不要使用箭头函数
知识点
1 | Element.addEventListener(event, callback, [useCapture]) |
其中:
event是要监听的事件名称(例如 “click”)。callback是当事件被触发时调用的函数。useCapture是一个可选参数,表示是否在捕获阶段处理事件。如果为true,则在捕获阶段处理事件;如果为false,则在冒泡阶段处理事件。
使用箭头函数注意事项:
- 箭头函数没有自己的
this对象(详见下文)。 - 不可以当作构造函数,也就是说,不可以对箭头函数使用
new命令,否则会抛出一个错误。 - 不可以使用
arguments对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。 - 不可以使用
yield命令,因此箭头函数不能用作 Generator 函数。
上面四点中,最重要的是第一点。对于普通函数来说,内部的this指向函数运行时所在的对象,但是这一点对箭头函数不成立。它没有自己的this对象,内部的this就是定义时上层作用域中的this。也就是说,箭头函数内部的this指向是固定的,相比之下,普通函数的this指向是可变的。
1 | function foo() { |
上面代码中,setTimeout()的参数是一个箭头函数,这个箭头函数的定义生效是在foo函数生成时,而它的真正执行要等到 100 毫秒后。如果是普通函数,执行时this应该指向全局对象window,这时应该输出21。但是,箭头函数导致this总是指向函数定义生效时所在的对象(本例是{id: 42}),所以打印出来的是42。
1 | let Person = () => { |
对象的扩展
属性的简洁表示法
ES6 允许在大括号里面,直接写入变量和函数,作为对象的属性和方法。这样的书写更加简洁。
1 | const foo = 'bar'; |
上面代码中,变量foo直接写在大括号里面。这时,属性名就是变量名, 属性值就是变量值。下面是另一个例子。
1 | // ES5 |
属性的赋值器(setter)和取值器(getter),事实上也是采用这种写法。
1 | let cart = { |
简洁写法在打印对象时也很有用。
1 | let user = { |
上面代码中,console.log直接输出user和foo两个对象时,就是两组键值对,可能会混淆。把它们放在大括号里面输出,就变成了对象的简洁表示法,每组键值对前面会打印对象名,这样就比较清晰了。
注意,简写的对象方法不能用作构造函数,会报错。
1 | const obj = { |
上面代码中,f是一个简写的对象方法,所以obj.f不能当作构造函数使用
属性名表达式
对象的新增方法
Object.is()
ES5 比较两个值是否相等,只有两个运算符:相等运算符(==)和严格相等运算符(===)。它们都有缺点,前者会自动转换数据类型,后者的NaN不等于自身,以及+0等于-0。JavaScript 缺乏一种运算,在所有环境中,只要两个值是一样的,它们就应该相等。
ES6 提出“Same-value equality”(同值相等)算法,用来解决这个问题。Object.is就是部署这个算法的新方法。它用来比较两个值是否严格相等,与严格比较运算符(===)的行为基本一致。
1 | Object.is('foo', 'foo') |
不同之处只有两个:一是+0不等于-0,二是NaN等于自身。
1 | +0 === -0 //true |
ES5 可以通过下面的代码,部署Object.is。
1 | Object.defineProperty(Object, 'is', { |
Object.assign()
Object.assign()方法用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target)。
1 | const target = { a: 1 }; |
Object.assign()方法的第一个参数是目标对象,后面的参数都是源对象。
注意,如果目标对象与源对象有同名属性,或多个源对象有同名属性,则后面的属性会覆盖前面的属性。
1 | const target = { a: 1, b: 1 }; |
如果只有一个参数,Object.assign()会直接返回该参数。
1 | const obj = {a: 1}; |
如果该参数不是对象,则会先转成对象,然后返回。
1 | typeof Object.assign(2) // "object" |
由于undefined和null无法转成对象,所以如果它们作为参数,就会报错。
1 | Object.assign(undefined) // 报错 |
如果非对象参数出现在源对象的位置(即非首参数),那么处理规则有所不同。首先,这些参数都会转成对象,如果无法转成对象,就会跳过。这意味着,如果undefined和null不在首参数,就不会报错。
1 | let obj = {a: 1}; |
其他类型的值(即数值、字符串和布尔值)不在首参数,也不会报错。但是,除了字符串会以数组形式,拷贝入目标对象,其他值都不会产生效果。
1 | const v1 = 'abc'; |
上面代码中,v1、v2、v3分别是字符串、布尔值和数值,结果只有字符串合入目标对象(以字符数组的形式),数值和布尔值都会被忽略。这是因为只有字符串的包装对象,会产生可枚举属性。
1 | Object(true) // {[[PrimitiveValue]]: true} |
上面代码中,布尔值、数值、字符串分别转成对应的包装对象,可以看到它们的原始值都在包装对象的内部属性[[PrimitiveValue]]上面,这个属性是不会被Object.assign()拷贝的。只有字符串的包装对象,会产生可枚举的实义属性,那些属性则会被拷贝。
Object.assign()拷贝的属性是有限制的,只拷贝源对象的自身属性(不拷贝继承属性),也不拷贝不可枚举的属性(enumerable: false)。
1 | Object.assign({b: 'c'}, |
上面代码中,Object.assign()要拷贝的对象只有一个不可枚举属性invisible,这个属性并没有被拷贝进去。
属性名为 Symbol 值的属性,也会被Object.assign()拷贝。
1 | Object.assign({ a: 'b' }, { [Symbol('c')]: 'd' }) |
常见用途
Object.assign()方法有很多用处。
(1)为对象添加属性
1 | class Point { |
上面方法通过Object.assign()方法,将x属性和y属性添加到Point类的对象实例。
(2)为对象添加方法
1 | Object.assign(SomeClass.prototype, { |
上面代码使用了对象属性的简洁表示法,直接将两个函数放在大括号中,再使用assign()方法添加到SomeClass.prototype之中。
(3)克隆对象
1 | function clone(origin) { |
上面代码将原始对象拷贝到一个空对象,就得到了原始对象的克隆。
不过,采用这种方法克隆,只能克隆原始对象自身的值,不能克隆它继承的值。如果想要保持继承链,可以采用下面的代码。
1 | function clone(origin) { |
(4)合并多个对象
将多个对象合并到某个对象。
1 | const merge = |
如果希望合并后返回一个新对象,可以改写上面函数,对一个空对象合并。
1 | const merge = |
(5)为属性指定默认值
1 | const DEFAULTS = { |
上面代码中,DEFAULTS对象是默认值,options对象是用户提供的参数。Object.assign()方法将DEFAULTS和options合并成一个新对象,如果两者有同名属性,则options的属性值会覆盖DEFAULTS的属性值。
注意,由于存在浅拷贝的问题,DEFAULTS对象和options对象的所有属性的值,最好都是简单类型,不要指向另一个对象。否则,DEFAULTS对象的该属性很可能不起作用。
1 | const DEFAULTS = { |
上面代码的原意是将url.port改成 8000,url.host不变。实际结果却是options.url覆盖掉DEFAULTS.url,所以url.host就不存在了。
数组的扩展
扩展运算符
扩展运算符(spread)是三个点(...)。它好比 rest 参数的逆运算,将一个数组转为用逗号分隔的参数序列。
1 | console.log(...[1, 2, 3]) |
该运算符主要用于函数调用。
1 | function push(array, ...items) { |
上面代码中,array.push(...items)和add(...numbers)这两行,都是函数的调用,它们都使用了扩展运算符。该运算符将一个数组,变为参数序列。
扩展运算符与正常的函数参数可以结合使用,非常灵活。
1 | function f(v, w, x, y, z) { } |
扩展运算符后面还可以放置表达式。
1 | const arr = [ |
如果扩展运算符后面是一个空数组,则不产生任何效果。
1 | [...[], 1] |
注意,只有函数调用时,扩展运算符才可以放在圆括号中,否则会报错。
1 | (...[1, 2]) |
上面三种情况,扩展运算符都放在圆括号里面,但是前两种情况会报错,因为扩展运算符所在的括号不是函数调用。
扩展运算符的应用
(1)复制数组
数组是复合的数据类型,直接复制的话,只是复制了指向底层数据结构的指针,而不是克隆一个全新的数组。
1 | const a1 = [1, 2]; |
上面代码中,a2并不是a1的克隆,而是指向同一份数据的另一个指针。修改a2,会直接导致a1的变化。
ES5 只能用变通方法来复制数组。
1 | const a1 = [1, 2]; |
上面代码中,a1会返回原数组的克隆,再修改a2就不会对a1产生影响。
扩展运算符提供了复制数组的简便写法。
1 | const a1 = [1, 2]; |
上面的两种写法,a2都是a1的克隆。
(2)合并数组
扩展运算符提供了数组合并的新写法。
1 | const arr1 = ['a', 'b']; |
不过,这两种方法都是浅拷贝,使用的时候需要注意。
1 | const a1 = [{ foo: 1 }]; |
上面代码中,a3和a4是用两种不同方法合并而成的新数组,但是它们的成员都是对原数组成员的引用,这就是浅拷贝。如果修改了引用指向的值,会同步反映到新数组。
(3)与解构赋值结合
扩展运算符可以与解构赋值结合起来,用于生成数组。
1 | // ES5 |
下面是另外一些例子。
1 | const [first, ...rest] = [1, 2, 3, 4, 5]; |
如果将扩展运算符用于数组赋值,只能放在参数的最后一位,否则会报错。
1 | const [...butLast, last] = [1, 2, 3, 4, 5]; |
(4)字符串
扩展运算符还可以将字符串转为真正的数组。
1 | [...'hello'] |
上面的写法,有一个重要的好处,那就是能够正确识别四个字节的 Unicode 字符。
1 | 'x\uD83D\uDE80y'.length // 4 |
上面代码的第一种写法,JavaScript 会将四个字节的 Unicode 字符,识别为 2 个字符,采用扩展运算符就没有这个问题。因此,正确返回字符串长度的函数,可以像下面这样写。
1 | function length(str) { |
凡是涉及到操作四个字节的 Unicode 字符的函数,都有这个问题。因此,最好都用扩展运算符改写。
1 | let str = 'x\uD83D\uDE80y'; |
上面代码中,如果不用扩展运算符,字符串的reverse()操作就不正确。
(5)实现了 Iterator 接口的对象
任何定义了遍历器(Iterator)接口的对象(参阅 Iterator 一章),都可以用扩展运算符转为真正的数组。
1 | let nodeList = document.querySelectorAll('div'); |
上面代码中,querySelectorAll()方法返回的是一个NodeList对象。它不是数组,而是一个类似数组的对象。这时,扩展运算符可以将其转为真正的数组,原因就在于NodeList对象实现了 Iterator。
1 | Number.prototype[Symbol.iterator] = function*() { |
上面代码中,先定义了Number对象的遍历器接口,扩展运算符将5自动转成Number实例以后,就会调用这个接口,就会返回自定义的结果。
对于那些没有部署 Iterator 接口的类似数组的对象,扩展运算符就无法将其转为真正的数组。
1 | let arrayLike = { |
上面代码中,arrayLike是一个类似数组的对象,但是没有部署 Iterator 接口,扩展运算符就会报错。这时,可以改为使用Array.from方法将arrayLike转为真正的数组。
(6)Map 和 Set 结构,Generator 函数
扩展运算符内部调用的是数据结构的 Iterator 接口,因此只要具有 Iterator 接口的对象,都可以使用扩展运算符,比如 Map 结构。
1 | let map = new Map([ |
Generator 函数运行后,返回一个遍历器对象,因此也可以使用扩展运算符。
1 | const go = function*(){ |
上面代码中,变量go是一个 Generator 函数,执行后返回的是一个遍历器对象,对这个遍历器对象执行扩展运算符,就会将内部遍历得到的值,转为一个数组。
如果对没有 Iterator 接口的对象,使用扩展运算符,将会报错。
1 | const obj = {a: 1, b: 2}; |
Array.from()
Array.from()方法用于将两类对象转为真正的数组:类似数组的对象(array-like object)和可遍历(iterable)的对象(包括 ES6 新增的数据结构 Set 和 Map)。
下面是一个类似数组的对象,Array.from()将它转为真正的数组。
1 | let arrayLike = { |
实际应用中,常见的类似数组的对象是 DOM 操作返回的 NodeList 集合,以及函数内部的arguments对象。Array.from()都可以将它们转为真正的数组。
1 | // NodeList 对象 |
上面代码中,querySelectorAll()方法返回的是一个类似数组的对象,可以将这个对象转为真正的数组,再使用filter()方法。
只要是部署了 Iterator 接口的数据结构,Array.from()都能将其转为数组。
值得提醒的是,扩展运算符(...)也可以将某些数据结构转为数组。
1 | // arguments对象 |
扩展运算符背后调用的是遍历器接口(Symbol.iterator),如果一个对象没有部署这个接口,就无法转换。Array.from()方法还支持类似数组的对象。所谓类似数组的对象,本质特征只有一点,即必须有length属性。因此,任何有length属性的对象,都可以通过Array.from()方法转为数组,而此时扩展运算符就无法转换。
1 | Array.from({ length: 3 }); |
上面代码中,Array.from()返回了一个具有三个成员的数组,每个位置的值都是undefined。扩展运算符转换不了这个对象。
Array.of()
Array.of()方法用于将一组值,转换为数组。
1 | Array.of(3, 11, 8) // [3,11,8] |
这个方法的主要目的,是弥补数组构造函数Array()的不足。因为参数个数的不同,会导致Array()的行为有差异。
1 | Array() // [] |
上面代码中,Array()方法没有参数、一个参数、三个参数时,返回的结果都不一样。只有当参数个数不少于 2 个时,Array()才会返回由参数组成的新数组。参数只有一个正整数时,实际上是指定数组的长度。
Array.of()基本上可以用来替代Array()或new Array(),并且不存在由于参数不同而导致的重载。它的行为非常统一。
1 | Array.of() // [] |
Array.of()总是返回参数值组成的数组。如果没有参数,就返回一个空数组。
Array.of()方法可以用下面的代码模拟实现。
1 | function ArrayOf(){ |
copyWithin()
数组实例的copyWithin()方法,在当前数组内部,将指定位置的成员复制到其他位置(会覆盖原有成员),然后返回当前数组。也就是说,使用这个方法,会修改当前数组。
1 | Array.prototype.copyWithin(target, start = 0, end = this.length) |
它接受三个参数。
- target(必需):从该位置开始替换数据。如果为负值,表示倒数。
- start(可选):从该位置开始读取数据,默认为 0。如果为负值,表示从末尾开始计算。
- end(可选):到该位置前停止读取数据,默认等于数组长度。如果为负值,表示从末尾开始计算。
这三个参数都应该是数值,如果不是,会自动转为数值。
1 | [1, 2, 3, 4, 5].copyWithin(0, 3) |
上面代码表示将从 3 号位直到数组结束的成员(4 和 5),复制到从 0 号位开始的位置,结果覆盖了原来的 1 和 2。
find()
数组实例的find()方法,用于找出第一个符合条件的数组成员。它的参数是一个回调函数,所有数组成员依次执行该回调函数,直到找出第一个返回值为true的成员,然后返回该成员。如果没有符合条件的成员,则返回undefined。
1 | let num = [1,4,-5,10,-11].find((n) => { |
上面代码找出数组中第一个小于 0 的成员。
1 | [1, 5, 10, 15].find(function(value, index, arr) { |
上面代码中,find()方法的回调函数可以接受三个参数,依次为当前的值、当前的位置和原数组。
findIndex()
数组实例的findIndex()方法的用法与find()方法非常类似,返回第一个符合条件的数组成员的位置,如果所有成员都不符合条件,则返回-1。
1 | let num = [1,4,-5,10,-11].findIndex(n => n < 0) |
这两个方法都可以接受第二个参数,用来绑定回调函数的this对象。
1 | function f(v){ |
上面的代码中,find()函数接收了第二个参数person对象,回调函数中的this对象指向person对象。
entries()、keys()和values()
ES6 提供三个新的方法——entries(),keys()和values()——用于遍历数组。它们都返回一个遍历器对象,可以用for...of循环进行遍历,唯一的区别是keys()是对键名的遍历、values()是对键值的遍历,entries()是对键值对的遍历。
1 | for (let index of ['a', 'b'].keys()) { |
如果不使用for...of循环,可以手动调用遍历器对象的next方法,进行遍历。
1 | let letter = ['a', 'b', 'c']; |
includes()
Array.prototype.includes方法返回一个布尔值,表示某个数组是否包含给定的值,与字符串的includes方法类似。ES2016 引入了该方法。
1 | [1, 2, 3].includes(2) // true |
该方法的第二个参数表示搜索的起始位置,默认为0。如果第二个参数为负数,则表示倒数的位置,如果这时它大于数组长度(比如第二个参数为-4,但数组长度为3),则会重置为从0开始。
1 | [1, 2, 3].includes(3, 3); // false |
没有该方法之前,我们通常使用数组的indexOf方法,检查是否包含某个值。
1 | [1,2,3].indexOf(2); // 1 |
indexOf方法有两个缺点,一是不够语义化,它的含义是找到参数值的第一个出现位置,所以要去比较是否不等于-1,表达起来不够直观。二是,它内部使用严格相等运算符(===)进行判断,这会导致对NaN的误判。
Symbol
ES5 的对象属性名都是字符串,这容易造成属性名的冲突。比如,你使用了一个他人提供的对象,但又想为这个对象添加新的方法(mixin 模式),新方法的名字就有可能与现有方法产生冲突。如果有一种机制,保证每个属性的名字都是独一无二的就好了,这样就从根本上防止属性名的冲突。这就是 ES6 引入Symbol的原因。
ES6 引入了一种新的原始数据类型Symbol,表示独一无二的值。它属于 JavaScript 语言的原生数据类型之一,其他数据类型是:undefined、null、布尔值(Boolean)、字符串(String)、数值(Number)、大整数(BigInt)、对象(Object)。
Symbol 值通过Symbol()函数生成。这就是说,对象的属性名现在可以有两种类型,一种是原来就有的字符串,另一种就是新增的 Symbol 类型。凡是属性名属于 Symbol 类型,就都是独一无二的,可以保证不会与其他属性名产生冲突。
1 | let s = Symbol(); |
上面代码中,变量s就是一个独一无二的值。typeof运算符的结果,表明变量s是 Symbol 数据类型,而不是字符串之类的其他类型。
注意,Symbol()函数前不能使用new命令,否则会报错。这是因为生成的 Symbol 是一个原始类型的值,不是对象,所以不能使用new命令来调用。另外,由于 Symbol 值不是对象,所以也不能添加属性。基本上,它是一种类似于字符串的数据类型。
Symbol()函数可以接受一个字符串作为参数,表示对 Symbol 实例的描述。这主要是为了在控制台显示,或者转为字符串时,比较容易区分。
1 | let s1 = Symbol('foo'); |
上面代码中,s1和s2是两个 Symbol 值。如果不加参数,它们在控制台的输出都是Symbol(),不利于区分。有了参数以后,就等于为它们加上了描述,输出的时候就能够分清,到底是哪一个值。
如果 Symbol 的参数是一个对象,就会调用该对象的toString()方法,将其转为字符串,然后才生成一个 Symbol 值。
1 | const obj = { |
注意,Symbol()函数的参数只是表示对当前 Symbol 值的描述,因此相同参数的Symbol函数的返回值是不相等的。
1 | // 没有参数的情况 |
上面代码中,s1和s2都是Symbol()函数的返回值,而且参数相同,但是它们是不相等的。事实上,如果调用100次Symbol(),会得到100个互不相等的值。
Symbol 值不能与其他类型的值进行运算,会报错。
1 | let sym = Symbol('My symbol'); |
但是,Symbol 值可以显式转为字符串。
1 | let sym = Symbol('My symbol'); |
另外,Symbol 值也可以转为布尔值,但是不能转为数值。
1 | let sym = Symbol(); |
Set 集合数据类型
ES6 提供了新的数据结构 Set。它类似于数组,但是成员的值都是唯一的,没有重复的值。
Set本身是一个构造函数,用来生成 Set 数据结构。
Set 结构不会添加重复的值。
Set函数可以接受一个数组(或者具有 iterable 接口的其他数据结构)作为参数,用来初始化。
1 | // 例一 |
上面代码中,例一和例二都是Set函数接受数组作为参数,例三是接受类似数组的对象作为参数。
上面代码也展示了一种去除数组重复成员的方法。
1 | // 去除数组的重复成员 |
上面的方法也可以用于,去除字符串里面的重复字符。
1 | [...new Set('ababbc')].join('') |
向 Set 加入值的时候,不会发生类型转换,所以5和"5"是两个不同的值。Set 内部判断两个值是否不同,使用的算法叫做“Same-value-zero equality”,它类似于精确相等运算符(===),主要的区别是向 Set 加入值时认为NaN等于自身,而精确相等运算符认为NaN不等于自身。
1 | let set = new Set(); |
上面代码向 Set 实例添加了两次NaN,但是只会加入一个。这表明,在 Set 内部,两个NaN是相等的。
另外,两个对象总是不相等的。
1 | let set = new Set(); |
上面代码表示,由于两个空对象不相等,所以它们被视为两个值。
Set 实例的属性和方法
Set 结构的实例有以下属性。
Set.prototype.constructor:构造函数,默认就是Set函数。Set.prototype.size:返回Set实例的成员总数。
Set 实例的方法分为两大类:操作方法(用于操作数据)和遍历方法(用于遍历成员)。下面先介绍四个操作方法。
Set.prototype.add(value):添加某个值,返回 Set 结构本身。Set.prototype.delete(value):删除某个值,返回一个布尔值,表示删除是否成功。Set.prototype.has(value):返回一个布尔值,表示该值是否为Set的成员。Set.prototype.clear():清除所有成员,没有返回值。
上面这些属性和方法的实例如下。
1 | s.add(1).add(2).add(2); |
下面是一个对比,判断是否包括一个键,Object结构和Set结构写法的不同。
1 | // 对象的写法 |
Array.from()方法可以将 Set 结构转为数组。
1 | const items = new Set([1, 2, 3, 4, 5]); |
这就提供了去除数组重复成员的另一种方法。
1 | function dedupe(array) { |
遍历操作
forEach()
Set 结构的实例与数组一样,也拥有forEach方法,用于对每个成员执行某种操作,没有返回值。
1 | let set = new Set([1, 4, 9]); |
上面代码说明,forEach方法的参数就是一个处理函数。该函数的参数与数组的forEach一致,依次为键值、键名、集合本身(上例省略了该参数)。这里需要注意,Set 结构的键名就是键值(两者是同一个值),因此第一个参数与第二个参数的值永远都是一样的。
另外,forEach方法还可以有第二个参数,表示绑定处理函数内部的this对象。
遍历的应用
扩展运算符(...)内部使用for...of循环,所以也可以用于 Set 结构。
1 | let set = new Set(['red', 'green', 'blue']); |
扩展运算符和 Set 结构相结合,就可以去除数组的重复成员。
1 | let arr = [3, 5, 2, 2, 5, 5]; |
而且,数组的map和filter方法也可以间接用于 Set 了。
1 | let set = new Set([1, 2, 3]); |
因此使用 Set 可以很容易地实现并集(Union)、交集(Intersect)和差集(Difference)。
1 | let a = new Set([1, 2, 3]); |
如果想在遍历操作中,同步改变原来的 Set 结构,目前没有直接的方法,但有两种变通方法。一种是利用原 Set 结构映射出一个新的结构,然后赋值给原来的 Set 结构;另一种是利用Array.from方法。
1 | // 方法一 |
上面代码提供了两种方法,直接在遍历操作中改变原来的 Set 结构。
Map数据类型
JavaScript 的对象(Object),本质上是键值对的集合(Hash 结构),但是传统上只能用字符串当作键。这给它的使用带来了很大的限制。
1 | const data = {}; |
上面代码原意是将一个 DOM 节点作为对象data的键,但是由于对象只接受字符串作为键名,所以element被自动转为字符串[object HTMLDivElement]。
为了解决这个问题,ES6 提供了 Map 数据结构。它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。也就是说,Object 结构提供了“字符串—值”的对应,Map 结构提供了“值—值”的对应,是一种更完善的 Hash 结构实现。如果你需要“键值对”的数据结构,Map 比 Object 更合适。
1 | const m = new Map(); |
上面代码使用 Map 结构的set方法,将对象o当作m的一个键,然后又使用get方法读取这个键,接着使用delete方法删除了这个键,clear直接清空m。
面的例子展示了如何向 Map 添加成员。作为构造函数,Map 也可以接受一个数组作为参数。该数组的成员是一个个表示键值对的数组。
1 | const map = new Map([ |
上面代码在新建 Map 实例时,就指定了两个键name和title。
Map构造函数接受数组作为参数,实际上执行的是下面的算法。
1 | const items = [ |
迭代器Iterator 和 for…of 循环
Iterator
JavaScript 原有的表示“集合”的数据结构,主要是数组(Array)和对象(Object),ES6 又添加了Map和Set。这样就有了四种数据集合,用户还可以组合使用它们,定义自己的数据结构,比如数组的成员是Map,Map的成员是对象。这样就需要一种统一的接口机制,来处理所有不同的数据结构。
遍历器(Iterator)就是这样一种机制。它是一种接口,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署 Iterator 接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。
Iterator 的作用有三个:一是为各种数据结构,提供一个统一的、简便的访问接口;二是使得数据结构的成员能够按某种次序排列;三是 ES6 创造了一种新的遍历命令for...of循环,Iterator 接口主要供for...of消费。
Iterator 的遍历过程是这样的。
(1)创建一个指针对象,指向当前数据结构的起始位置。也就是说,遍历器对象本质上,就是一个指针对象。
(2)第一次调用指针对象的next方法,可以将指针指向数据结构的第一个成员。
(3)第二次调用指针对象的next方法,指针就指向数据结构的第二个成员。
(4)不断调用指针对象的next方法,直到它指向数据结构的结束位置。
每一次调用next方法,都会返回数据结构的当前成员的信息。具体来说,就是返回一个包含value和done两个属性的对象。其中,value属性是当前成员的值,done属性是一个布尔值,表示遍历是否结束。
下面是一个模拟next方法返回值的例子。
1 | const items = ['one','two','three'] |
上面代码中,变量items是一个数组,原生就具有遍历器接口,部署在items的Symbol.iterator属性上面。所以,调用这个属性,就得到遍历器对象。
ES6 规定,默认的 Iterator 接口部署在数据结构的Symbol.iterator属性,或者说,一个数据结构只要具有Symbol.iterator属性,就可以认为是“可遍历的”(iterable)。Symbol.iterator属性本身是一个函数,就是当前数据结构默认的遍历器生成函数。执行这个函数,就会返回一个遍历器。至于属性名Symbol.iterator,它是一个表达式,返回Symbol对象的iterator属性,这是一个预定义好的、类型为 Symbol 的特殊值,所以要放在方括号内。
原生具备 Iterator 接口的数据结构如下。
- Array
- Map
- Set
- String
- TypedArray
- 函数的 arguments 对象
- NodeList 对象
一个对象如果要具备可被for...of循环调用的 Iterator 接口,就必须在Symbol.iterator的属性上部署遍历器生成方法(原型链上的对象具有该方法也可)。
1 | class RangeIterator { |
上面代码是一个类部署 Iterator 接口的写法。Symbol.iterator属性对应一个函数,执行后返回当前对象的遍历器对象。
for…of
ES6 借鉴 C++、Java、C# 和 Python 语言,引入了for...of循环,作为遍历所有数据结构的统一的方法。
一个数据结构只要部署了Symbol.iterator属性,就被视为具有 iterator 接口,就可以用for...of循环遍历它的成员。也就是说,for...of循环内部调用的是数据结构的Symbol.iterator方法。
for...of循环可以使用的范围包括数组、Set 和 Map 结构、某些类似数组的对象(比如arguments对象、DOM NodeList 对象)、后文的 Generator 对象,以及字符串。
数组
数组原生具备iterator接口(即默认部署了Symbol.iterator属性),for...of循环本质上就是调用这个接口产生的遍历器,可以用下面的代码证明。
1 | const arr = ['red', 'green', 'blue']; |
上面代码中,空对象obj部署了数组arr的Symbol.iterator属性,结果obj的for...of循环,产生了与arr完全一样的结果。
for...of循环可以代替数组实例的forEach方法。
1 | const arr = ['red', 'green', 'blue']; |
JavaScript 原有的for...in循环,只能获得对象的键名,不能直接获取键值。ES6 提供for...of循环,允许遍历获得键值。
1 | var arr = ['a', 'b', 'c', 'd']; |
上面代码表明,for...in循环读取键名,for...of循环读取键值。如果要通过for...of循环,获取数组的索引,可以借助数组实例的entries方法和keys方法(参见《数组的扩展》一章)。
for...of循环调用遍历器接口,数组的遍历器接口只返回具有数字索引的属性。这一点跟for...in循环也不一样。
1 | let arr = [3, 5, 7]; |
上面代码中,for...of循环不会返回数组arr的foo属性。
Set 和 Map 结构
Set 和 Map 结构也原生具有 Iterator 接口,可以直接使用for...of循环。
1 | var engines = new Set(["Gecko", "Trident", "Webkit", "Webkit"]); |
上面代码演示了如何遍历 Set 结构和 Map 结构。值得注意的地方有两个,首先,遍历的顺序是按照各个成员被添加进数据结构的顺序。其次,Set 结构遍历时,返回的是一个值,而 Map 结构遍历时,返回的是一个数组,该数组的两个成员分别为当前 Map 成员的键名和键值。
1 | let map = new Map().set('a', 1).set('b', 2); |
对象
对于普通的对象,for...of结构不能直接使用,会报错,必须部署了 Iterator 接口后才能使用。但是,这样情况下,for...in循环依然可以用来遍历键名。
1 | let es6 = { |
上面代码表示,对于普通的对象,for...in循环可以遍历键名,for...of循环会报错。
一种解决方法是,使用Object.keys方法将对象的键名生成一个数组,然后遍历这个数组。
1 | for (var key of Object.keys(someObject)) { |
另一个方法是使用 Generator 函数将对象重新包装一下。
1 | const obj = { a: 1, b: 2, c: 3 } |
生成器Generator
Generator 函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同。
Generator 函数有多种理解角度。语法上,首先可以把它理解成,Generator 函数是一个状态机,封装了多个内部状态。
执行 Generator 函数会返回一个遍历器对象,也就是说,Generator 函数除了状态机,还是一个遍历器对象生成函数。返回的遍历器对象,可以依次遍历 Generator 函数内部的每一个状态。
形式上,Generator 函数是一个普通函数,但是有两个特征。一是,function关键字与函数名之间有一个星号;二是,函数体内部使用yield表达式,定义不同的内部状态(yield在英语里的意思就是“产出”)。
1 | function* helloWorldGenerator() { |
上面代码定义了一个 Generator 函数helloWorldGenerator,它内部有两个yield表达式(hello和world),即该函数有三个状态:hello,world 和 return 语句(结束执行)。
然后,Generator 函数的调用方法与普通函数一样,也是在函数名后面加上一对圆括号。不同的是,调用 Generator 函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象,也就是上一章介绍的遍历器对象(Iterator Object)。
下一步,必须调用遍历器对象的next方法,使得指针移向下一个状态。也就是说,每次调用next方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个yield表达式(或return语句)为止。换言之,Generator 函数是分段执行的,yield表达式是暂停执行的标记,而next方法可以恢复执行。
1 | hw.next() |
上面代码一共调用了四次next方法。
第一次调用,Generator 函数开始执行,直到遇到第一个yield表达式为止。next方法返回一个对象,它的value属性就是当前yield表达式的值hello,done属性的值false,表示遍历还没有结束。
第二次调用,Generator 函数从上次yield表达式停下的地方,一直执行到下一个yield表达式。next方法返回的对象的value属性就是当前yield表达式的值world,done属性的值false,表示遍历还没有结束。
第三次调用,Generator 函数从上次yield表达式停下的地方,一直执行到return语句(如果没有return语句,就执行到函数结束)。next方法返回的对象的value属性,就是紧跟在return语句后面的表达式的值(如果没有return语句,则value属性的值为undefined),done属性的值true,表示遍历已经结束。
第四次调用,此时 Generator 函数已经运行完毕,next方法返回对象的value属性为undefined,done属性为true。以后再调用next方法,返回的都是这个值。
总结一下,调用 Generator 函数,返回一个遍历器对象,代表 Generator 函数的内部指针。以后,每次调用遍历器对象的next方法,就会返回一个有着value和done两个属性的对象。value属性表示当前的内部状态的值,是yield表达式后面那个表达式的值;done属性是一个布尔值,表示是否遍历结束。
ES6 没有规定,function关键字与函数名之间的星号,写在哪个位置。这导致下面的写法都能通过。
1 | function * foo(x, y) { ··· } |
由于 Generator 函数仍然是普通函数,所以一般的写法是上面的第三种,即星号紧跟在function关键字后面。本书也采用这种写法。
yield 表达式
由于 Generator 函数返回的遍历器对象,只有调用next方法才会遍历下一个内部状态,所以其实提供了一种可以暂停执行的函数。yield表达式就是暂停标志。
遍历器对象的next方法的运行逻辑如下。
(1)遇到yield表达式,就暂停执行后面的操作,并将紧跟在yield后面的那个表达式的值,作为返回的对象的value属性值。
(2)下一次调用next方法时,再继续往下执行,直到遇到下一个yield表达式。
(3)如果没有再遇到新的yield表达式,就一直运行到函数结束,直到return语句为止,并将return语句后面的表达式的值,作为返回的对象的value属性值。
(4)如果该函数没有return语句,则返回的对象的value属性值为undefined。
需要注意的是,yield表达式后面的表达式,只有当调用next方法、内部指针指向该语句时才会执行,因此等于为 JavaScript 提供了手动的“惰性求值”(Lazy Evaluation)的语法功能。
1 | function* gen() { |
上面代码中,yield后面的表达式123 + 456,不会立即求值,只会在next方法将指针移到这一句时,才会求值。
yield表达式与return语句既有相似之处,也有区别。相似之处在于,都能返回紧跟在语句后面的那个表达式的值。区别在于每次遇到yield,函数暂停执行,下一次再从该位置继续向后执行,而return语句不具备位置记忆的功能。一个函数里面,只能执行一次(或者说一个)return语句,但是可以执行多次(或者说多个)yield表达式。正常函数只能返回一个值,因为只能执行一次return;Generator 函数可以返回一系列的值,因为可以有任意多个yield。从另一个角度看,也可以说 Generator 生成了一系列的值,这也就是它的名称的来历(英语中,generator 这个词是“生成器”的意思)。
Generator 函数可以不用yield表达式,这时就变成了一个单纯的暂缓执行函数。
1 | function* f() { |
上面代码中,函数f如果是普通函数,在为变量generator赋值时就会执行。但是,函数f是一个 Generator 函数,就变成只有调用next方法时,函数f才会执行。
另外需要注意,yield表达式只能用在 Generator 函数里面,用在其他地方都会报错。
1 | (function (){ |
上面代码在一个普通函数中使用yield表达式,结果产生一个句法错误。
另外,yield表达式如果用在另一个表达式之中,必须放在圆括号里面。
1 | function* demo() { |
yield表达式用作函数参数或放在赋值表达式的右边,可以不加括号。
1 | function* demo() { |
应用
Generator 可以暂停函数执行,返回任意表达式的值。这种特点使得 Generator 有多种应用场景。
异步操作的同步化表达
Generator 函数的暂停执行的效果,意味着可以把异步操作写在yield表达式里面,等到调用next方法时再往后执行。这实际上等同于不需要写回调函数了,因为异步操作的后续操作可以放在yield表达式下面,反正要等到调用next方法时再执行。所以,Generator 函数的一个重要实际意义就是用来处理异步操作,改写回调函数。
1 | function* loadUI() { |
上面代码中,第一次调用loadUI函数时,该函数不会执行,仅返回一个遍历器。下一次对该遍历器调用next方法,则会显示Loading界面(showLoadingScreen),并且异步加载数据(loadUIDataAsynchronously)。等到数据加载完成,再一次使用next方法,则会隐藏Loading界面。可以看到,这种写法的好处是所有Loading界面的逻辑,都被封装在一个函数,按部就班非常清晰。
Ajax 是典型的异步操作,通过 Generator 函数部署 Ajax 操作,可以用同步的方式表达。
1 | function* main() { |
上面代码的main函数,就是通过 Ajax 操作获取数据。可以看到,除了多了一个yield,它几乎与同步操作的写法完全一样。注意,makeAjaxCall函数中的next方法,必须加上response参数,因为yield表达式,本身是没有值的,总是等于undefined。
下面是另一个例子,通过 Generator 函数逐行读取文本文件。
1 | function* numbers() { |
上面代码打开文本文件,使用yield表达式可以手动逐行读取文件。
控制流管理
如果有一个多步操作非常耗时,采用回调函数,可能会写成下面这样。
1 | step1(function (value1) { |
采用 Promise 改写上面的代码。
1 | Promise.resolve(step1) |
上面代码已经把回调函数,改成了直线执行的形式,但是加入了大量 Promise 的语法。Generator 函数可以进一步改善代码运行流程。
1 | function* longRunningTask(value1) { |
然后,使用一个函数,按次序自动执行所有步骤。
1 | scheduler(longRunningTask(initialValue)); |
注意,上面这种做法,只适合同步操作,即所有的task都必须是同步的,不能有异步操作。因为这里的代码一得到返回值,就继续往下执行,没有判断异步操作何时完成。如果要控制异步的操作流程,详见后面的《异步操作》一章。
下面,利用for...of循环会自动依次执行yield命令的特性,提供一种更一般的控制流管理的方法。
1 | let steps = [step1Func, step2Func, step3Func]; |
上面代码中,数组steps封装了一个任务的多个步骤,Generator 函数iterateSteps则是依次为这些步骤加上yield命令。
将任务分解成步骤之后,还可以将项目分解成多个依次执行的任务。
1 | let jobs = [job1, job2, job3]; |
上面代码中,数组jobs封装了一个项目的多个任务,Generator 函数iterateJobs则是依次为这些任务加上yield*命令。
最后,就可以用for...of循环一次性依次执行所有任务的所有步骤。
1 | for (var step of iterateJobs(jobs)){ |
再次提醒,上面的做法只能用于所有步骤都是同步操作的情况,不能有异步操作的步骤。如果想要依次执行异步的步骤,必须使用后面的《异步操作》一章介绍的方法。
for...of的本质是一个while循环,所以上面的代码实质上执行的是下面的逻辑。
1 | var it = iterateJobs(jobs); |
为不具备 Iterator 接口提供遍历操作
1 | function* objectEntries(obj){ |
利用 Generator 函数,可以在任意对象上部署 Iterator 接口。
1 | // 定义一个名为 iterEntries 的生成器函数,它接受一个对象作为参数 |
上述代码中,myObj是一个普通对象,通过iterEntries函数,就有了 Iterator 接口。也就是说,可以在任意对象上部署next方法。
下面是一个对数组部署 Iterator 接口的例子,尽管数组原生具有这个接口。
1 | // 定义一个名为 makeSimpleGenerator 的生成器函数,它接受一个数组作为参数 |
Promise对象
Promise 的含义
Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。它由社区最早提出和实现,ES6 将其写进了语言标准,统一了用法,原生提供了Promise对象。
所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。
Promise对象有以下两个特点。
(1)对象的状态不受外界影响。Promise对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。这也是Promise这个名字的由来,它的英语意思就是“承诺”,表示其他手段无法改变。
(2)一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise对象的状态改变,只有两种可能:从pending变为fulfilled和从pending变为rejected。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果,这时就称为 resolved(已定型)。如果改变已经发生了,你再对Promise对象添加回调函数,也会立即得到这个结果。这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。
注意,为了行文方便,本章后面的resolved统一只指fulfilled状态,不包含rejected状态。
有了Promise对象,就可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。此外,Promise对象提供统一的接口,使得控制异步操作更加容易。
Promise也有一些缺点。首先,无法取消Promise,一旦新建它就会立即执行,无法中途取消。其次,如果不设置回调函数,Promise内部抛出的错误,不会反应到外部。第三,当处于pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。
如果某些事件不断地反复发生,一般来说,使用 Stream 模式是比部署Promise更好的选择。
基本用法
ES6 规定,Promise对象是一个构造函数,用来生成Promise实例。
下面代码创造了一个Promise实例。
1 | const promise = new Promise(function(resolve, reject) { |
Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolve和reject。它们是两个函数,由 JavaScript 引擎提供,不用自己部署。
resolve函数的作用是,将Promise对象的状态从“未完成”变为“成功”(即从 pending 变为 resolved),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去;reject函数的作用是,将Promise对象的状态从“未完成”变为“失败”(即从 pending 变为 rejected),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。
Promise实例生成以后,可以用then方法分别指定resolved状态和rejected状态的回调函数。
1 | promise.then(function(value) { |
then方法可以接受两个回调函数作为参数。第一个回调函数是Promise对象的状态变为resolved时调用,第二个回调函数是Promise对象的状态变为rejected时调用。这两个函数都是可选的,不一定要提供。它们都接受Promise对象传出的值作为参数。
下面是一个Promise对象的简单例子。
1 | const pro = new Promise(function(resolve,reject){ |
上面代码不能传参
1 | function timeOut(ms){ |
上面代码中,timeOut方法返回一个Promise实例,表示一段时间以后才会发生的结果。过了指定的时间(ms参数)以后,Promise实例的状态变为resolved,就会触发then方法绑定的回调函数。
Promise 新建后就会立即执行。
1 | let promise = new Promise(function(resolve, reject) { |
上面代码中,Promise 新建后立即执行,所以首先输出的是Promise。然后,then方法指定的回调函数,将在当前脚本所有同步任务执行完才会执行,所以resolved最后输出。
下面是异步加载图片的例子。
1 | function loadImageAsync(url) { |
上面代码中,使用Promise包装了一个图片加载的异步操作。如果加载成功,就调用resolve方法,否则就调用reject方法。
下面是一个用Promise对象实现的 Ajax 操作的例子。
1 | getJSON = function(url){ |
上面代码中,getJSON是对 XMLHttpRequest 对象的封装,用于发出一个针对 JSON 数据的 HTTP 请求,并且返回一个Promise对象。需要注意的是,在getJSON内部,resolve函数和reject函数调用时,都带有参数。
Promise.prototype.then()
Promise 实例具有then方法,也就是说,then方法是定义在原型对象Promise.prototype上的。它的作用是为 Promise 实例添加状态改变时的回调函数。前面说过,then方法的第一个参数是resolved状态的回调函数,第二个参数是rejected状态的回调函数,它们都是可选的。
then方法返回的是一个新的Promise实例(注意,不是原来那个Promise实例)。因此可以采用链式写法,即then方法后面再调用另一个then方法。
1 | getJSON("/posts.json").then(function(json) { |
上面的代码使用then方法,依次指定了两个回调函数。第一个回调函数完成以后,会将返回结果作为参数,传入第二个回调函数。
采用链式的then,可以指定一组按照次序调用的回调函数。这时,前一个回调函数,有可能返回的还是一个Promise对象(即有异步操作),这时后一个回调函数,就会等待该Promise对象的状态发生变化,才会被调用。
1 | getJSON("/post/1.json").then(function(post) { |
上面代码中,第一个then方法指定的回调函数,返回的是另一个Promise对象。这时,第二个then方法指定的回调函数,就会等待这个新的Promise对象状态发生变化。如果变为resolved,就调用第一个回调函数,如果状态变为rejected,就调用第二个回调函数。
如果采用箭头函数,上面的代码可以写得更简洁。
1 | getJSON("/post/1.json").then( |
Promise.prototype.catch()
Promise.prototype.catch()方法是.then(null, rejection)或.then(undefined, rejection)的别名,用于指定发生错误时的回调函数。
1 | getJSON('/posts.json').then(function(posts) { |
上面代码中,getJSON()方法返回一个 Promise 对象,如果该对象状态变为resolved,则会调用then()方法指定的回调函数;如果异步操作抛出错误,状态就会变为rejected,就会调用catch()方法指定的回调函数,处理这个错误。另外,then()方法指定的回调函数,如果运行中抛出错误,也会被catch()方法捕获。
1 | p.then((val) => console.log('fulfilled:', val)) |
下面是一个例子。
1 | const promise = new Promise(function(resolve, reject) { |
上面代码中,promise抛出一个错误,就被catch()方法指定的回调函数捕获。注意,上面的写法与下面两种写法是等价的。
1 | // 写法一 |
比较上面两种写法,可以发现reject()方法的作用,等同于抛出错误。
如果 Promise 状态已经变成resolved,再抛出错误是无效的。
1 | const promise = new Promise(function(resolve, reject) { |
上面代码中,Promise 在resolve语句后面,再抛出错误,不会被捕获,等于没有抛出。因为 Promise 的状态一旦改变,就永久保持该状态,不会再变了。
Promise 对象的错误具有“冒泡”性质,会一直向后传递,直到被捕获为止。也就是说,错误总是会被下一个catch语句捕获。
1 | getJSON('/post/1.json').then(function(post) { |
上面代码中,一共有三个 Promise 对象:一个由getJSON()产生,两个由then()产生。它们之中任何一个抛出的错误,都会被最后一个catch()捕获。
一般来说,不要在then()方法里面定义 Reject 状态的回调函数(即then的第二个参数),总是使用catch方法。
1 | // bad |
上面代码中,第二种写法要好于第一种写法,理由是第二种写法可以捕获前面then方法执行中的错误,也更接近同步的写法(try/catch)。因此,建议总是使用catch()方法,而不使用then()方法的第二个参数。
Promise.all()
Promise.all()方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。
1 | const p = Promise.all([p1, p2, p3]); |
上面代码中,Promise.all()方法接受一个数组作为参数,p1、p2、p3都是 Promise 实例,如果不是,就会先调用下面讲到的Promise.resolve方法,将参数转为 Promise 实例,再进一步处理。另外,Promise.all()方法的参数可以不是数组,但必须具有 Iterator 接口,且返回的每个成员都是 Promise 实例。
p的状态由p1、p2、p3决定,分成两种情况。
(1)只有p1、p2、p3的状态都变成fulfilled,p的状态才会变成fulfilled,此时p1、p2、p3的返回值组成一个数组,传递给p的回调函数。
(2)只要p1、p2、p3之中有一个被rejected,p的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。
下面是一个具体的例子。
1 | // 生成一个Promise对象的数组 |
上面代码中,promises是包含 6 个 Promise 实例的数组,只有这 6 个实例的状态都变成fulfilled,或者其中有一个变为rejected,才会调用Promise.all方法后面的回调函数。
下面是另一个例子。
1 | // 比如 加载静态资源 |
Promise.race()
Promise.race()方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例。
1 | const p = Promise.race([p1, p2, p3]); |
上面代码中,只要p1、p2、p3之中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p的回调函数。
Promise.race()方法的参数与Promise.all()方法一样,如果不是 Promise 实例,就会先调用下面讲到的Promise.resolve()方法,将参数转为 Promise 实例,再进一步处理。
下面是一个例子,如果指定时间内没有获得结果,就将 Promise 的状态变为reject,否则变为resolve。
1 | const p = Promise.race([ |
上面代码中,如果 5 秒之内fetch方法无法返回结果,变量p的状态就会变为rejected,从而触发catch方法指定的回调函数。
下面是一个代码示例:
1 | // race() 某个异步请求设置超时时间,并在超时后执行相应操作 |
Promise.prototype.finally()
finally()方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。该方法是 ES2018 引入标准的。
1 | promise |
上面代码中,不管promise最后的状态,在执行完then或catch指定的回调函数以后,都会执行finally方法指定的回调函数。
下面是一个例子,服务器使用 Promise 处理请求,然后使用finally方法关掉服务器。
1 | // 示例:不管请求结果如何,请求完成都关闭服务器监听 |
finally方法的回调函数不接受任何参数,这意味着没有办法知道,前面的 Promise 状态到底是fulfilled还是rejected。这表明,finally方法里面的操作,应该是与状态无关的,不依赖于 Promise 的执行结果。
async函数
async 函数是什么?一句话,它就是 Generator 函数的语法糖。
一比较就会发现,async函数就是将 Generator 函数的星号(*)替换成async,将yield替换成await,仅此而已。
async函数对 Generator 函数的改进,体现在以下四点。
(1)内置执行器。
Generator 函数的执行必须靠执行器,所以才有了co模块,而async函数自带执行器。也就是说,async函数的执行,与普通函数一模一样,只要一行。
1 | asyncReadFile(); |
上面的代码调用了asyncReadFile函数,然后它就会自动执行,输出最后结果。这完全不像 Generator 函数,需要调用next方法,或者用co模块,才能真正执行,得到最后结果。
(2)更好的语义。
async和await,比起星号和yield,语义更清楚了。async表示函数里有异步操作,await表示紧跟在后面的表达式需要等待结果。
(3)更广的适用性。
co模块约定,yield命令后面只能是 Thunk 函数或 Promise 对象,而async函数的await命令后面,可以是 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时会自动转成立即 resolved 的 Promise 对象)。
(4)返回值是 Promise。
async函数的返回值是 Promise 对象,这比 Generator 函数的返回值是 Iterator 对象方便多了。你可以用then方法指定下一步的操作。
进一步说,async函数完全可以看作多个异步操作,包装成的一个 Promise 对象,而await命令就是内部then命令的语法糖。
基本用法
async函数返回一个 Promise 对象,可以使用then方法添加回调函数。当函数执行的时候,一旦遇到await就会先返回,等到异步操作完成,再接着执行函数体内后面的语句。
下面是一个例子。
1 | async function f() { |
函数前面的async关键字,表明该函数内部有异步操作。调用该函数时,会立即返回一个Promise对象。
下面是另一个例子,指定多少毫秒后输出一个值。
1 | function timeout(ms) { |
上面代码指定 50 毫秒以后,输出hello world。
由于async函数返回的是 Promise 对象,可以作为await命令的参数。所以,上面的例子也可以写成下面的形式。
1 | async function timeout(ms) { |
async 函数有多种使用形式。
1 | // 函数声明 |
语法
async函数的语法规则总体上比较简单,难点是错误处理机制。
返回 Promise 对象
async函数返回一个 Promise 对象。
async函数内部return语句返回的值,会成为then方法回调函数的参数。
1 | async function f() { |
上面代码中,函数f内部return命令返回的值,会被then方法回调函数接收到。
async函数内部抛出错误,会导致返回的 Promise 对象变为reject状态。抛出的错误对象会被catch方法回调函数接收到。
1 | async function f() { |
错误处理
如果await后面的异步操作出错,那么等同于async函数返回的 Promise 对象被reject。
1 | async function f() { |
上面代码中,async函数f执行后,await后面的 Promise 对象会抛出一个错误对象,导致catch方法的回调函数被调用,它的参数就是抛出的错误对象。具体的执行机制,可以参考后文的“async 函数的实现原理”。
防止出错的方法,也是将其放在try...catch代码块之中。
1 | async function f() { |
如果有多个await命令,可以统一放在try...catch结构中。
1 | async function main() { |
下面的例子使用try...catch结构,实现多次重复尝试。
1 | const superagent = require('superagent'); |
上面代码中,如果await操作成功,就会使用break语句退出循环;如果失败,会被catch语句捕捉,然后进入下一轮循环。
实例:按顺序完成异步操作
实际开发中,经常遇到一组异步操作,需要按照顺序完成。比如,依次远程读取一组 URL,然后按照读取的顺序输出结果。
Promise 的写法如下。
1 | function logInOrder(urls) { |
上面代码使用fetch方法,同时远程读取一组 URL。每个fetch操作都返回一个 Promise 对象,放入textPromises数组。然后,reduce方法依次处理每个 Promise 对象,然后使用then,将所有 Promise 对象连起来,因此就可以依次输出结果。
这种写法不太直观,可读性比较差。下面是 async 函数实现。
1 | async function logInOrder(urls) { |
上面代码确实大大简化,问题是所有远程操作都是继发。只有前一个 URL 返回结果,才会去读取下一个 URL,这样做效率很差,非常浪费时间。我们需要的是并发发出远程请求。
1 | async function logInOrder(urls) { |
上面代码中,虽然map方法的参数是async函数,但它是并发执行的,因为只有async函数内部是继发执行,外部不受影响。后面的for..of循环内部使用了await,因此实现了按顺序输出。
实例:获取天气 现在now数据
1 | getJSON = function (url) { |
class类
基本用法
JavaScript 语言中,生成实例对象的传统方法是通过构造函数。下面是一个例子。
1 | function Point(x, y) { |
上面这种写法跟传统的面向对象语言(比如 C++ 和 Java)差异很大,很容易让新学习这门语言的程序员感到困惑。
ES6 提供了更接近传统语言的写法,引入了 Class(类)这个概念,作为对象的模板。通过class关键字,可以定义类。
基本上,ES6 的class可以看作只是一个语法糖,它的绝大部分功能,ES5 都可以做到,新的class写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。上面的代码用 ES6 的class改写,就是下面这样。
1 | class Point { |
上面代码定义了一个“类”,可以看到里面有一个constructor()方法,这就是构造方法,而this关键字则代表实例对象。这种新的 Class 写法,本质上与本章开头的 ES5 的构造函数Point是一致的。
Point类除了构造方法,还定义了一个toString()方法。注意,定义toString()方法的时候,前面不需要加上function这个关键字,直接把函数定义放进去了就可以了。另外,方法与方法之间不需要逗号分隔,加了会报错。
ES6 的类,完全可以看作构造函数的另一种写法。
1 | class Point { |
上面代码表明,类的数据类型就是函数,类本身就指向构造函数。
使用的时候,也是直接对类使用new命令,跟构造函数的用法完全一致。
1 | class Bar { |
构造函数的prototype属性,在 ES6 的“类”上面继续存在。事实上,类的所有方法都定义在类的prototype属性上面。
1 | class Point { |
上面代码中,constructor()、toString()、toValue()这三个方法,其实都是定义在Point.prototype上面。
示例:
1 | class Person { |
继承
Class 可以通过extends关键字实现继承,让子类继承父类的属性和方法。extends 的写法比 ES5 的原型链继承,要清晰和方便很多。
1 | class Point { |
上面示例中,Point是父类,ColorPoint是子类,它通过extends关键字,继承了Point类的所有属性和方法。但是由于没有部署任何代码,所以这两个类完全一样,等于复制了一个Point类。
1 | class Animal { |
super这个关键字,既可以当作函数使用,也可以当作对象使用。在这两种情况下,它的用法完全不同。
调用super()的作用是形成子类的this对象,把父类的实例属性和方法放到这个this对象上面。子类在调用super()之前,是没有this对象的,任何对this的操作都要放在super()的后面。
注意,这里的super虽然代表了父类的构造函数,但是因为返回的是子类的this(即子类的实例对象),所以super内部的this代表子类的实例,而不是父类的实例,这里的super()相当于A.prototype.constructor.call(this)(在子类的this上运行父类的构造函数)。
ES6模块化实现
ES6 模块不是对象,而是通过export命令显式指定输出的代码,再通过import命令输入。
模块功能主要由两个命令构成:export和import。export命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能。
export
一个模块就是一个独立的文件。该文件内部的所有变量,外部无法获取。如果你希望外部能够读取模块内部的某个变量,就必须使用export关键字输出该变量。下面是一个 JS 文件,里面使用export命令输出变量。
1 | // profile.js |
上面代码是profile.js文件,保存了用户信息。ES6 将其视为一个模块,里面用export命令对外部输出了三个变量。
export的写法,除了像上面这样,还有另外一种。
1 | // profile.js |
上面代码在export命令后面,使用大括号指定所要输出的一组变量。它与前一种写法(直接放置在var语句前)是等价的,但是应该优先考虑使用这种写法。因为这样就可以在脚本尾部,一眼看清楚输出了哪些变量。
export命令除了输出变量,还可以输出函数或类(class)。
1 | export function multiply(x, y) { |
上面代码对外输出一个函数multiply。
通常情况下,export输出的变量就是本来的名字,但是可以使用as关键字重命名。
1 | function v1() { ... } |
上面代码使用as关键字,重命名了函数v1和v2的对外接口。重命名后,v2可以用不同的名字输出两次。
需要特别注意的是,export命令规定的是对外的接口,必须与模块内部的变量建立一一对应关系。
1 | // 报错 |
上面两种写法都会报错,因为没有提供对外的接口。第一种写法直接输出 1,第二种写法通过变量m,还是直接输出 1。1只是一个值,不是接口。正确的写法是下面这样。
1 | // 写法一 |
上面三种写法都是正确的,规定了对外的接口m。其他脚本可以通过这个接口,取到值1。它们的实质是,在接口名与模块内部变量之间,建立了一一对应的关系。
同样的,function和class的输出,也必须遵守这样的写法。
1 | // 报错 |
目前,export 命令能够对外输出的就是三种接口:函数(Functions), 类(Classes),var、let、const 声明的变量(Variables)。
另外,export语句输出的接口,与其对应的值是动态绑定关系,即通过该接口,可以取到模块内部实时的值。
1 | export var foo = 'bar'; |
上面代码输出变量foo,值为bar,500 毫秒之后变成baz。
import
profile.js文件内容
1 | export var firstName = 'Michael'; |
使用export命令定义了模块的对外接口以后,其他 JS 文件就可以通过import命令加载这个模块。
前提我们需要在当前script指定类型type="module"
1 | <script type="module"> |
上面代码的import命令,用于加载profile.js文件,并从中输入变量。import命令接受一对大括号,里面指定要从其他模块导入的变量名。大括号里面的变量名,必须与被导入模块(profile.js)对外接口的名称相同。
如果想为输入的变量重新取一个名字,import命令要使用as关键字,将输入的变量重命名。
1 | import { lastName as surname } from './profile.js'; |
模块整体加载
除了指定加载某个输出值,还可以使用整体加载,即用星号(*)指定一个对象,所有输出值都加载在这个对象上面。
1 | <script type="module"> |
export default
为了给用户提供方便,让他们不用阅读文档就能加载模块,就要用到export default命令,为模块指定默认输出。
1 | export var firstName = 'Michael'; |
上面代码是一个模块文件profile.js,它的默认输出是一个函数。
其他模块加载该模块时,import命令可以为该匿名函数指定任意名字。
1 | import customName from './profile.js'; |
上面代码的import命令,可以用任意名称指向profile.js输出的方法,这时就不需要知道原模块输出的函数名。需要注意的是,这时import命令后面,不使用大括号。









