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

✎ 变量声明

概述

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

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

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

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

  1. for (let i = 0; i < 3; i++) {
  2. let i = 'abc';
  3. console.log(i);
  4. }
  5. // abc
  6. // abc
  7. // abc
  • let 不存在变量申明提升
  1. console.log(bar); // 报错 ReferenceError
  2. let bar = 2;
  • TDZ(Temporal Dead Zone),暂时性死区

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

  1. let tmp = 123;
  2. if (true) {
  3. tmp = 'abc'; // ReferenceError: tmp is not defined
  4. let tmp;
  5. }

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

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

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

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

  1. const foo = Object.freeze({});
  2. // 常规模式时,下面一行不起作用;
  3. // 严格模式时,该行会报错
  4. foo.prop = 123;
  • letconstclass 命令声明的全局变量,不属于顶层对象(如 window)的属性
  1. let a = 1;
  2. // 如果在 Node 的 REPL 环境,可以写成 global.a
  3. // 或者采用通用方法,写成 this.a
  4. window.a; // 1
  5. let b = 1;
  6. window.b; // undefined

✎ 变量的解构赋值

概述

  • 默认值:解构赋值指定默认值时,ES6 内部使用严格相等运算符(===),判断一个位置是否有值。所以,如果一个数组成员不严格等于 undefined,默认值是不会生效的
  1. let [x = 1] = [undefined];
  2. x // 1
  3. let [x = 1] = [null];
  4. x // null
  • 函数的参数也可以使用解构赋值
  1. function add([x, y]){
  2. return x + y;
  3. }
  4. add([1, 2]); // 3

技巧

  • 交换变量的值
  1. let x = 1;
  2. let y = 2;
  3. [x, y] = [y, x];
  • 遍历 Map 结构

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

  1. const map = new Map();
  2. map.set('first', 'hello');
  3. map.set('second', 'world');
  4. for (let [key, value] of map) {
  5. console.log(key + " is " + value);
  6. }
  7. // first is hello
  8. // second is world
  9. // 如果只想获取键名,或者只想获取键值,可以写成下面这样。
  10. // 获取键名
  11. for (let [key] of map) {
  12. // ...
  13. }
  14. // 获取键值
  15. for (let [,value] of map) {
  16. // ...
  17. }

✎ 字符串的扩展

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

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

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

✎ 正则表达式的扩展

RegExp 构造函数

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

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

u 修饰符

  • ES6 对正则表达式添加了 u 修饰符,含义为 “Unicode 模式”,用来正确处理大于 \uFFFFUnicode 字符。也就是说,会正确处理 4个字节 的 UTF-16 编码
  1. /^\uD83D/u.test('\uD83D\uDC2A'); // false
  2. /^\uD83D/.test('\uD83D\uDC2A'); // true
  • 一旦加上u修饰符号,就会修改下面这些正则表达式的行为
  • 点字符

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

  1. let s = '𠮷';
  2. /^.$/.test(s); // false
  3. /^.$/u.test(s); // true
  • Unicode 字符表示法

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

  1. /\u{61}/.test('a'); // false
  2. /\u{61}/u.test('a'); // true
  3. /\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. Number.isInteger(25); // true
  2. Number.isInteger(25.0); // true
  3. Number.isInteger(25.1); // false
  4. Number.isInteger("15"); // false
  5. 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. let x = 99;
  2. function foo(p = x + 1) {
  3. console.log(p);
  4. }
  5. foo(); // 100
  6. x = 100;
  7. foo(); // 101

函数的 length 属性

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

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

函数定义

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

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

尾调用优化

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

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

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

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

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

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

✎ 数组的扩展

扩展运算符的应用

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

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

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

  1. function length(str) {
  2. return [...str].length;
  3. }
  4. length('x\uD83D\uDE80y'); // 3

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

  1. let str = 'x\uD83D\uDE80y';
  2. str.split('').reverse().join('');
  3. // 'y\uDE80\uD83Dx'
  4. [...str].reverse().join('');
  5. // 'y\uD83D\uDE80x'

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

Array.from

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

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

Array.of

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

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

copyWithin()

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

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

find() 和 findIndex()

  • 数组实例的 find 方法,用于找出第一个符合条件的数组成员。它的参数是一个回调函数,所有数组成员依次执行该回调函数,直到找出第一个返回值为 true 的成员,然后返回该成员。如果没有符合条件的成员,则返回 undefined
  1. [1, 5, 10, 15].find(function(value, index, arr) {
  2. return value > 9;
  3. }) // 10

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

  • 数组实例的 findIndex 方法的用法与 find 方法非常类似,返回第一个符合条件的数组成员的位置,如果所有成员都不符合条件,则返回 -1
  1. [1, 5, 10, 15].findIndex(function(value, index, arr) {
  2. return value > 9;
  3. }) // 2

