ECMAScript 6 语法关键点

通过阅读阮一峰老师著作《ECMAScript 6 入门》,提取和总结的 ECMAScript6 语法关键点。取一些需要注意的地方,供学习和参考。

目录

✎ 变量声明

概述

  • 代码块,双大括号,拥有块级作用域

  • 可不将大括号写在行首,取消 JavaScript 将其解释为代码块

    1
    2
    3
    // 参考:变量的解构赋值(对象)
    let x;
    ({x} = {x: 1});
  • let 声明的变量只在它所在的代码块有效

  • for 循环中用 let 声明的 i 只在循环体内有效,且为父作用域内,与函数体作用域独立

    1
    2
    3
    4
    5
    6
    7
    for (let i = 0; i < 3; i++) {
    let i = 'abc';
    console.log(i);
    }
    // abc
    // abc
    // abc
  • let 不存在变量申明提升

    1
    2
    console.log(bar); // 报错 ReferenceError
    let bar = 2;
  • TDZ(Temporal Dead Zone),暂时性死区

    只要块级作用域内存在 let 命令,它所声明的变量就 “绑定”(binding)这个区域,不再受外部的影响。

    1
    2
    3
    4
    5
    6
    let tmp = 123;

    if (true) {
    tmp = 'abc'; // ReferenceError: tmp is not defined
    let tmp;
    }

    “暂时性死区” 也意味着 typeof 不再是一个百分之百安全的操作

    1
    2
    typeof x; // ReferenceError: x is not defined
    let x;
  • 不允许重复声明

  • const 实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址不得改动

  • 如果真的想将对象冻结,应该使用 Object.freeze 方法

    1
    2
    3
    4
    5
    const foo = Object.freeze({});

    // 常规模式时,下面一行不起作用;
    // 严格模式时,该行会报错
    foo.prop = 123;
  • letconstclass 命令声明的全局变量,不属于顶层对象(如 window)的属性

    1
    2
    3
    4
    5
    6
    7
    let a = 1;
    // 如果在 Node 的 REPL 环境,可以写成 global.a
    // 或者采用通用方法,写成 this.a
    window.a; // 1

    let b = 1;
    window.b; // undefined

✎ 变量的解构赋值

概述

  • 默认值:解构赋值指定默认值时,ES6 内部使用严格相等运算符(===),判断一个位置是否有值。所以,如果一个数组成员不严格等于 undefined,默认值是不会生效的

    1
    2
    3
    4
    5
    let [x = 1] = [undefined];
    x // 1

    let [x = 1] = [null];
    x // null
  • 函数的参数也可以使用解构赋值

    1
    2
    3
    4
    5
    function add([x, y]){
    return x + y;
    }

    add([1, 2]); // 3

技巧

  • 交换变量的值

    1
    2
    3
    4
    let x = 1;
    let y = 2;

    [x, y] = [y, x];
  • 遍历 Map 结构

    任何部署了 Iterator 接口的对象,都可以用 for...of 循环遍历。Map 结构原生支持 Iterator 接口,配合变量的解构赋值,获取键名和键值就非常方便。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    const map = new Map();
    map.set('first', 'hello');
    map.set('second', 'world');

    for (let [key, value] of map) {
    console.log(key + " is " + value);
    }
    // first is hello
    // second is world

    // 如果只想获取键名,或者只想获取键值,可以写成下面这样。

    // 获取键名
    for (let [key] of map) {
    // ...
    }

    // 获取键值
    for (let [,value] of map) {
    // ...
    }

✎ 字符串的扩展

  • codePointAt 方法会正确返回32位UTF-16 字符的码点(10进制)。对于那些两个字节储存的常规字符,它的返回结果与 charCodeAt 方法相同

  • 使用 for...of 循环,因为它会正确识别32位UTF-16字符(可以识别大于 0xFFFF 的码点)

    1
    2
    3
    4
    5
    6
    let s = '𠮷a';
    for (let ch of s) {
    console.log(ch.codePointAt(0).toString(16));
    }
    // 20bb7
    // 61
  • codePointAt 方法是测试一个字符由两个字节还是由四个字节组成的最简单方法

    1
    2
    3
    4
    5
    6
    function is32Bit(c) {
    return c.codePointAt(0) > 0xFFFF;
    }

    is32Bit("𠮷"); // true
    is32Bit("a") // false

✎ 正则表达式的扩展

RegExp 构造函数

如果 RegExp 构造函数第一个参数是一个正则对象,那么可以使用第二个参数指定修饰符。 而且,返回的正则表达式会忽略原有的正则表达式的修饰符,只使用新指定的修饰符

1
2
new RegExp(/abc/ig, 'i').flags
// "i"

u 修饰符

  • ES6 对正则表达式添加了 u 修饰符,含义为 “Unicode 模式”,用来正确处理大于 \uFFFFUnicode 字符。也就是说,会正确处理 4个字节 的 UTF-16 编码

    1
    2
    /^\uD83D/u.test('\uD83D\uDC2A'); // false
    /^\uD83D/.test('\uD83D\uDC2A'); // true
  • 一旦加上u修饰符号,就会修改下面这些正则表达式的行为

    • 点字符

      对于码点大于0xFFFFUnicode 字符,点字符不能识别,必须加上 u 修饰符

      1
      2
      3
      4
      let s = '𠮷';

      /^.$/.test(s); // false
      /^.$/u.test(s); // true
    • Unicode 字符表示法

      ES6 新增了使用大括号表示 Unicode 字符,这种表示法在正则表达式中必须加上u修饰符,才能识别当中的大括号,否则会被解读为量词

      1
      2
      3
      /\u{61}/.test('a'); // false
      /\u{61}/u.test('a'); // true
      /\u{20BB7}/u.test('𠮷'); // true

✎ 数值的扩展

Number.isFinite 和 Number.isNaN

它们与传统的全局方法 isFinite()isNaN() 的区别在于,传统方法先调用 Number() 将非数值的值转为数值,再进行判断, 而这两个新方法只对数值有效,Number.isFinite() 对于非数值一律返回 false,而 Number.isNaN() 只有对于 NaN 才返回 true非NaN 一律返回 false

1
return typeof value === 'number' && global_isFinite(value);

Number.parseInt 和 Number.parseFloat

window 对象上移植到 Number 对象上,行为不变

Number.isInteger

Number.isInteger()用来判断一个值是否为整数。需要注意的是,在JavaScript内部,整数和浮点数是同样的储存方法

1
2
3
4
5
Number.isInteger(25);   // true
Number.isInteger(25.0); // true
Number.isInteger(25.1); // false
Number.isInteger("15"); // false
Number.isInteger(true); // false

Number.EPSILON

  • ES6 在Number对象上面,新增一个极小的常量 Number.EPSILON。根据规格,它表示 1 与大于 1 的最小浮点数之间的差。
  • Number.EPSILON 实际上是 JavaScript 能够表示的最小精度。误差如果小于这个值,就可以认为已经没有意义了,即不存在误差了

Math.sign

Math.sign 方法用来判断一个数到底是正数、负数、还是零。对于非数值,会先将其转换为数值。

它会返回五种值:

  • 参数为正数,返回 +1
  • 参数为负数,返回 -1
  • 参数为 0,返 回0
  • 参数为-0,返回 -0;
  • 其他值,返回 NaN

✎ 函数的扩展

函数参数的默认值

参数默认值不是传值的,而是每次都重新计算默认值表达式的值。也就是说,参数默认值是惰性求值的

1
2
3
4
5
6
7
8
9
let x = 99;
function foo(p = x + 1) {
console.log(p);
}

foo(); // 100

x = 100;
foo(); // 101

函数的 length 属性

length 属性的含义是,该函数预期传入的参数个数。某个参数指定默认值以后,预期传入的参数个数就不包括这个参数了 指定了默认值以后,函数的length属性,将返回没有指定默认值的参数个数。也就是说,指定了默认值后,length 属性将失真。

1
2
3
4
5
6
(function (a) {}).length; // 1
(function (a = 5) {}).length; // 0
(function (a, b, c = 5) {}).length; // 2
(function(...args) {}).length; // 0
(function (a = 0, b, c) {}).length; // 0
(function (a, b = 1, c) {}).length; // 1

函数定义

由于大括号被解释为代码块,所以如果箭头函数直接返回一个对象,必须在对象外面加上括号,否则会报错。

1
2
3
4
5
// 报错
let getTempItem = id => { id: id, name: "Temp" };

// 不报错
let getTempItem = id => ({ id: id, name: "Temp" });

尾调用优化

定义:某个函数的最后一步是调用另一个函数

1
2
3
function f(x){
return g(x);
}

上面代码中,函数 f 的最后一步是调用函数 g,这就叫尾调用

以下三种情况,都不属于尾调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 情况一
function f(x){
let y = g(x);
return y;
}

// 情况二
function f(x){
return g(x) + 1;
}

// 情况三
function f(x){
g(x);
}

尾调用由于是函数的最后一步操作,所以不需要保留外层函数的调用帧,因为调用位置、内部变量等信息都不会再用到了,只要直接用内层函数的调用帧,取代外层函数的调用帧就可以了 “尾调用优化” 对递归操作意义重大,所以一些函数式编程语言将其写入了语言规格。 ES6 是如此,第一次明确规定,所有 ECMAScript 的实现,都必须部署 “尾调用优化”。 这就是说,ES6 中只要使用尾递归,就不会发生栈溢出,相对节省内存

✎ 数组的扩展

扩展运算符的应用