fill()

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

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

entries()、keys() 和 values()

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

  • keys() 是对键名的遍历

  • values()是对键值的遍历

  • entries()是对键值对的遍历

includes()

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

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

数组的空位

  • 数组的空位指,数组的某一个位置没有任何值。比如,Array 构造函数返回的数组都是空位
  1. new Array(3) // [, , ,]

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

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

  1. 0 in [undefined, undefined, undefined] // true
  2. 0 in [, , ,] // false
  • ES5 对空位的处理,已经很不一致了,大多数情况下会忽略空位:
  • forEach(), filter(), every()some() 都会跳过空位

  • map() 会跳过空位,但会保留这个值

  • join()toString() 会将空位视为 undefined,而 undefinednull 会被处理成空字符串

  • ES6 则是明确将空位转为 undefined
  • Array.from、扩展运算符(...) 方法会将数组的空位,转为 undefined,也就是说,这个方法不会忽略空位
  1. Array.from(['a',,'b']);
  2. // [ "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. let arr = [, ,];
  2. for (let i of arr) {
  3. console.log(1);
  4. }
  5. // 1
  6. // 1
  • entries()keys()values()find()findIndex() 会将空位处理成 undefined
  • 由于空位的处理规则非常不统一,所以建议避免出现空位

✎ 对象的扩展

属性名表达式

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

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

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

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

Object.is()

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

  • +0 不等于 -0

  • NaN 等于自身

  1. +0 === -0; //true
  2. NaN === NaN; // false
  3. Object.is(+0, -0); // false
  4. Object.is(NaN, NaN); // true

Object.assign()

  • Object.assign 方法用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target)如果目标对象与源对象有同名属性,或多个源对象有同名属性,则后面的属性会覆盖前面的属性
  1. const target = { a: 1, b: 1 };
  2. const source1 = { b: 2, c: 2 };
  3. const source2 = { c: 3 };
  4. Object.assign(target, source1, source2);
  5. target // {a:1, b:2, c:3}
  • 如果只有一个参数,Object.assign 会直接返回该参数
  1. const obj = {a: 1};
  2. Object.assign(obj) === obj; // true
  • 如果该参数不是对象,则会先转成对象,然后返回。
  1. typeof Object.assign(2); // "object"
  • 数值、字符串和布尔值不在首参数,不会报错。但是,除了字符串会以数组形式,拷贝入目标对象,其他值都不会产生效果。因为只有字符串的包装对象,会产生可枚举属性。
  1. const v1 = 'abc';
  2. const v2 = true;
  3. const v3 = 10;
  4. const obj = Object.assign({}, v1, v2, v3);
  5. console.log(obj); // { "0": "a", "1": "b", "2": "c" }

注意

  1. Object.assign 方法实行的是浅拷贝,而不是深拷贝。也就是说,如果源对象某个属性的值是对象,那么目标对象拷贝得到的是这个对象的引用
  1. const obj1 = {a: {b: 1}};
  2. const obj2 = Object.assign({}, obj1);
  3. obj1.a.b = 2;
  4. obj2.a.b // 2
  1. 同名属性的替换

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

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

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

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

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

  1. const source = {
  2. get foo() { return 1 }
  3. };
  4. const target = {};
  5. Object.assign(target, source)
  6. // { foo: 1 }

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

属性的遍历

属性遍历的次序规则:

  1. 首先遍历所有数值键,按照数值升序排列

  2. 其次遍历所有字符串键,按照加入时间升序排列

  3. 最后遍历所有 Symbol 键,按照加入时间升序排列

super 关键字

指向当前对象的原型对象

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

  1. // 报错
  2. const obj = {
  3. foo: super.foo
  4. }
  5. // 报错
  6. const obj = {
  7. foo: () => super.foo
  8. }
  9. // 报错
  10. const obj = {
  11. foo: function () {
  12. return super.foo
  13. }
  14. }
  15. // 正确
  16. const obj = {
  17. find() {
  18. return super.foo;
  19. }
  20. };

对象的扩展运算符

解构赋值

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

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

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

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

扩展运算符

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

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

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

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

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

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

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

✎ Symbol