扩展运算符有一个重要的好处,那就是能够正确识别 4个字节 的 Unicode 字符

1
2
'x\uD83D\uDE80y'.length; // 4
[...'x\uD83D\uDE80y'].length; // 3

上面代码的第一种写法,JavaScript会将 4个字节 的 Unicode 字符,识别为 2 个字符,采用扩展运算符就没有这个问题。 因此,正确返回字符串长度的函数,可以像下面这样写

1
2
3
4
5
function length(str) {
return [...str].length;
}

length('x\uD83D\uDE80y'); // 3

凡是涉及到操作4个字节的 Unicode 字符的函数,都有这个问题。因此,最好都用扩展运算符改写

1
2
3
4
5
6
7
let str = 'x\uD83D\uDE80y';

str.split('').reverse().join('');
// 'y\uDE80\uD83Dx'

[...str].reverse().join('');
// 'y\uD83D\uDE80x'

上面代码中,如果不用扩展运算符,字符串的 reverse 操作就不正确

Array.from

Array.from() 的另一个应用是,将字符串转为数组,然后返回字符串的长度。因为它能正确处理各种 Unicode 字符,可以避免JavaScript将大于 \uFFFFUnicode字符,算作两个字符的bug。

1
2
3
function countSymbols(string) {
return Array.from(string).length;
}

Array.of

Array.of 方法用于将一组值,转换为数组

1
2
3
Array.of(3, 11, 8); // [3, 11, 8]
Array.of(3); // [3]
Array.of(3).length; // 1

copyWithin()

组实例的 copyWithin 方法,在当前数组内部,将指定位置的成员复制到其他位置(会覆盖原有成员),然后返回当前数组。 也就是说,使用这个方法,会修改当前数组

1
2
3
4
Array.prototype.copyWithin(target, start = 0, end = this.length);

[1, 2, 3, 4, 5].copyWithin(0, 3);
// [4, 5, 3, 4, 5]

find() 和 findIndex()

  • 数组实例的 find 方法,用于找出第一个符合条件的数组成员。它的参数是一个回调函数,所有数组成员依次执行该回调函数,直到找出第一个返回值为 true 的成员,然后返回该成员。如果没有符合条件的成员,则返回 undefined

    1
    2
    3
    [1, 5, 10, 15].find(function(value, index, arr) {
    return value > 9;
    }) // 10

    上面代码中,find 方法的回调函数可以接受三个参数,依次为当前的值、当前的位置和原数组。

  • 数组实例的 findIndex 方法的用法与 find 方法非常类似,返回第一个符合条件的数组成员的位置,如果所有成员都不符合条件,则返回 -1

    1
    2
    3
    [1, 5, 10, 15].findIndex(function(value, index, arr) {
    return value > 9;
    }) // 2

fill()

fill 方法用于空数组的初始化非常方便。数组中已有的元素,会被全部抹去

1
2
3
4
5
['a', 'b', 'c'].fill(7);
// [7, 7, 7]

['a', 'b', 'c'].fill(7, 1, 2);
// ['a', 7, 'c']

entries()、keys() 和 values()

用于遍历数组。它们都返回一个遍历器对象(详见《Iterator》一章),可以用 for...of 循环进行遍历,唯一的区别是:

  • keys() 是对键名的遍历
  • values()是对键值的遍历
  • entries()是对键值对的遍历

includes()

Array.prototype.includes 方法返回一个布尔值,表示某个数组是否包含给定的值,与字符串的 includes 方法类似。ES2016 引入了该方法

1
2
3
[1, 2, 3].includes(2);     // true
[1, 2, 3].includes(4); // false
[1, 2, NaN].includes(NaN); // true

数组的空位

  • 数组的空位指,数组的某一个位置没有任何值。比如,Array 构造函数返回的数组都是空位

    1
    new Array(3) // [, , ,]

    上面代码中,Array(3)返回一个具有 3 个空位的数组。

    注意,空位不是 undefined,一个位置的值等于 undefined,依然是有值的。空位是没有任何值,in 运算符可以说明这一点

    1
    2
    0 in [undefined, undefined, undefined] // true
    0 in [, , ,] // false
  • ES5 对空位的处理,已经很不一致了,大多数情况下会忽略空位:

    • forEach(), filter(), every()some() 都会跳过空位
    • map() 会跳过空位,但会保留这个值
    • join()toString() 会将空位视为 undefined,而 undefinednull 会被处理成空字符串
  • ES6 则是明确将空位转为 undefined

    • Array.from、扩展运算符(...) 方法会将数组的空位,转为 undefined,也就是说,这个方法不会忽略空位

      1
      2
      Array.from(['a',,'b']);
      // [ "a", undefined, "b" ]
    • fill() 会将空位视为正常的数组位置

      1
      new Array(3).fill('a') // ["a","a","a"]
    • copyWithin() 会连空位一起拷贝

      1
      [,'a','b',,].copyWithin(2,0) // [,"a",,"a"]
    • for...of 循环也会遍历空位

      1
      2
      3
      4
      5
      6
      let arr = [, ,];
      for (let i of arr) {
      console.log(1);
      }
      // 1
      // 1
    • entries()keys()values()find()findIndex() 会将空位处理成 undefined

  • 由于空位的处理规则非常不统一,所以建议避免出现空位

✎ 对象的扩展

属性名表达式

可用 表达式 作为对象的属性名,即把表达式放在方括号内:

1
2
3
4
5
let propKey = 'foo';
let obj = {
[propKey]: true,
['a' + 'bc']: 123
};

表达式还可以用于定义方法名:

1
2
3
4
5
6
7
let obj = {
['h' + 'ello']() {
return 'hi';
}
};

obj.hello() // hi

Object.is()

“Same-value equality”,同值相等,Object.is 就是部署这个算法的新方法。 它用来比较两个值是否严格相等,与严格比较运算符(===)的行为基本一致。不同之处只有两个:

  • +0 不等于 -0
  • NaN 等于自身
1
2
3
4
5
+0 === -0; //true
NaN === NaN; // false

Object.is(+0, -0); // false
Object.is(NaN, NaN); // true

Object.assign()

  • Object.assign 方法用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target)如果目标对象与源对象有同名属性,或多个源对象有同名属性,则后面的属性会覆盖前面的属性

    1
    2
    3
    4
    5
    6
    7
    const target = { a: 1, b: 1 };

    const source1 = { b: 2, c: 2 };
    const source2 = { c: 3 };

    Object.assign(target, source1, source2);
    target // {a:1, b:2, c:3}
  • 如果只有一个参数,Object.assign 会直接返回该参数

    1
    2
    const obj = {a: 1};
    Object.assign(obj) === obj; // true
  • 如果该参数不是对象,则会先转成对象,然后返回。

    1
    typeof Object.assign(2); // "object"
  • 数值、字符串和布尔值不在首参数,不会报错。但是,除了字符串会以数组形式,拷贝入目标对象,其他值都不会产生效果。因为只有字符串的包装对象,会产生可枚举属性。

    1
    2
    3
    4
    5
    6
    const v1 = 'abc';
    const v2 = true;
    const v3 = 10;

    const obj = Object.assign({}, v1, v2, v3);
    console.log(obj); // { "0": "a", "1": "b", "2": "c" }

注意

  1. Object.assign 方法实行的是浅拷贝,而不是深拷贝。也就是说,如果源对象某个属性的值是对象,那么目标对象拷贝得到的是这个对象的引用

    1
    2
    3
    4
    5
    const obj1 = {a: {b: 1}};
    const obj2 = Object.assign({}, obj1);

    obj1.a.b = 2;
    obj2.a.b // 2
  2. 同名属性的替换

    对于嵌套的对象,一旦遇到同名属性,Object.assign 的处理方法是替换,而不是添加

    1
    2
    3
    4
    const target = { a: { b: 'c', d: 'e' } };
    const source = { a: { b: 'hello' } };
    Object.assign(target, source);
    // { a: { b: 'hello' } }
  3. 数组的处理

    Object.assign 可以用来处理数组,但是会把数组视为对象

    1
    2
    3
    let arr1 = [1, 2, 3, 4];
    Object.assign(arr1, [4, 5]);
    arr1 // [4, 5, 3, 4]
  4. 取值函数的处理

    Object.assign 只能进行值的复制,如果要复制的值是一个取值函数,那么将求值后再复制

    1
    2
    3
    4
    5
    6
    7
    const source = {
    get foo() { return 1 }
    };
    const target = {};

    Object.assign(target, source)
    // { foo: 1 }

    上面代码中,source 对象的 foo 属性是一个取值函数,Object.assign 不会复制这个取值函数,只会拿到值以后,将这个值复制过去

属性的遍历

属性遍历的次序规则:

  1. 首先遍历所有数值键,按照数值升序排列
  2. 其次遍历所有字符串键,按照加入时间升序排列
  3. 最后遍历所有 Symbol 键,按照加入时间升序排列

super 关键字

指向当前对象的原型对象

super 关键字表示原型对象时,只能用在对象的方法之中,用在其他地方都会报错。 目前,只有对象方法的简写法可以让 JavaScript 引擎确认,定义的是对象的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 报错
const obj = {
foo: super.foo
}

// 报错
const obj = {
foo: () => super.foo
}

// 报错
const obj = {
foo: function () {
return super.foo
}
}

// 正确
const obj = {
find() {
return super.foo;
}
};

对象的扩展运算符

解构赋值

对象的解构赋值用于从一个对象取值,相当于将所有可遍历的、但尚未被读取的属性,分配到指定的对象上面。 所有的键和它们的值,都会拷贝到新对象上面。

1
2
3
4
let { x, y, ...z } = { x: 1, y: 2, a: 3, b: 4 };
x // 1
y // 2
z // { a: 3, b: 4 }

扩展运算符的解构赋值,不能复制继承自原型对象的属性。

1
2
3
4
5
6
let o1 = { a: 1 };
let o2 = { b: 2 };
o2.__proto__ = o1;
let { ...o3 } = o2;
o3 // { b: 2 }
o3.a // undefined

扩展运算符

扩展运算符(...)用于取出参数对象的所有可遍历属性,拷贝到当前对象之中。

扩展运算符可以用于合并两个对象

1
2
3
4
let ab = { ...a, ...b };

// 等同于
let ab = Object.assign({}, a, b);

与数组的扩展运算符一样,对象的扩展运算符后面可以跟表达式

1
2
3
4
const obj = {
...(x > 1 ? {a: 1} : {}),
b: 2,
};

扩展运算符的参数对象之中,如果有取值函数 get,这个函数是会执行的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 并不会抛出错误,因为 x 属性只是被定义,但没执行
let aWithXGetter = {
...a,
get x() {
throw new Error('not throw yet');
}
};

// 会抛出错误,因为 x 属性被执行了
let runtimeError = {
...a,
...{
get x() {
throw new Error('throw now');
}
}
};

✎ Symbol