概述

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

  • null

  • 布尔值(Boolean

  • 字符串(String

  • 数值(Number

  • 对象(Object

  • Symbol 函数的参数只是表示对当前 Symbol 值的描述,因此相同参数的 Symbol 函数的返回值是不相等的
  1. // 没有参数的情况
  2. let s1 = Symbol();
  3. let s2 = Symbol();
  4. s1 === s2 // false
  5. // 有参数的情况
  6. let s1 = Symbol('foo');
  7. let s2 = Symbol('foo');
  8. s1 === s2 // false
  • Symbol 值不能与其他类型的值进行运算
  1. let sym = Symbol('My symbol');
  2. let b = "your symbol is " + sym;
  3. // TypeError: Cannot convert a Symbol value to a string
  • Symbol 值可以显式转为字符串,也可以转为布尔值,但是不能转为数值
  1. let sym = Symbol('My symbol');
  2. String(sym); // 'Symbol(My symbol)'
  3. sym.toString(); // 'Symbol(My symbol)'
  4. let sym2 = Symbol();
  5. Boolean(sym2); // true
  6. !sym2 // false
  7. if (sym2) {
  8. // ...
  9. }
  10. Number(sym2); // TypeError
  11. sym2 + 2 // TypeError

作为属性名的 Symbol

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

属性名的遍历

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

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

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

Symbol.for()

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

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

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

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

Symbol.keyFor()

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

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

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

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

  1. iframe = document.createElement('iframe');
  2. iframe.src = String(window.location);
  3. document.body.appendChild(iframe);
  4. iframe.contentWindow.Symbol.for('foo') === Symbol.for('foo'); // true

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

✎ Set 和 Map 数据结构

Set

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

WeakSet

  • WeakSet 结构与 Set 类似,也是不重复的值的集合。但是,它与 Set 有两个区别:
  • 首先,WeakSet 的成员只能是对象,而不能是其他类型的值
  1. const ws = new WeakSet();
  2. ws.add(1);
  3. // TypeError: Invalid value used in weak set
  4. ws.add(Symbol());
  5. // 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. const a = [[1, 2], [3, 4]];
  2. const ws = new WeakSet(a);
  3. // WeakSet {[1, 2], [3, 4]}
  • WeakSet 的一个用处,是储存 DOM 节点,而不用担心这些节点从文档移除时,会引发内存泄漏

Map

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

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

  1. const map = new Map([
  2. ['name', '张三'],
  3. ['title', 'Author']
  4. ]);
  5. map.size; // 2
  6. map.has('name'); // true
  7. map.get('name'); // "张三"
  8. map.has('title'); // true
  9. map.get('title'); // "Author"

WeakMap

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

WeakMapMap 的区别:

  1. WeakMap 只接受对象作为键名(null除外),不接受其他类型的值作为键名
  1. const map = new WeakMap();
  2. map.set(1, 2); // TypeError: Invalid value used as weak map key
  3. map.set(Symbol(), 2); // TypeError: Invalid value used as weak map key
  4. map.set(null, 2); // TypeError: Invalid value used as weak map key
  1. WeakMap 的键名所指向的对象,不计入垃圾回收机制

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

  1. const wm = new WeakMap();
  2. const el = document.getElementById('example');
  3. wm.set(el, 'some information');
  4. wm.get(el) // "some information"
  1. WeakMapMap 在 API 上的区别主要是两个:
  • 没有遍历操作(即没有 key()values()entries() 方法),也没有 size 属性

  • 无法清空,即不支持clear方法

✎ Proxy

概述

  • Proxy 用于修改某些操作的默认行为,等同于在语言层面做出修改,所以属于一种 “元编程”(meta programming),即对编程语言进行编程。
    Proxy 可以理解成,在目标对象之前架设一层 “拦截”,外界对该对象的访问,都必须先通过这层拦截,
    因此提供了一种机制,可以对外界的访问进行过滤和改写
  1. let proxy = new Proxy({}, {
  2. get: function(target, property) {
  3. return 35;
  4. }
  5. });
  6. proxy.time // 35
  7. proxy.name // 35
  8. proxy.title // 35
  • 如果 handler 没有设置任何拦截,那就等同于直接通向原对象
  1. let target = {};
  2. let handler = {};
  3. let proxy = new Proxy(target, handler);
  4. proxy.a = 'b';
  5. target.a // "b"

Proxy 实例的方法

get()

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

  1. const target = Object.defineProperties({}, {
  2. foo: {
  3. value: 123,
  4. writable: false,
  5. configurable: false
  6. },
  7. });
  8. const handler = {
  9. get(target, propKey) {
  10. return 'abc';
  11. }
  12. };
  13. const proxy = new Proxy(target, handler);
  14. proxy.foo // TypeError: Invariant check failed

this 问题

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

  1. const target = {
  2. m: function () {
  3. console.log(this === proxy);
  4. }
  5. };
  6. const handler = {};
  7. const proxy = new Proxy(target, handler);
  8. target.m(); // false
  9. 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. try {
  3. Object.defineProperty(target, property, attributes);
  4. // success
  5. } catch (e) {
  6. // failure
  7. }
  8. // 新写法
  9. if (Reflect.defineProperty(target, property, attributes)) {
  10. // success
  11. } else {
  12. // failure
  13. }
  • Object 操作都变成函数行为。某些 Object 操作是命令式,比如 name in objdelete obj[name]
    Reflect.has(obj, name)Reflect.deleteProperty(obj, name) 让它们变成了函数行为
  1. // 老写法
  2. 'assign' in Object; // true
  3. // 新写法
  4. Reflect.has(Object, 'assign'); // true
  • Reflect 对象的方法与 Proxy 对象的方法一一对应,只要是 Proxy 对象的方法,就能在 Reflect 对象上找到对应的方法
  1. Proxy(target, {
  2. set: function(target, name, value, receiver) {
  3. let success = Reflect.set(target,name, value, receiver);
  4. if (success) {
  5. log('property ' + name + ' on ' + target + ' set to ' + value);
  6. }
  7. return success;
  8. }
  9. });

✎ Promise 对象

Promise 的含义

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

  • 特点

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

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

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

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

  • 缺点
  • 无法取消 Promise,一旦新建它就会立即执行,无法中途取消

  • 如果不设置回调函数,Promise 内部抛出的错误,不会反应到外部

  • 当处于 pending 状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)

基本用法

  • 如果调用 resolve 函数和 reject 函数时带有参数,那么它们的参数会被传递给回调函数。
    (如果参数是 Promise 的实例,那么参数的状态就会传递给改 Promise 的状态,即参数实例的状态决定了改实例的状态)
  1. const p1 = new Promise(function (resolve, reject) {
  2. setTimeout(() => reject(new Error('fail')), 3000);
  3. });
  4. const p2 = new Promise(function (resolve, reject) {
  5. setTimeout(() => resolve(p1), 1000);
  6. });
  7. p2
  8. .then(result => console.log(result))
  9. .catch(error => console.error(error));
  10. // Error: fail
  • 调用 resolvereject 并不会终结 Promise 的参数函数的执行
  1. new Promise((resolve, reject) => {
  2. resolve(1);
  3. console.log(2);
  4. }).then(r => {
  5. console.log(r);
  6. });
  7. // 2
  8. // 1

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

Promise.prototype.then()

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

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

Promise.race()

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

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

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

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

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

Promise.resolve()

将现有对象转为 Promise 对象

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

  1. 参数是一个 Promise 实例

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

  1. 参数是一个 thenable 对象

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

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

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

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

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

  1. const p = Promise.resolve('Hello');
  2. p.then(function (s){
  3. console.log(s)
  4. });
  5. // Hello
  1. 不带有任何参数

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

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

  1. setTimeout(function () {
  2. console.log('three');
  3. }, 0);
  4. Promise.resolve().then(function () {
  5. console.log('two');
  6. });
  7. console.log('one');
  8. // one
  9. // two
  10. // three

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

Promise.reject()

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

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

  1. const thenable = {
  2. then(resolve, reject) {
  3. reject('出错了');
  4. }
  5. };
  6. Promise.reject(thenable)
  7. .catch(e => {
  8. console.log(e === thenable)
  9. })
  10. // true

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

附加方法

done()

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

finally()

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

✎ 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. class RangeIterator {
  2. constructor(start, stop) {
  3. this.value = start;
  4. this.stop = stop;
  5. }
  6. next() {
  7. let value = this.value;
  8. if (value < this.stop) {
  9. this.value++;
  10. return {done: false, value: value};
  11. }
  12. return {done: true, value: undefined};
  13. }
  14. [Symbol.iterator]() { return this; }
  15. }
  16. function range(start, stop) {
  17. return new RangeIterator(start, stop);
  18. }
  19. for (let val of range(0, 3)) {
  20. console.log(val); // 0, 1, 2
  21. }
  • 凡是部署了 Symbol.iterator 属性的数据结构,就称为部署了遍历器接口。调用这个接口,就会返回一个遍历器对象

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

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

调用 Iterator 接口的场合

  • 解构赋值

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

  1. let set = new Set().add('a').add('b').add('c');
  2. let [x, y] = set; // x='a'; y='b'
  • 扩展运算符
  1. let str = 'hello';
  2. [...str]; // ['h','e','l','l','o']
  • yield*
  1. let generator = function* () {
  2. yield 1;
  3. yield* [2,3,4];
  4. yield 5;
  5. };
  6. let iterator = generator();
  7. iterator.next(); // { value: 1, done: false }
  8. iterator.next(); // { value: 2, done: false }
  9. iterator.next(); // { value: 3, done: false }
  10. iterator.next(); // { value: 4, done: false }
  11. iterator.next(); // { value: 5, done: false }
  12. 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. function readLinesSync(file) {
  2. return {
  3. next() {
  4. return { done: false };
  5. },
  6. return() {
  7. file.close();
  8. return { done: true };
  9. },
  10. };
  11. }
  12. // 情况一
  13. for (let line of readLinesSync(fileName)) {
  14. console.log(line);
  15. break;
  16. }
  17. // 情况二
  18. for (let line of readLinesSync(fileName)) {
  19. console.log(line);
  20. continue;
  21. }
  22. // 情况三
  23. for (let line of readLinesSync(fileName)) {
  24. console.log(line);
  25. throw new Error();
  26. }
  • 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. let arr = [3, 5, 7];
  2. arr.foo = 'hello';
  3. for (let i in arr) {
  4. console.log(i); // "0", "1", "2", "foo"
  5. }
  6. for (let i of arr) {
  7. console.log(i); // "3", "5", "7", 不包含 'foo'
  8. }

字符串

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

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

✎ Generator 函数的语法

简介

  1. Generator 函数是 ES6 提供的一种异步编程解决方案

  2. Generator 函数是一个状态机,封装了多个内部状态

  3. Generator 函数执行后返回一个遍历器对象,也就是说,Generator 函数除了状态机,还是一个遍历器对象生成函数

语法:

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

总结:

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

yield 表达式

  • yield 表达式 表示遍历器对象的暂停标识

  • yield 表达式 如果用在另一个表达式之中,必须放在 圆括号 里面

  • yield 表达式 本身没有返回值,或者说总是返回 undefined

  • 紧跟在 yield 后面的那个表达式的值,作为 next() 返回的对象的 value 属性值

  • return 后表示遍历结束状态时,返回值作为遍历后 value 的值

  1. function* Gen() {
  2. console.log('Hello' + yield 123); // SyntaxError
  3. console.log('Hello' + (yield 123)); // OK
  4. }

与 Iterator 接口的关系

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

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

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

next() 方法的参数

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

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

  1. function* foo(x) {
  2. let y = 2 * (yield (x + 1));
  3. let z = yield (y / 3);
  4. return (x + y + z);
  5. }
  6. let a = foo(5);
  7. a.next(); // Object{value:6, done:false}
  8. a.next(); // Object{value:NaN, done:false}
  9. a.next(); // Object{value:NaN, done:true}
  10. let b = foo(5);
  11. b.next(); // { value:6, done:false }
  12. b.next(12); // { value:8, done:false }
  13. b.next(13); // { value:42, done:true }

for…of 循环

  1. function *foo() {
  2. yield 1;
  3. yield 2;
  4. return 3;
  5. yield 4;
  6. }
  7. for (let v of foo()) {
  8. console.log(v);
  9. }
  10. // 1 2

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

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

  1. const Gen = function* () {
  2. yield 1;
  3. yield 2;
  4. yield 3;
  5. yield 4;
  6. };
  7. let g = Gen();
  8. g.next(); // {value: 1, done: false}
  9. [...g] // [2, 3, 4]
  10. [...g] // []

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

  1. let it = iterateJobs(jobs);
  2. let res = it.next();
  3. while (!res.done){
  4. let result = res.value;
  5. // ...
  6. res = it.next();
  7. }

Generator.prototype.throw()

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

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

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

  1. const gen = function* gen(){
  2. try {
  3. yield console.log('a');
  4. } catch (e) {
  5. // ...
  6. }
  7. yield console.log('b');
  8. yield console.log('c');
  9. }
  10. let g = gen();
  11. g.next(); // a
  12. g.throw(); // b
  13. g.next(); // c
  • 一旦 Generator 执行过程中抛出错误,且没有被内部捕获,就不会再执行下去了。如果此后还调用 next() 方法,将返回一个 value 属性等于 undefineddone 属性等于 true 的对象,即 JavaScript 引擎认为这个 Generator 已经运行结束了

Generator.prototype.return()

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

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

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

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

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

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

  1. const g = function* (x, y) {
  2. let result = yield x + y;
  3. return result;
  4. };
  5. const gen = g(1, 2);
  6. gen.next(); // Object {value: 3, done: false}
  7. gen.next(1); // Object {value: 1, done: true}
  8. // 相当于将 let result = yield x + y
  9. // 替换成 let result = 1;
  10. gen.throw(new Error('出错了')); // Uncaught Error: 出错了
  11. // 相当于将 let result = yield x + y
  12. // 替换成 let result = throw(new Error('出错了'))
  13. // ;
  14. gen.return(2); // Object {value: 2, done: true}
  15. // 相当于将 let result = yield x + y
  16. // 替换成 let result = return 2;

yield* 表达式

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

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

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

  1. function* bar() {
  2. yield 'x';
  3. yield* foo();
  4. yield 'y';
  5. }
  6. // 等同于
  7. function* bar() {
  8. yield 'x';
  9. yield 'a';
  10. yield 'b';
  11. yield 'y';
  12. }
  13. // 等同于
  14. function* bar() {
  15. yield 'x';
  16. for (let v of foo()) {
  17. yield v;
  18. }
  19. yield 'y';
  20. }
  21. for (let v of bar()){
  22. console.log(v);
  23. }
  24. // "x"
  25. // "a"
  26. // "b"
  27. // "y"
  • yield* 后面的 Generator 函数(没有 return 语句时),等同于在 Generator 函数内部,部署一个 for...of 循环反之,在有 return 语句时,则需要用 var value = yield* iterator 的形式获取 return 语句的值
  1. function* concat(iter1, iter2) {
  2. yield* iter1;
  3. }
  4. // 等同于
  5. function* concat(iter1, iter2) {
  6. for (let value of iter1) {
  7. yield value;
  8. }
  9. }
  • 实际上,任何数据结构只要有 Iterator 接口,就可以被 yield* 遍历

✎ Generator 函数的异步调用

传统方法

  • 回调函数

  • 事件监听

  • 发布/订阅

  • Promise 对象

基本概念

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

协程

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

运行流程大致如下:

  1. 协程 A 开始执行

  2. 协程 A 执行到一半,进入暂停,执行权转移到协程 B

  3. (一段时间后)协程 B 交还执行权

  4. 协程 A 恢复执行

Thunk 函数

  • 编译器的 “传名调用” 实现,往往是将参数放到一个临时函数之中,再将这个临时函数传入函数体。这个临时函数就叫做 Thunk 函数
  1. // 正常版本的readFile(多参数版本)
  2. fs.readFile(fileName, callback);
  3. // Thunk版本的readFile(单参数版本)
  4. const Thunk = function (fileName) {
  5. return function (callback) {
  6. return fs.readFile(fileName, callback);
  7. };
  8. };
  9. const readFileThunk = Thunk(fileName);
  10. readFileThunk(callback);

co 模块

  • 用于 Generator 函数的自动执行

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

✎ async 函数

基本用法

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

  1. async function asyncPrint(val, delay) {
  2. let tm = await timeout(delay);
  3. console.log(val);
  4. }
  5. function timeout(ms) {
  6. return new Promise((resolve, reject) => {
  7. console.log('hello')
  8. setTimeout(resolve, ms);
  9. });
  10. }
  11. asyncPrint('world', 500);
  12. // hello
  13. // Promise {[[PromiseStatus]]: "pending", [[PromiseValue]]: undefined}
  14. // world // after 500ms

语法

  • async 函数返回一个 Promise 对象

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

  1. async function f() {
  2. return 'hello world';
  3. }
  4. f().then(v => console.log(v));
  5. // "hello world"
  • async 函数返回的 Promise 对象,必须等到内部所有 await 命令后面的 Promise 对象执行完,才会发生状态改变,除非遇到 return 语句或者 抛出错误 。也就是说,只有 async 函数内部的异步操作执行完,才会执行 then 方法指定的回调函数
  1. async function getTitle(url) {
  2. let response = await fetch(url);
  3. let html = await response.text();
  4. return html.match(/<title>([\s\S]+)<\/title>/i)[1];
  5. }
  6. getTitle('https://tc39.github.io/ecma262/').then(console.log);
  7. // "ECMAScript 2017 Language Specification"

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

  • 正常情况下,await 命令后面是一个 Promise 对象。如果不是,会被转成一个立即 resolvePromise 对象
  1. async function f() {
  2. return await 123;
  3. }
  4. f().then(v => console.log(v));
  5. // 123
  • 只要一个 await 语句后面的 Promise 变为 reject,那么整个 async 函数都会中断执行。(个人理解: reject() 改变了整个函数 async 返回的 promise 对象的状态(rejected),抛出了异常,从而中断函数体的继续执行)
  1. async function f() {
  2. await Promise.reject('出错了');
  3. await Promise.resolve('hello world'); // 不会执行
  4. }

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

使用注意点

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

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

  1. let foo = await getFoo();
  2. let bar = await getBar();
  3. // 写法一
  4. let [foo, bar] = await Promise.all([getFoo(), getBar()]);
  5. // 写法二
  6. let fooPromise = getFoo();
  7. let barPromise = getBar();
  8. let foo = await fooPromise;
  9. let bar = await barPromise;
  1. await 命令只能用在 async 函数之中,如果用在普通函数,就会报错

async 函数的实现原理

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

  1. async function fn(args) {
  2. // do something
  3. }
  4. // 等同于
  5. function fn(args) {
  6. return spawn(function* () {
  7. // do something
  8. });
  9. }

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

✎ Class 的基本语法

基本语法

  1. 类的方法都定义在 prototype 对象上面

  2. 类的内部所有定义的方法,都是不可枚举的(non-enumerable

  3. 类和模块的内部,默认就是严格模式,所以不需要使用 use strict 指定运行模式。只要你的代码写在类或模块之中,就只有严格模式可用

constructor 方法

  • constructor 方法是类的默认方法,通过 new 命令生成对象实例时,自动调用该方法。一个类必须有 constructor 方法,如果没有显式定义,一个空的 constructor 方法会被默认添加
  1. class Point {
  2. }
  3. // 等同于
  4. class Point {
  5. constructor() {}
  6. }
  • 类必须使用 new 调用,否则会报错。这是它跟普通构造函数的一个主要区别,后者不用 new 也可以执行
  1. class Foo {
  2. constructor() {
  3. return Object.create(null);
  4. }
  5. }
  6. Foo();
  7. // TypeError: Class constructor Foo cannot be invoked without 'new'

不存在变量提升

  • 不存在变量提升(hoist),这一点与 ES5 完全不同
  1. new Foo(); // ReferenceError
  2. class Foo {}

Class 的静态方法

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

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

  1. class Foo {
  2. static bar () {
  3. this.baz();
  4. }
  5. static baz () {
  6. console.log('hello');
  7. }
  8. baz () {
  9. console.log('world');
  10. }
  11. }
  12. Foo.bar() // hello
  • 父类的静态方法,可以被子类继承
  1. class Foo {
  2. static classMethod() {
  3. return 'hello';
  4. }
  5. }
  6. class Bar extends Foo {
  7. static classMethod() {
  8. return super.classMethod() + ', too';
  9. }
  10. }
  11. Bar.classMethod() // 'hello'

Class 的静态属性和实例属性

  • ES6 明确规定,Class 内部只有静态方法,没有静态属性
  1. class Foo {
  2. // 写法一 - 无效
  3. prop: 2
  4. // 写法二 - 无效
  5. static prop: 2
  6. }
  7. Foo.prop; // undefined
  8. Foo.prop = 1; // 有效
  9. Foo.prop; // 1

new.target 属性

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

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

  1. class Rectangle {
  2. constructor(length, width) {
  3. console.log(new.target === Rectangle);
  4. console.log(new.target.name); // Square
  5. }
  6. }
  7. class Square extends Rectangle {
  8. constructor(length) {
  9. super(length, length);
  10. }
  11. }
  12. 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. class Point {
  2. constructor(x, y) {
  3. this.x = x;
  4. this.y = y;
  5. }
  6. }
  7. class ColorPoint extends Point {
  8. constructor(x, y, color) {
  9. this.color = color; // ReferenceError
  10. super(x, y);
  11. this.color = color; // 正确
  12. }
  13. }
  • 如果子类没有定义 constructor 方法,这个方法会被默认添加,代码如下。也就是说,不管有没有显式定义,任何一个子类都有 constructor 方法
  1. class ColorPoint extends Point {
  2. }
  3. // 等同于
  4. class ColorPoint extends Point {
  5. constructor(...args) {
  6. super(...args);
  7. }
  8. }

super 关键字

  • super 这个关键字,既可以当作函数使用,也可以当作对象使用:
  1. super 作为函数调用时,代表父类的构造函数。ES6 要求,子类的构造函数必须执行一次 super 函数
  1. class A {
  2. constructor() {
  3. console.log(new.target.name);
  4. }
  5. }
  6. class B extends A {
  7. constructor() {
  8. super();
  9. }
  10. }
  11. new A(); // A
  12. new B(); // B

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

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

  1. super 作为对象时,在普通方法中,指向父类的原型对象;在静态方法中,指向父类
  1. class A {
  2. p() {
  3. return 2;
  4. }
  5. }
  6. class B extends A {
  7. constructor() {
  8. super();
  9. console.log(super.p()); // 2
  10. }
  11. }
  12. let b = new B();
  • 注意,使用 super 的时候,必须显式指定是作为函数、还是作为对象使用,否则会报错 javascript class A {}

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

  1. ```
  2. <a name="tzfrhs"></a>
  3. ### 类的 `prototype` 属性和 `__proto__` 属性
  4. 类的继承是按照下面的模式实现的:
  5. ```javascript
  6. class A {
  7. }
  8. class B {
  9. }
  10. // B 的实例继承 A 的实例
  11. Object.setPrototypeOf(B.prototype, A.prototype);
  12. // B 的实例继承 A 的静态属性
  13. Object.setPrototypeOf(B, A);
  14. const b = new B();

可以这样理解:

  1. 作为一个对象,子类(B)的原型(__proto__属性)是父类(A);

  2. 作为一个构造函数,子类(B)的原型对象(prototype 属性)是父类的原型对象(prototype 属性)的实例

  1. Object.create(A.prototype);
  2. // 等同于
  3. B.prototype.__proto__ = A.prototype;

原生构造函数的继承

  • ES6 可以自定义原生数据结构(比如 ArrayString 等)的子类,这是 ES5 无法做到的
  1. class MyArray extends Array {
  2. constructor(...args) {
  3. super(...args);
  4. }
  5. }
  6. let arr = new MyArray();
  7. arr[0] = 12;
  8. arr.length; // 1
  9. arr.length = 0;
  10. arr[0]; // undefined

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

  1. class NewObj extends Object{
  2. constructor(){
  3. super(...arguments);
  4. }
  5. }
  6. let o = new NewObj({attr: true});
  7. 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. export 1;
  3. // 报错
  4. let m = 1;
  5. export m;
  6. // 报错
  7. function f() {}
  8. export f;

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

  1. // 写法一
  2. export var m = 1;
  3. // 写法二
  4. let m = 1;
  5. export {m};
  6. // 写法三
  7. let n = 1;
  8. export {n as m};
  9. // 正确
  10. export function f() {}
  11. // 正确
  12. function f() {}
  13. export {f};

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

  • export 语句输出的接口,与其对应的值是动态绑定关系,即通过该接口,可以取到模块内部实时的值。即:ES6 模块输出的是值的引用
  1. export let foo = 'bar';
  2. setTimeout(() => foo = 'baz', 500);

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

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

import 命令

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

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

  1. // 报错
  2. import { 'f' + 'oo' } from 'my_module';
  3. // 报错
  4. let module = 'my_module';
  5. import { foo } from module;
  6. // 报错
  7. if (x === 1) {
  8. import { foo } from 'module1';
  9. } else {
  10. import { foo } from 'module2';
  11. }
  • 如果多次重复执行同一句 import 语句,那么只会执行一次,而不会执行多次(Singleton 模式)

模块的整体加载

  • 用星号(*)指定一个对象,所有输出值都加载在这个对象上面
  1. import * as circle from './circle';
  2. console.log('圆面积:' + circle.area(4));
  3. console.log('圆周长:' + circle.circumference(14));

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

  1. import * as circle from './circle';
  2. // 下面两行都是不允许的
  3. circle.foo = 'hello';
  4. circle.area = function () {};

export default 命令

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

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

  1. // modules.js
  2. function add(x, y) {
  3. return x * y;
  4. }
  5. export {add as default};
  6. // 等同于
  7. // export default add;
  8. // app.js
  9. import { default as foo } from 'modules';
  10. // 等同于
  11. // 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. // lib.js
  2. export let counter = 3;
  3. export function incCounter() {
  4. counter++;
  5. }
  6. // main.js
  7. import { counter, incCounter } from './lib';
  8. console.log(counter); // 3
  9. incCounter();
  10. console.log(counter); // 4

CommonJS 模块的加载原理

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

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

以后需要用到这个模块的时候,就会到 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. // a.js
  2. module.exports = {
  3. foo: 'hello',
  4. bar: 'world'
  5. };
  6. // 等同于
  7. export default {
  8. foo: 'hello',
  9. bar: 'world'
  10. };
  1. // c.js
  2. module.exports = function two() {
  3. return 2;
  4. };
  5. // es.js
  6. import foo from './c';
  7. foo(); // 2
  8. import * as bar from './c';
  9. bar.default(); // 2
  10. bar(); // throws, bar is not a function

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

CommonJS 模块加载 ES6 模块

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

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

循环加载

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 入门