概述

  • ES6 引入了一种新的原始数据类型 Symbol,表示独一无二的值。它是 JavaScript 语言的第七种数据类型,前六种是:

    • undefined
    • null
    • 布尔值(Boolean
    • 字符串(String
    • 数值(Number
    • 对象(Object
  • Symbol 函数的参数只是表示对当前 Symbol 值的描述,因此相同参数的 Symbol 函数的返回值是不相等的

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 没有参数的情况
    let s1 = Symbol();
    let s2 = Symbol();

    s1 === s2 // false

    // 有参数的情况
    let s1 = Symbol('foo');
    let s2 = Symbol('foo');

    s1 === s2 // false
  • Symbol 值不能与其他类型的值进行运算

    1
    2
    3
    4
    let sym = Symbol('My symbol');

    let b = "your symbol is " + sym;
    // TypeError: Cannot convert a Symbol value to a string
  • Symbol 值可以显式转为字符串,也可以转为布尔值,但是不能转为数值

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    let sym = Symbol('My symbol');

    String(sym); // 'Symbol(My symbol)'
    sym.toString(); // 'Symbol(My symbol)'

    let sym2 = Symbol();
    Boolean(sym2); // true
    !sym2 // false

    if (sym2) {
    // ...
    }

    Number(sym2); // TypeError
    sym2 + 2 // TypeError

作为属性名的 Symbol

Symbol 值作为对象属性名时,不能用点运算符,该属性还是公开属性,不是私有属性

属性名的遍历

Symbol 作为属性名,该属性不会出现在 for...infor...of 循环中, 也不会被 Object.keys()Object.getOwnPropertyNames()JSON.stringify() 返回。 但是,它也不是私有属性,有一个 Object.getOwnPropertySymbols 方法,可以获取指定对象的所有 Symbol 属性名

Reflect.ownKeys() 方法可以返回所有类型的键名,包括常规键名和 Symbol 键名

1
2
3
4
5
6
7
8
let obj = {
[Symbol('my_key')]: 1,
enum: 2,
nonEnum: 3
};

Reflect.ownKeys(obj)
// ["enum", "nonEnum", Symbol(my_key)]

Symbol.for()

Symbol.for 方法接受一个字符串作为参数,然后搜索有没有以该参数作为名称的 Symbol 值。 如果有,就返回这个 Symbol 值,否则就新建并返回一个以该字符串为名称的 Symbol

1
2
3
4
let s1 = Symbol.for('foo');
let s2 = Symbol.for('foo');

s1 === s2 // true

Symbol.for()Symbol() 这两种写法,都会生成新的 Symbol。它们的区别是,前者会被登记在全局环境中供搜索,后者不会。 Symbol.for() 不会每次调用就返回一个新的 Symbol 类型的值,而是会先检查给定的 key 是否已经存在,如果不存在才会新建一个值。 比如,如果你调用 Symbol.for("cat") 30 次,每次都会返回同一个 Symbol 值, 但是调用 Symbol("cat") 30 次,会返回 30 个不同的 Symbol

1
2
Symbol.for("bar") === Symbol.for("bar"); // true
Symbol("bar") === Symbol("bar"); // false

Symbol.keyFor()

Symbol.keyFor 方法返回一个已登记的 Symbol 类型值的 key

1
2
3
4
5
let s1 = Symbol.for("foo");
Symbol.keyFor(s1); // "foo"

let s2 = Symbol("foo");
Symbol.keyFor(s2); // undefined

上面代码中,变量 s2 属于未登记的 Symbol 值,所以返回 undefined

需要注意的是,Symbol.forSymbol 值登记的名字,是全局环境的,可以在不同的 iframeservice worker 中取到同一个值

1
2
3
4
5
iframe = document.createElement('iframe');
iframe.src = String(window.location);
document.body.appendChild(iframe);

iframe.contentWindow.Symbol.for('foo') === Symbol.for('foo'); // true

上面代码中,iframe 窗口生成的 Symbol 值,可以在主页面得到

✎ Set 和 Map 数据结构

Set

ES6 提供了新的数据结构 Set。它类似于数组,但是成员的值都是唯一的,没有重复的值

WeakSet

  • WeakSet 结构与 Set 类似,也是不重复的值的集合。但是,它与 Set 有两个区别:

    • 首先,WeakSet 的成员只能是对象,而不能是其他类型的值

      1
      2
      3
      4
      5
      const ws = new WeakSet();
      ws.add(1);
      // TypeError: Invalid value used in weak set
      ws.add(Symbol());
      // TypeError: invalid value used in weak set

      上面代码试图向 WeakSet 添加一个数值和 Symbol 值,结果报错,因为 WeakSet 只能放置对象。

    • 其次,WeakSet 中的对象都是弱引用,即垃圾回收机制不考虑 WeakSet 对该对象的引用。 也就是说,如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存,不考虑该对象还存在于 WeakSet 之中

      这是因为垃圾回收机制依赖引用计数,如果一个值的引用次数不为0,垃圾回收机制就不会释放这块内存。 结束使用该值之后,有时会忘记取消引用,导致内存无法释放,进而可能会引发内存泄漏。 WeakSet 里面的引用,都不计入垃圾回收机制,所以就不存在这个问题。 因此,WeakSet 适合临时存放一组对象,以及存放跟对象绑定的信息。 只要这些对象在外部消失,它在 WeakSet 里面的引用就会自动消失 由于上面这个特点,WeakSet 的成员是不适合引用的,因为它会随时消失。 另外,由于 WeakSet 内部有多少个成员,取决于垃圾回收机制有没有运行,运行前后很可能成员个数是不一样的, 而垃圾回收机制何时运行是不可预测的,因此 ES6 规定 WeakSet 不可遍历

  • WeakSet 可以接受一个数组或类似数组的对象作为参数(实际上,任何具有 Iterable 接口的对象,都可以作为 WeakSet 的参数)。该数组的所有成员(必须都为对象),都会自动成为 WeakSet 实例对象的成员

    1
    2
    3
    const a = [[1, 2], [3, 4]];
    const ws = new WeakSet(a);
    // WeakSet {[1, 2], [3, 4]}
  • WeakSet 的一个用处,是储存 DOM 节点,而不用担心这些节点从文档移除时,会引发内存泄漏

Map

ES6 提供了 Map 数据结构。它类似于对象,也是键值对的集合,但是 “键” 的范围不限于字符串,各种类型的值(包括对象)都可以当作键。 也就是说,Object 结构提供了 “字符串 — 值” 的对应,Map 结构提供了 “值 — 值” 的对应,是一种更完善的 Hash 结构实现。 如果你需要 “键值对” 的数据结构,MapObject 更合适

作为构造函数,Map 也可以接受一个数组作为参数。该数组的成员是一个个表示键值对的数组

1
2
3
4
5
6
7
8
9
10
const map = new Map([
['name', '张三'],
['title', 'Author']
]);

map.size; // 2
map.has('name'); // true
map.get('name'); // "张三"
map.has('title'); // true
map.get('title'); // "Author"

WeakMap

WeakMap 的专用场合就是,它的键所对应的对象,可能会在将来消失。WeakMap 结构有助于防止内存泄漏

WeakMapMap 的区别:

  1. WeakMap 只接受对象作为键名(null除外),不接受其他类型的值作为键名

    1
    2
    3
    4
    const map = new WeakMap();
    map.set(1, 2); // TypeError: Invalid value used as weak map key
    map.set(Symbol(), 2); // TypeError: Invalid value used as weak map key
    map.set(null, 2); // TypeError: Invalid value used as weak map key
  2. WeakMap 的键名所指向的对象,不计入垃圾回收机制

    WeakMap 的设计目的在于,有时我们想在某个对象上面存放一些数据,但是这会形成对于这个对象的引用。 WeakMap 就是为了解决这个问题而诞生的,它的键名所引用的对象都是弱引用,即垃圾回收机制不将该引用考虑在内。 因此,只要所引用的对象的其他引用都被清除,垃圾回收机制就会释放该对象所占用的内存。 也就是说,一旦不再需要,WeakMap 里面的键名对象和所对应的键值对会自动消失,不用手动删除引用。

    1
    2
    3
    4
    5
    const wm = new WeakMap();
    const el = document.getElementById('example');

    wm.set(el, 'some information');
    wm.get(el) // "some information"
  3. WeakMapMap 在 API 上的区别主要是两个:

    • 没有遍历操作(即没有 key()values()entries() 方法),也没有 size 属性
    • 无法清空,即不支持clear方法

✎ Proxy

概述

  • Proxy 用于修改某些操作的默认行为,等同于在语言层面做出修改,所以属于一种 “元编程”(meta programming),即对编程语言进行编程。 Proxy 可以理解成,在目标对象之前架设一层 “拦截”,外界对该对象的访问,都必须先通过这层拦截, 因此提供了一种机制,可以对外界的访问进行过滤和改写

    1
    2
    3
    4
    5
    6
    7
    8
    9
    let proxy = new Proxy({}, {
    get: function(target, property) {
    return 35;
    }
    });

    proxy.time // 35
    proxy.name // 35
    proxy.title // 35
  • 如果 handler 没有设置任何拦截,那就等同于直接通向原对象

    1
    2
    3
    4
    5
    let target = {};
    let handler = {};
    let proxy = new Proxy(target, handler);
    proxy.a = 'b';
    target.a // "b"

Proxy 实例的方法

get()

如果一个属性不可配置(configurable)和不可写(writable),则该属性不能被代理,通过 Proxy 对象访问该属性会报错

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const target = Object.defineProperties({}, {
foo: {
value: 123,
writable: false,
configurable: false
},
});

const handler = {
get(target, propKey) {
return 'abc';
}
};

const proxy = new Proxy(target, handler);

proxy.foo // TypeError: Invariant check failed

this 问题

虽然 Proxy 可以代理针对目标对象的访问,但它不是目标对象的透明代理,即不做任何拦截的情况下,也无法保证与目标对象的行为一致。 主要原因就是在 Proxy 代理的情况下,目标对象内部的 this 关键字会指向 Proxy 代理

1
2
3
4
5
6
7
8
9
10
11
const target = {
m: function () {
console.log(this === proxy);
}
};
const handler = {};

const proxy = new Proxy(target, handler);

target.m(); // false
proxy.m(); // true

✎ Reflect

概述

  • Reflect 对象与 Proxy 对象一样,也是 ES6 为了操作对象而提供的新 API

  • Reflect 对象的设计目的有这样几个:

    • Object 对象的一些明显属于语言内部的方法(比如 Object.defineProperty),放到 Reflect 对象上。 现阶段,某些方法同时在 ObjectReflect 对象上部署,未来的新方法将只部署在 Reflect 对象上。 也就是说,从 Reflect 对象上可以拿到语言内部的方法

    • 修改某些 Object 方法的返回结果,让其变得更合理。 比如,Object.defineProperty(obj, name, desc) 在无法定义属性时,会抛出一个错误, 而 Reflect.defineProperty(obj, name, desc) 则会返回 false

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      // 老写法
      try {
      Object.defineProperty(target, property, attributes);
      // success
      } catch (e) {
      // failure
      }

      // 新写法
      if (Reflect.defineProperty(target, property, attributes)) {
      // success
      } else {
      // failure
      }
    • Object 操作都变成函数行为。某些 Object 操作是命令式,比如 name in objdelete obj[name] , 而 Reflect.has(obj, name)Reflect.deleteProperty(obj, name) 让它们变成了函数行为

      1
      2
      3
      4
      5
      // 老写法
      'assign' in Object; // true

      // 新写法
      Reflect.has(Object, 'assign'); // true
    • Reflect 对象的方法与 Proxy 对象的方法一一对应,只要是 Proxy 对象的方法,就能在 Reflect 对象上找到对应的方法

      1
      2
      3
      4
      5
      6
      7
      8
      9
      Proxy(target, {
      set: function(target, name, value, receiver) {
      let success = Reflect.set(target,name, value, receiver);
      if (success) {
      log('property ' + name + ' on ' + target + ' set to ' + value);
      }
      return success;
      }
      });

✎ Promise 对象

Promise 的含义

  • 所谓 Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理

  • 特点

    • 对象的状态不受外界影响

      Promise 对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和 rejected(已失败)。 只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态

    • 一旦状态改变,就不会再变,任何时候都可以得到这个结果

      Promise 对象的状态改变,只有两种可能:从 pending 变为 fulfilled 和从 pending 变为 rejected

  • 缺点

    • 无法取消 Promise,一旦新建它就会立即执行,无法中途取消
    • 如果不设置回调函数,Promise 内部抛出的错误,不会反应到外部
    • 当处于 pending 状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)

基本用法

  • 如果调用 resolve 函数和 reject 函数时带有参数,那么它们的参数会被传递给回调函数。 (如果参数是 Promise 的实例,那么参数的状态就会传递给改 Promise 的状态,即参数实例的状态决定了改实例的状态)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    const p1 = new Promise(function (resolve, reject) {
    setTimeout(() => reject(new Error('fail')), 3000);
    });

    const p2 = new Promise(function (resolve, reject) {
    setTimeout(() => resolve(p1), 1000);
    });

    p2
    .then(result => console.log(result))
    .catch(error => console.error(error));

    // Error: fail
  • 调用 resolvereject 并不会终结 Promise 的参数函数的执行

    1
    2
    3
    4
    5
    6
    7
    8
    9
    new Promise((resolve, reject) => {
    resolve(1);
    console.log(2);
    }).then(r => {
    console.log(r);
    });

    // 2
    // 1

    说明:立即 resolvedPromise 是在本轮事件循环的末尾执行,总是晚于本轮循环的同步任务

Promise.prototype.then()

采用链式的 then,可以指定一组按照次序调用的回调函数。 这时,前一个回调函数,有可能返回的还是一个 Promise 对象(即有异步操作), 这时后一个回调函数,就会等待该 Promise 对象的状态发生变化,才会被调用

1
2
3
4
5
6
7
8
getJSON("/post/1.json")
.then(
post => getJSON(post.commentURL)
)
.then(
comments => console.log("resolved: ", comments),
err => console.warn("rejected: ", err)
);

Promise.race()

Promise.race 方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例

1
const p = Promise.race([p1, p2, p3]);

上面代码中,只要 p1、p2、p3 之中有一个实例率先改变状态,p 的状态就跟着改变。 那个率先改变的 Promise 实例的返回值,就传递给 p 的回调函数

1
2
3
4
5
6
7
8
const p = Promise.race([
fetch('/resource-that-may-take-a-while'),
new Promise((resolve, reject) => {
setTimeout(() => reject(new Error('request timeout')), 5000)
})
]);
p.then(response => console.log(response));
p.catch(error => console.log(error));

上面代码中,如果 5 秒之内 fetch 方法无法返回结果,变量 p 的状态就会变为 rejected,从而触发 catch 方法指定的回调函数

Promise.resolve()

将现有对象转为 Promise 对象

Promise.resolve 方法的参数分成四种情况:

  1. 参数是一个 Promise 实例

    如果参数是 Promise 实例,那么 Promise.resolve 将不做任何修改、原封不动地返回这个实例

  2. 参数是一个 thenable 对象

    thenable 对象指的是具有 then 方法的对象

    1
    2
    3
    4
    5
    let thenable = {
    then: function(resolve, reject) {
    resolve(42);
    }
    };

    Promise.resolve 方法会将这个对象转为 Promise 对象,然后就立即执行 thenable 对象的 then 方法

  3. 参数不是具有 then 方法的对象,或根本就不是对象

    如果参数是一个原始值,或者是一个不具有 then 方法的对象,则 Promise.resolve 方法返回一个新的 Promise 对象,状态为 resolved

    1
    2
    3
    4
    5
    6
    const p = Promise.resolve('Hello');

    p.then(function (s){
    console.log(s)
    });
    // Hello
  4. 不带有任何参数

    Promise.resolve 方法允许调用时不带参数,直接返回一个 resolved 状态的 Promise 对象

    注意:立即 resolvePromise 对象,是在本轮 “事件循环”(event loop)的结束时,而不是在下一轮 “事件循环” 的开始时

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    setTimeout(function () {
    console.log('three');
    }, 0);

    Promise.resolve().then(function () {
    console.log('two');
    });

    console.log('one');

    // one
    // two
    // three

    上面代码中, setTimeout(fn, 0) 在下一轮 “事件循环” 开始时执行, Promise.resolve() 在本轮 “事件循环” 结束时执行, console.log('one') 则是立即执行,因此最先输出

Promise.reject()

Promise.reject(reason) 方法也会返回一个新的 Promise 实例,该实例的状态为 rejected

注意:Promise.reject() 方法的参数,会原封不动地作为 reject 的理由,变成后续方法的参数。这一点与 Promise.resolve 方法不一致

1
2
3
4
5
6
7
8
9
10
11
const thenable = {
then(resolve, reject) {
reject('出错了');
}
};

Promise.reject(thenable)
.catch(e => {
console.log(e === thenable)
})
// true

上面代码中,Promise.reject 方法的参数是一个 thenable 对象,执行以后,后面 catch 方法的参数不是 reject 抛出的 “出错了” 这个字符串,而是 thenable 对象。

附加方法

done()

1
2
3
4
5
6
7
Promise.prototype.done = Promise.prototype.done || function (onFulfilled, onRejected) {
this.then(onFulfilled, onRejected)
.catch((reason) => {
// 抛出一个全局错误
setTimeout(() => { throw reason }, 0);
});
};

finally()

1
2
3
4
5
6
7
Promise.prototype.finally = Promise.prototype.finally || function (callback) {
let P = this.constructor;
return this.then(
value => P.resolve(callback()).then(() => value),
reason => P.resolve(callback()).then(() => { throw reason })
);
};

✎ Iterator 和 for...of 循环

Iterator(遍历器)的概念

遍历器(Iterator)是一种接口,为各种不同的数据结构提供统一的访问机制。 任何数据结构只要部署 Iterator 接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)

Iterator 的作用:

  1. 为各种数据结构,提供一个统一的、简便的访问接口
  2. 使得数据结构的成员能够按某种次序排列
  3. ES6 创造了一种新的遍历命令 for...of 循环,Iterator 接口主要供 for...of 消费

默认 Iterator 接口

  • 当使用 for...of 循环遍历某种数据结构时,该循环会自动去寻找 Iterator 接口。一种数据结构只要部署了 Iterator 接口,我们就称这种数据结构是 “可遍历的”(iterable

  • ES6 规定,默认的 Iterator 接口部署在数据结构的 Symbol.iterator 属性,或者说,一个数据结构只要具有 Symbol.iterator 属性,就可以认为是“可遍历的”(iterable)。Symbol.iterator 属性本身是一个函数,就是当前数据结构默认的遍历器生成函数。执行这个函数,就会返回一个遍历器。至于属性名 Symbol.iterator,它是一个表达式,返回 Symbol 对象的 iterator 属性,这是一个预定义好的、类型为 Symbol 的特殊值,所以要放在方括号内

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    class RangeIterator {
    constructor(start, stop) {
    this.value = start;
    this.stop = stop;
    }

    next() {
    let value = this.value;
    if (value < this.stop) {
    this.value++;
    return {done: false, value: value};
    }
    return {done: true, value: undefined};
    }

    [Symbol.iterator]() { return this; }
    }

    function range(start, stop) {
    return new RangeIterator(start, stop);
    }

    for (let val of range(0, 3)) {
    console.log(val); // 0, 1, 2
    }
  • 凡是部署了 Symbol.iterator 属性的数据结构,就称为部署了遍历器接口。调用这个接口,就会返回一个遍历器对象

  • 对于类似数组的对象(存在数值键名和 length 属性),部署 Iterator 接口,有一个简便方法,就是 Symbol.iterator 方法直接引用数组的 Iterator 接口

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    let iterable = {
    0: 'a',
    1: 'b',
    2: 'c',
    length: 3,
    [Symbol.iterator]: Array.prototype[Symbol.iterator]
    };
    for (let item of iterable) {
    console.log(item); // 'a', 'b', 'c'
    }

调用 Iterator 接口的场合

  • 解构赋值

    ArraySet 结构进行解构赋值时,会默认调用 Symbol.iterator 方法

    1
    2
    let set = new Set().add('a').add('b').add('c');
    let [x, y] = set; // x='a'; y='b'
  • 扩展运算符

    1
    2
    let str = 'hello';
    [...str]; // ['h','e','l','l','o']
  • yield*

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    let generator = function* () {
    yield 1;
    yield* [2,3,4];
    yield 5;
    };

    let iterator = generator();

    iterator.next(); // { value: 1, done: false }
    iterator.next(); // { value: 2, done: false }
    iterator.next(); // { value: 3, done: false }
    iterator.next(); // { value: 4, done: false }
    iterator.next(); // { value: 5, done: false }
    iterator.next(); // { value: undefined, done: true }
  • 其他场合

    由于数组的遍历会调用遍历器接口,所以任何接受数组作为参数的场合,其实都调用了遍历器接口:

    • for...of
    • Array.from()
    • Map(), Set(), WeakMap(), WeakSet()
    • Promise.all()
    • Promise.race()

遍历器对象的 return()throw()

  • 遍历器对象除了具有 next 方法,还可以具有 return 方法和 throw 方法。如果你自己写遍历器对象生成函数,那么 next 方法是必须部署的,return 方法和 throw 方法是否部署是可选的

  • return 方法的使用场合是:如果 for...of 循环提前退出(通常是因为出错,或者有 break 语句或 continue 语句),就会调用 return 方法。如果一个对象在完成遍历前,需要清理或释放资源,就可以部署 return 方法。注意,return 方法必须返回一个对象,这是 Generator 规格决定的

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    function readLinesSync(file) {
    return {
    next() {
    return { done: false };
    },
    return() {
    file.close();
    return { done: true };
    },
    };
    }

    // 情况一
    for (let line of readLinesSync(fileName)) {
    console.log(line);
    break;
    }

    // 情况二
    for (let line of readLinesSync(fileName)) {
    console.log(line);
    continue;
    }

    // 情况三
    for (let line of readLinesSync(fileName)) {
    console.log(line);
    throw new Error();
    }
  • throw 方法主要是配合 Generator 函数使用,一般的遍历器对象用不到这个方法

for...of 循环

数组

  • 一个数据结构只要部署了 Symbol.iterator 属性,就被视为具有 iterator 接口,就可以用 for...of 循环遍历它的成员。也就是说,for...of 循环内部调用的是数据结构的 Symbol.iterator 方法

  • for...of 循环可以使用的范围包括 ArraySetMap 结构、某些类似数组的对象(比如 arguments 对象、DOM NodeList 对象)、Generator 对象,以及 字符串

  • for...in 的区别:

    • for...in 循环,只能获得对象的键名,不能直接获取键值。ES6 提供 for...of 循环,允许遍历获得键值
    • for...of 循环调用遍历器接口,数组的遍历器接口只返回具有数字索引的属性
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    let arr = [3, 5, 7];
    arr.foo = 'hello';

    for (let i in arr) {
    console.log(i); // "0", "1", "2", "foo"
    }

    for (let i of arr) {
    console.log(i); // "3", "5", "7", 不包含 'foo'
    }

字符串

对于字符串来说,for...of 循环还有一个特点,就是会正确识别 32 位 UTF-16 字符

1
2
3
4
5
for (let x of 'a\uD83D\uDC0A') {
console.log(x);
}
// 'a'
// '\uD83D\uDC0A'

✎ Generator 函数的语法

简介

  1. Generator 函数是 ES6 提供的一种异步编程解决方案
  2. Generator 函数是一个状态机,封装了多个内部状态
  3. Generator 函数执行后返回一个遍历器对象,也就是说,Generator 函数除了状态机,还是一个遍历器对象生成函数

语法:

1
2
3
4
function * foo(x, y) { /*···*/ }
function *foo(x, y) { /*···*/ }
function* foo(x, y) { /*···*/ }
function*foo(x, y) { /*···*/ }

总结:

调用 Generator 函数,返回一个遍历器对象,代表 Generator 函数的内部指针。 以后,每次调用遍历器对象的 next 方法,就会返回一个有着 valuedone 两个属性的对象: value 属性表示当前的内部状态的值,是 yield 表达式后面那个表达式的值; done 属性是一个布尔值,表示是否遍历结束

yield 表达式

  • yield 表达式 表示遍历器对象的暂停标识
  • yield 表达式 如果用在另一个表达式之中,必须放在 圆括号 里面
  • yield 表达式 本身没有返回值,或者说总是返回 undefined
  • 紧跟在 yield 后面的那个表达式的值,作为 next() 返回的对象的 value 属性值
  • return 后表示遍历结束状态时,返回值作为遍历后 value 的值
1
2
3
4
function* Gen() {
console.log('Hello' + yield 123); // SyntaxError
console.log('Hello' + (yield 123)); // OK
}

与 Iterator 接口的关系

任意一个对象的 Symbol.iterator 方法,等于该对象的遍历器生成函数,调用该函数会返回该对象的一个遍历器对象

由于 Generator 函数就是遍历器生成函数,因此可以把 Generator 赋值给对象的 Symbol.iterator 属性,从而使得该对象具有 Iterator 接口

1
2
3
4
5
6
7
8
let myIterable = {};
myIterable[Symbol.iterator] = function* () {
yield 1;
yield 2;
yield 3;
};

[...myIterable] // [1, 2, 3]

next() 方法的参数

next 方法可以带一个参数,该参数就会被当作 上一个 yield 表达式的返回值

注意: 由于 next 方法的参数表示 上一个 yield 表达式的返回值,所以在 第一次 使用 next 方法时,传递参数是无效的。 从语义上讲,第一个 next 方法用来启动遍历器对象,所以不用带有参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function* foo(x) {
let y = 2 * (yield (x + 1));
let z = yield (y / 3);
return (x + y + z);
}

let a = foo(5);
a.next(); // Object{value:6, done:false}
a.next(); // Object{value:NaN, done:false}
a.next(); // Object{value:NaN, done:true}

let b = foo(5);
b.next(); // { value:6, done:false }
b.next(12); // { value:8, done:false }
b.next(13); // { value:42, done:true }

for...of 循环

1
2
3
4
5
6
7
8
9
10
11
function *foo() {
yield 1;
yield 2;
return 3;
yield 4;
}

for (let v of foo()) {
console.log(v);
}
// 1 2

上面代码使用 for...of 循环,依次显示 2yield 表达式的值。 这里需要注意,一旦 next 方法的返回对象的 done 属性为 truefor...of 循环就会中止,且不包含该返回对象, 所以上面代码的 return 语句返回的 3 和之后的 4,不包括在 for...of 循环之中

除了 for...of 循环以外,扩展运算符(...)、解构赋值和 Array.from 方法内部调用的,都是遍历器接口。 这意味着,它们都可以将 Generator 函数返回的 Iterator 对象,作为参数 自注: 一旦执行了 next() 之后,再进行遍历操作(解构赋值、扩展运算等), 结果中将不包含 next() 之前的返回结果,即从 Generator 对象当前的状态开始遍历

1
2
3
4
5
6
7
8
9
10
11
12
const Gen = function* () {
yield 1;
yield 2;
yield 3;
yield 4;
};

let g = Gen();

g.next(); // {value: 1, done: false}
[...g] // [2, 3, 4]
[...g] // []

for...of 的本质是一个 while 循环,所以上面的代码实质上执行的是下面的逻辑

1
2
3
4
5
6
7
8
let it = iterateJobs(jobs);
let res = it.next();

while (!res.done){
let result = res.value;
// ...
res = it.next();
}

Generator.prototype.throw()

  • throw 方法,可以在函数体抛出错误,然后在 Generator 函数体捕获

  • throw 方法被捕获以后,会附带执行下一条 yield 表达式。也就是说,会附带执行一次 next() 方法

  • 只要 Generator 函数内部部署了 try...catch 代码块,那么遍历器的 throw 方法抛出的错误,不影响下一次遍历

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    const gen = function* gen(){
    try {
    yield console.log('a');
    } catch (e) {
    // ...
    }
    yield console.log('b');
    yield console.log('c');
    }

    let g = gen();
    g.next(); // a
    g.throw(); // b
    g.next(); // c
  • 一旦 Generator 执行过程中抛出错误,且没有被内部捕获,就不会再执行下去了。如果此后还调用 next() 方法,将返回一个 value 属性等于 undefineddone 属性等于 true 的对象,即 JavaScript 引擎认为这个 Generator 已经运行结束了

Generator.prototype.return()

return 方法,可以返回给定的值,并且终结遍历 Generator 函数

如果 Generator 函数内部有 try...finally 代码块,那么 return 方法会推迟到 finally 代码块执行完再执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function* numbers () {
yield 1;
try {
yield 2;
yield 3;
} finally {
yield 4;
yield 5;
}
yield 6;
}
let g = numbers();
g.next(); // { value: 1, done: false }
g.next(); // { value: 2, done: false }
g.return(7); // { value: 4, done: false }
g.next(); // { value: 5, done: false }
g.next(); // { value: 7, done: true }

上面代码中,调用 return 方法后,就开始执行 finally 代码块,然后等到 finally 代码块执行完,再执行 return 方法

next()、throw()、return() 的共同点

next()throw()return() 这三个方法本质上是同一件事,可以放在一起理解。 它们的作用都是让 Generator 函数恢复执行,并且使用不同的语句替换 yield 表达式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const g = function* (x, y) {
let result = yield x + y;
return result;
};

const gen = g(1, 2);
gen.next(); // Object {value: 3, done: false}

gen.next(1); // Object {value: 1, done: true}
// 相当于将 let result = yield x + y
// 替换成 let result = 1;

gen.throw(new Error('出错了')); // Uncaught Error: 出错了
// 相当于将 let result = yield x + y
// 替换成 let result = throw(new Error('出错了'))
// ;

gen.return(2); // Object {value: 2, done: true}
// 相当于将 let result = yield x + y
// 替换成 let result = return 2;

yield* 表达式

yield* 表达式,用来在一个 Generator 函数里面执行另一个 Generator 函数

  • 如果在 Generator 函数内部,调用另一个 Generator 函数,默认情况下是没有效果的

  • 从语法角度看,如果 yield 表达式后面跟的是一个遍历器对象,需要在 yield 表达式后面加上星号,表明它返回的是一个遍历器对象。这被称为 yield* 表达式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    function* bar() {
    yield 'x';
    yield* foo();
    yield 'y';
    }

    // 等同于
    function* bar() {
    yield 'x';
    yield 'a';
    yield 'b';
    yield 'y';
    }

    // 等同于
    function* bar() {
    yield 'x';
    for (let v of foo()) {
    yield v;
    }
    yield 'y';
    }

    for (let v of bar()){
    console.log(v);
    }
    // "x"
    // "a"
    // "b"
    // "y"
  • yield* 后面的 Generator 函数(没有 return 语句时),等同于在 Generator 函数内部,部署一个 for...of 循环反之,在有 return 语句时,则需要用 var value = yield* iterator 的形式获取 return 语句的值

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    function* concat(iter1, iter2) {
    yield* iter1;
    }

    // 等同于
    function* concat(iter1, iter2) {
    for (let value of iter1) {
    yield value;
    }
    }
  • 实际上,任何数据结构只要有 Iterator 接口,就可以被 yield* 遍历

✎ Generator 函数的异步调用

传统方法

  • 回调函数
  • 事件监听
  • 发布/订阅
  • Promise 对象

基本概念

所谓"异步",简单说就是一个任务不是连续完成的,可以理解成该任务被人为分成两段, 先执行第一段,然后转而执行其他任务,等做好了准备,再回过头执行第二段。 相应地,连续的执行就叫做同步。由于是连续执行,不能插入其他任务,所以操作系统从硬盘读取文件的这段时间,程序只能干等着

协程

"协程"(coroutine),意思是多个线程互相协作,完成异步任务

运行流程大致如下:

  1. 协程 A 开始执行
  2. 协程 A 执行到一半,进入暂停,执行权转移到协程 B
  3. (一段时间后)协程 B 交还执行权
  4. 协程 A 恢复执行

Thunk 函数

  • 编译器的 “传名调用” 实现,往往是将参数放到一个临时函数之中,再将这个临时函数传入函数体。这个临时函数就叫做 Thunk 函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // 正常版本的readFile(多参数版本)
    fs.readFile(fileName, callback);

    // Thunk版本的readFile(单参数版本)
    const Thunk = function (fileName) {
    return function (callback) {
    return fs.readFile(fileName, callback);
    };
    };

    const readFileThunk = Thunk(fileName);
    readFileThunk(callback);

co 模块

  • 用于 Generator 函数的自动执行

  • co 模块其实就是将两种自动执行器(Thunk 函数和 Promise 对象),包装成一个模块。使用 co 的前提条件是,Generator 函数的yield命令后面,只能是 Thunk 函数或 Promise 对象。如果数组或对象的成员,全部都是 Promise 对象,也可以使用 co

✎ async 函数

基本用法

async 函数返回一个 Promise 对象,可以使用 then 方法添加回调函数。 当函数执行的时候,一旦遇到 await 就会先返回,等到异步操作完成,再接着执行函数体内后面的语句

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
async function asyncPrint(val, delay) {
let tm = await timeout(delay);
console.log(val);
}

function timeout(ms) {
return new Promise((resolve, reject) => {
console.log('hello')
setTimeout(resolve, ms);
});
}

asyncPrint('world', 500);

// hello
// Promise {[[PromiseStatus]]: "pending", [[PromiseValue]]: undefined}
// world // after 500ms

语法

  • async 函数返回一个 Promise 对象

  • async 函数内部 return 语句返回的值,会成为 then 方法回调函数的参数

    1
    2
    3
    4
    5
    6
    async function f() {
    return 'hello world';
    }

    f().then(v => console.log(v));
    // "hello world"
  • async 函数返回的 Promise 对象,必须等到内部所有 await 命令后面的 Promise 对象执行完,才会发生状态改变,除非遇到 return 语句或者 抛出错误 。也就是说,只有 async 函数内部的异步操作执行完,才会执行 then 方法指定的回调函数

    1
    2
    3
    4
    5
    6
    7
    async function getTitle(url) {
    let response = await fetch(url);
    let html = await response.text();
    return html.match(/<title>([\s\S]+)<\/title>/i)[1];
    }
    getTitle('https://tc39.github.io/ecma262/').then(console.log);
    // "ECMAScript 2017 Language Specification"

    上面代码中,函数 getTitle 内部有三个操作:抓取网页、取出文本、匹配页面标题。只有这三个操作全部完成,才会执行 then 方法里面的 console.log

  • 正常情况下,await 命令后面是一个 Promise 对象。如果不是,会被转成一个立即 resolvePromise 对象

    1
    2
    3
    4
    5
    6
    async function f() {
    return await 123;
    }

    f().then(v => console.log(v));
    // 123
  • 只要一个 await 语句后面的 Promise 变为 reject,那么整个 async 函数都会中断执行。(个人理解: reject() 改变了整个函数 async 返回的 promise 对象的状态(rejected),抛出了异常,从而中断函数体的继续执行)

    1
    2
    3
    4
    async function f() {
    await Promise.reject('出错了');
    await Promise.resolve('hello world'); // 不会执行
    }

    解决办法是,将第一个 await 放在 try...catch 里面,或用 catch 方法处理异常

使用注意点

  1. await 命令后面的 Promise 对象,运行结果可能是 rejected,所以最好把 await 命令放在 try...catch 代码块中

  2. 多个 await 命令后面的异步操作,如果不存在继发关系,最好让它们同时触发

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    let foo = await getFoo();
    let bar = await getBar();

    // 写法一
    let [foo, bar] = await Promise.all([getFoo(), getBar()]);

    // 写法二
    let fooPromise = getFoo();
    let barPromise = getBar();
    let foo = await fooPromise;
    let bar = await barPromise;
  3. await 命令只能用在 async 函数之中,如果用在普通函数,就会报错

async 函数的实现原理

async 函数的实现原理,就是将 Generator 函数和自动执行器,包装在一个函数里

1
2
3
4
5
6
7
8
9
10
async function fn(args) {
// do something
}

// 等同于
function fn(args) {
return spawn(function* () {
// do something
});
}

所有的 async 函数都可以写成上面的第二种形式,其中的 spawn 函数就是自动执行器

✎ Class 的基本语法

基本语法

  1. 类的方法都定义在 prototype 对象上面
  2. 类的内部所有定义的方法,都是不可枚举的(non-enumerable
  3. 类和模块的内部,默认就是严格模式,所以不需要使用 use strict 指定运行模式。只要你的代码写在类或模块之中,就只有严格模式可用

constructor 方法

  • constructor 方法是类的默认方法,通过 new 命令生成对象实例时,自动调用该方法。一个类必须有 constructor 方法,如果没有显式定义,一个空的 constructor 方法会被默认添加

    1
    2
    3
    4
    5
    6
    7
    class Point {
    }

    // 等同于
    class Point {
    constructor() {}
    }
  • 类必须使用 new 调用,否则会报错。这是它跟普通构造函数的一个主要区别,后者不用 new 也可以执行

    1
    2
    3
    4
    5
    6
    7
    8
    class Foo {
    constructor() {
    return Object.create(null);
    }
    }

    Foo();
    // TypeError: Class constructor Foo cannot be invoked without 'new'

不存在变量提升

  • 不存在变量提升(hoist),这一点与 ES5 完全不同

    1
    2
    new Foo(); // ReferenceError
    class Foo {}

Class 的静态方法

  • 类相当于实例的原型,所有在类中定义的方法,都会被实例继承。如果在一个方法前,加上 static 关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为 “静态方法”。注意: 如果静态方法包含 this 关键字,这个 this 指的是,而不是实例

  • 静态方法可以与非静态方法重名

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    class Foo {
    static bar () {
    this.baz();
    }
    static baz () {
    console.log('hello');
    }
    baz () {
    console.log('world');
    }
    }

    Foo.bar() // hello
  • 父类的静态方法,可以被子类继承

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    class Foo {
    static classMethod() {
    return 'hello';
    }
    }

    class Bar extends Foo {
    static classMethod() {
    return super.classMethod() + ', too';
    }
    }

    Bar.classMethod() // 'hello'

Class 的静态属性和实例属性

  • ES6 明确规定,Class 内部只有静态方法,没有静态属性

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    class Foo {
    // 写法一 - 无效
    prop: 2

    // 写法二 - 无效
    static prop: 2
    }

    Foo.prop; // undefined

    Foo.prop = 1; // 有效
    Foo.prop; // 1

new.target 属性

  • new.target 属性,该属性一般用在构造函数之中,返回 new 命令作用于的那个构造函数。如果构造函数不是通过 new 命令调用的,new.target 会返回 undefined,因此这个属性可以用来确定构造函数是怎么调用的

  • 子类继承父类时,new.target 会返回子类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    class Rectangle {
    constructor(length, width) {
    console.log(new.target === Rectangle);
    console.log(new.target.name); // Square
    }
    }

    class Square extends Rectangle {
    constructor(length) {
    super(length, length);
    }
    }

    var obj = new Square(3); // 输出 false

✎ Class 的继承

简介

  • 子类必须在 constructor 方法中调用 super 方法,否则新建实例时会报错。这是因为子类没有自己的 this 对象,而是继承父类的 this 对象,然后对其进行加工。如果不调用 super 方法,子类就得不到 this 对象

  • ES5 的继承,实质是先创造子类的实例对象 this,然后再将父类的方法添加到 this 上面(Parent.apply(this))。ES6 的继承机制完全不同,实质是先创造父类的实例对象 this(所以必须先调用 super 方法),然后再用子类的构造函数修改 this

  • 在子类的构造函数中,只有调用 super() 之后,才可以使用 this 关键字,否则会报错。这是因为子类实例的构建,是基于对父类实例加工,只有super方法才能返回父类实例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    class Point {
    constructor(x, y) {
    this.x = x;
    this.y = y;
    }
    }

    class ColorPoint extends Point {
    constructor(x, y, color) {
    this.color = color; // ReferenceError
    super(x, y);
    this.color = color; // 正确
    }
    }
  • 如果子类没有定义 constructor 方法,这个方法会被默认添加,代码如下。也就是说,不管有没有显式定义,任何一个子类都有 constructor 方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    class ColorPoint extends Point {
    }

    // 等同于
    class ColorPoint extends Point {
    constructor(...args) {
    super(...args);
    }
    }

super 关键字

  • super 这个关键字,既可以当作函数使用,也可以当作对象使用:

    1. super 作为函数调用时,代表父类的构造函数。ES6 要求,子类的构造函数必须执行一次 super 函数

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      class A {
      constructor() {
      console.log(new.target.name);
      }
      }
      class B extends A {
      constructor() {
      super();
      }
      }
      new A(); // A
      new B(); // B

      注意,super 虽然代表了父类A的构造函数,但是返回的是子类B的实例, 即 super 内部的 this 指的是B,因此 super() 在这里相当于 A.prototype.constructor.call(this)

      作为函数时,super() 只能用在子类构造函数之中,用在其他地方就会报错

    2. super 作为对象时,在普通方法中,指向父类的原型对象;在静态方法中,指向父类

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      class A {
      p() {
      return 2;
      }
      }

      class B extends A {
      constructor() {
      super();
      console.log(super.p()); // 2
      }
      }

      let b = new B();
  • 注意,使用 super 的时候,必须显式指定是作为函数、还是作为对象使用,否则会报错

    1
    2
    3
    4
    5
    6
    7
    8
    class A {}

    class B extends A {
    constructor() {
    super();
    console.log(super); // 报错
    }
    }

类的 prototype 属性和 __proto__ 属性

类的继承是按照下面的模式实现的:

1
2
3
4
5
6
7
8
9
10
11
12
13
class A {
}

class B {
}

// B 的实例继承 A 的实例
Object.setPrototypeOf(B.prototype, A.prototype);

// B 的实例继承 A 的静态属性
Object.setPrototypeOf(B, A);

const b = new B();

可以这样理解:

  1. 作为一个对象,子类(B)的原型(__proto__属性)是父类(A);
  2. 作为一个构造函数,子类(B)的原型对象(prototype 属性)是父类的原型对象(prototype 属性)的实例
1
2
3
Object.create(A.prototype);
// 等同于
B.prototype.__proto__ = A.prototype;

原生构造函数的继承

  • ES6 可以自定义原生数据结构(比如 ArrayString 等)的子类,这是 ES5 无法做到的

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    class MyArray extends Array {
    constructor(...args) {
    super(...args);
    }
    }

    let arr = new MyArray();
    arr[0] = 12;
    arr.length; // 1

    arr.length = 0;
    arr[0]; // undefined

    注意:继承 Object 的子类,有一个 行为差异

    1
    2
    3
    4
    5
    6
    7
    class NewObj extends Object{
    constructor(){
    super(...arguments);
    }
    }
    let o = new NewObj({attr: true});
    o.attr === true // false

    上面代码中,NewObj 继承了 Object,但是无法通过 super 方法向父类 Object 传参。 这是因为 ES6 改变了 Object 构造函数的行为,一旦发现 Object 方法不是通过 new Object() 这种形式调用,ES6 规定 Object 构造函数会忽略参数

✎ Decorator

提案

✎ Module 的语法

概述

  • ES6 模块的设计思想,是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量

    1
    import { stat, exists, readFile } from 'fs';

    这种加载称为 “编译时加载” 或者 静态加载,即 ES6 可以在编译时就完成模块加载,效率要比 CommonJS 模块的加载方式高。 当然,这也导致了没法引用 ES6 模块本身,因为它不是对象

  • ES6 模块的好处:

    • 静态加载,编译时就能确定模块的依赖关系,以及输入和输出的变量
    • 不再需要 UMD 模块格式了,将来服务器和浏览器都会支持 ES6 模块格式。目前,通过各种工具库,其实已经做到了这一点
    • 将来浏览器的新 API 就能用模块格式提供,不再必须做成全局变量或者 navigator 对象的属性
    • 不再需要对象作为命名空间(比如 Math 对象),未来这些功能可以通过模块提供

严格模式

  • ES6 的模块自动采用严格模式,不管你有没有在模块头部加上 "use strict";

  • ES6 模块之中,顶层的 this 指向 undefined,即不应该在顶层代码使用 this

  • 严格模式 主要有以下限制:

    • 变量必须声明后再使用
    • 函数的参数不能有同名属性,否则报错
    • 不能使用 with 语句
    • 不能对只读属性赋值,否则报错
    • 不能使用 前缀 0 表示八进制数,否则报错
    • 不能删除不可删除的属性,否则报错
    • 不能删除变量 delete variable,会报错,只能删除属性 delete global[prop]
    • eval 不会在它的外层作用域引入变量
    • evalarguments 不能被重新赋值
    • arguments 不会自动反映函数参数的变化
    • 不能使用 arguments.callee
    • 不能使用 arguments.caller
    • 禁止 this 指向全局对象(ES6 模块之中,顶层的 this 指向 undefined,即不应该在顶层代码使用 this
    • 不能使用 fn.callerfn.arguments 获取函数调用的堆栈
    • 增加了保留字(比如 protectedstaticinterface

export 命令

  • 一个模块就是一个独立的文件。该文件内部的所有变量,外部无法获取。如果你希望外部能够读取模块内部的某个变量,就必须使用 export 关键字输出该变量

    注意export 命令规定的是对外的接口,必须与模块内部的变量建立一一对应关系

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // 报错
    export 1;

    // 报错
    let m = 1;
    export m;

    // 报错
    function f() {}
    export f;

    上面两种写法都会报错,因为没有提供对外的接口。 第一种写法直接输出 1, 第二种写法通过变量 m,还是直接输出 11 只是一个值,不是接口。正确的写法是下面这样

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // 写法一
    export var m = 1;

    // 写法二
    let m = 1;
    export {m};

    // 写法三
    let n = 1;
    export {n as m};

    // 正确
    export function f() {}

    // 正确
    function f() {}
    export {f};

    上面三种写法都是正确的,规定了对外的接口 m。其他脚本可以通过这个接口,取到值 1。 它们的实质是,在接口名与模块内部变量之间,建立了一一对应的关系

  • export 语句输出的接口,与其对应的值是动态绑定关系,即通过该接口,可以取到模块内部实时的值。即:ES6 模块输出的是值的引用

    1
    2
    export let foo = 'bar';
    setTimeout(() => foo = 'baz', 500);

    上面代码输出变量 foo,值为 bar500 毫秒之后变成 baz

    这一点与 CommonJS 规范完全不同。CommonJS 模块输出的是值的缓存,不存在动态更新

import 命令

  • import 命令具有提升效果,会提升到整个模块的头部,首先执行。import 命令是编译阶段执行的,在代码运行之前

  • 由于 import 是静态执行,所以不能使用表达式和变量,这些只有在运行时才能得到结果的语法结构

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // 报错
    import { 'f' + 'oo' } from 'my_module';

    // 报错
    let module = 'my_module';
    import { foo } from module;

    // 报错
    if (x === 1) {
    import { foo } from 'module1';
    } else {
    import { foo } from 'module2';
    }
  • 如果多次重复执行同一句 import 语句,那么只会执行一次,而不会执行多次(Singleton 模式)

模块的整体加载

  • 用星号(*)指定一个对象,所有输出值都加载在这个对象上面

    1
    2
    3
    4
    import * as circle from './circle';

    console.log('圆面积:' + circle.area(4));
    console.log('圆周长:' + circle.circumference(14));

    注意,模块整体加载所在的那个对象(上例是 circle),应该是可以静态分析的,所以不允许运行时改变。下面的写法都是不允许

    1
    2
    3
    4
    5
    import * as circle from './circle';

    // 下面两行都是不允许的
    circle.foo = 'hello';
    circle.area = function () {};

export default 命令

  • export default 命令用于指定模块的默认输出。显然,一个模块只能有一个默认输出,因此 export default 命令只能使用一次所以,import 命令后面才不用加大括号,因为只可能对应一个方法

  • 本质上,export default 就是输出一个叫做 default 的变量或方法,然后系统允许你为它取任意名字(export default 本质是将该命令后面的值,赋给 default 变量以后再默认)

1
2
3
4
5
6
7
8
9
10
11
12
// modules.js
function add(x, y) {
return x * y;
}
export {add as default};
// 等同于
// export default add;

// app.js
import { default as foo } from 'modules';
// 等同于
// import foo from 'modules';

✎ Module 的加载实现

浏览器加载

  • defer 是 “渲染完再执行”,async 是 “下载完就执行”

    defer 要等到整个页面在内存中正常渲染结束(DOM 结构完全生成,以及其他脚本执行完成),才会执行 async 是一旦下载完,渲染引擎就会中断渲染,执行这个脚本以后,再继续渲染 如果有多个 defer 脚本,会按照它们在页面出现的顺序加载,而多个 async 脚本是不能保证加载顺序的

  • 浏览器加载 ES6 模块,也使用 <script> 标签,但是要加入 type="module" 属性

  • 浏览器对于带有 type="module"<script>,都是异步加载,不会造成堵塞浏览器,即等到整个页面渲染完,再执行模块脚本,等同于打开了 <script> 标签的 defer 属性

  • 对于外部的模块脚本,需要注意:

    • 代码是在模块作用域之中运行,而不是在全局作用域运行。模块内部的顶层变量,外部不可见
    • 模块脚本自动采用严格模式,不管有没有声明 use strict;
    • 模块之中,可以使用 import 命令加载其他模块(.js后缀不可省略,需要提供绝对 URL 或相对 URL),也可以使用 export 命令输出对外接口
    • 模块之中,顶层的 this 关键字返回 undefined,而不是指向 window。也就是说,在模块顶层使用 this 关键字,是无意义的。
    • 同一个模块如果加载多次,将只执行一次

ES6 模块与 CommonJS 模块的差异

对比

  • CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用
  • CommonJS 模块是运行时加载,ES6 模块是编译时输出接口

ES6 模块的运行机制与 CommonJS 不一样。JS 引擎对脚本静态分析的时候,遇到模块加载命令 import,就会生成一个只读引用。 等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。 换句话说,ES6import 有点像 Unix 系统的 “符号连接”,原始值变了,import 加载的值也会跟着变。 因此,ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块

1
2
3
4
5
6
7
8
9
10
11
// lib.js
export let counter = 3;
export function incCounter() {
counter++;
}

// main.js
import { counter, incCounter } from './lib';
console.log(counter); // 3
incCounter();
console.log(counter); // 4

CommonJS 模块的加载原理

CommonJS 的一个模块,就是一个脚本文件。require 命令第一次加载该脚本,就会执行整个脚本,然后在内存生成一个对象

1
2
3
4
5
6
{
id: '...',
exports: { ... },
loaded: true,
...
}

以后需要用到这个模块的时候,就会到 exports 属性上面取值。 即使再次执行 require 命令,也不会再次执行该模块,而是到缓存之中取值。 也就是说,CommonJS 模块无论加载多少次,都只会在第一次加载时运行一次,以后再加载,就返回第一次运行的结果,除非手动清除系统缓存

Node 加载

内部变量

ES6 模块应该是通用的,同一个模块不用修改,就可以用在浏览器环境和服务器环境。 为了达到这个目标,Node 规定 ES6 模块之中不能使用 CommonJS 模块的特有的一些内部变量

  • 首先,就是 this 关键字:

    • ES6 模块之中,顶层的 this 指向 undefined
    • CommonJS 模块的顶层 this 指向当前模块
  • 其次,以下这些顶层变量在 ES6 模块之中都是不存在的:

    • arguments
    • require
    • module
    • exports
    • __filename
    • __dirname

ES6 模块加载 CommonJS 模块

CommonJS 模块的输出都定义在 module.exports 这个属性上面。 Nodeimport 命令加载 CommonJS 模块,Node 会自动将 module.exports 属性,当作模块的默认输出,即等同于 export default xxx

1
2
3
4
5
6
7
8
9
10
11
// a.js
module.exports = {
foo: 'hello',
bar: 'world'
};

// 等同于
export default {
foo: 'hello',
bar: 'world'
};
1
2
3
4
5
6
7
8
9
10
11
12
// c.js
module.exports = function two() {
return 2;
};

// es.js
import foo from './c';
foo(); // 2

import * as bar from './c';
bar.default(); // 2
bar(); // throws, bar is not a function

上面代码中,bar 本身是一个对象,不能当作函数调用,只能通过 bar.default 调用

CommonJS 模块加载 ES6 模块

CommonJS 模块加载 ES6 模块,不能使用 require 命令,而要使用 import() 函数。 ES6 模块的所有输出接口,会成为输入对象的属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// es.js
export let foo = { bar:'my-default' };
export { foo as bar };
export function f() {};
export class c {};

// cjs.js
const es_namespace = await import('./es');
// es_namespace = {
// get foo() {return foo;}
// get bar() {return foo;}
// get f() {return f;}
// get c() {return c;}
// }

循环加载

CommonJS 模块的循环加载

CommonJS 模块的重要特性是加载时执行,即脚本代码在 require 的时候,就会全部执行。 一旦出现某个模块被"循环加载",就只输出已经执行的部分,还未执行的部分不会输出

ES6 模块的循环加载

ES6 模块是动态引用,如果使用 import 从一个模块加载变量(即 import foo from 'foo'), 那些变量不会被缓存,而是成为一个指向被加载模块的引用,需要开发者自己保证,真正取值的时候能够取到值

✎ 编程风格

Airbnb JavaScript 风格规范

✎ 读懂规格

ECMA 国际标准组织的官方网站

✎ ArrayBuffer

ArrayBuffer 对象、TypedArray 视图和 DataView 视图是 JavaScript 操作二进制数据的一个接口

二进制数组由三类对象组成:

  1. ArrayBuffer 对象:代表内存之中的一段二进制数据,可以通过 “视图” 进行操作。“视图” 部署了数组接口,这意味着,可以用数组的方法操作内存

  2. TypedArray 视图:共包括 9 种类型的视图,比如 Uint8Array(无符号 8 位整数)数组视图, Int16Array(16 位整数)数组视图, Float32Array(32 位浮点数)数组视图等等

  3. DataView 视图:可以自定义复合格式的视图,比如第一个字节是 Uint8(无符号 8 位整数)、第二、三个字节是 Int16(16 位整数)、第四个字节开始是 Float32(32 位浮点数)等等,此外还可以自定义字节序列

简单说,ArrayBuffer 对象代表原始的二进制数据,TypedArray 视图用来读写简单类型的二进制数据,DataView 视图用来读写复杂类型的二进制数据。

注意:二进制数组并不是真正的数组,而是类似数组的对象

很多浏览器操作的 API,用到了二进制数组操作二进制数据,下面是其中的几个:

  • File API
  • XMLHttpRequest
  • Fetch API
  • Canvas
  • WebSockets

✎ 参考链接

ECMAScript 6 入门

晓月风尘 wechat
扫描二维码与我相识
你我共同创造价值,记得支持一下哦~