通过阅读阮一峰老师著作《ECMAScript 6 入门》,提取和总结的 ECMAScript6 语法关键点。取一些需要注意的地方,供学习和参考。
目录
- 目录
- ✎ 变量声明
- ✎ 变量的解构赋值
- ✎ 字符串的扩展
- ✎ 正则表达式的扩展
- ✎ 数值的扩展
- ✎ 函数的扩展
- ✎ 数组的扩展
- ✎ 对象的扩展
- ✎ Symbol
- ✎ Set 和 Map 数据结构
- ✎ Proxy
- ✎ Reflect
- ✎ Promise 对象
- ✎ Iterator 和 for...of 循环
- ✎ Generator 函数的语法
- ✎ Generator 函数的异步调用
- ✎ async 函数
- ✎ Class 的基本语法
- ✎ Class 的继承
- ✎ Decorator
- ✎ Module 的语法
- ✎ Module 的加载实现
- ✎ 编程风格
- ✎ 读懂规格
- ✎ ArrayBuffer
- ✎ 参考链接
✎ 变量声明
概述
代码块,双大括号,拥有块级作用域
可不将大括号写在行首,取消 JavaScript 将其解释为代码块
1
2
3// 参考:变量的解构赋值(对象)
let x;
({x} = {x: 1});let
声明的变量只在它所在的代码块有效for
循环中用let
声明的i
只在循环体内有效,且为父作用域内,与函数体作用域独立1
2
3
4
5
6
7for (let i = 0; i < 3; i++) {
let i = 'abc';
console.log(i);
}
// abc
// abc
// abclet
不存在变量申明提升1
2console.log(bar); // 报错 ReferenceError
let bar = 2;TDZ(Temporal Dead Zone),暂时性死区
只要块级作用域内存在
let
命令,它所声明的变量就 “绑定”(binding)这个区域,不再受外部的影响。1
2
3
4
5
6let tmp = 123;
if (true) {
tmp = 'abc'; // ReferenceError: tmp is not defined
let tmp;
}“暂时性死区” 也意味着
typeof
不再是一个百分之百安全的操作1
2typeof x; // ReferenceError: x is not defined
let x;不允许重复声明
const
实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址不得改动如果真的想将对象冻结,应该使用
Object.freeze
方法1
2
3
4
5const foo = Object.freeze({});
// 常规模式时,下面一行不起作用;
// 严格模式时,该行会报错
foo.prop = 123;let
、const
和class
命令声明的全局变量,不属于顶层对象(如window
)的属性1
2
3
4
5
6
7let a = 1;
// 如果在 Node 的 REPL 环境,可以写成 global.a
// 或者采用通用方法,写成 this.a
window.a; // 1
let b = 1;
window.b; // undefined
✎ 变量的解构赋值
概述
默认值:解构赋值指定默认值时,ES6 内部使用严格相等运算符(
===
),判断一个位置是否有值。所以,如果一个数组成员不严格等于undefined
,默认值是不会生效的1
2
3
4
5let [x = 1] = [undefined];
x // 1
let [x = 1] = [null];
x // null函数的参数也可以使用解构赋值
1
2
3
4
5function add([x, y]){
return x + y;
}
add([1, 2]); // 3
技巧
交换变量的值
1
2
3
4let 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
21const 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
6let s = '𠮷a';
for (let ch of s) {
console.log(ch.codePointAt(0).toString(16));
}
// 20bb7
// 61codePointAt
方法是测试一个字符由两个字节还是由四个字节组成的最简单方法1
2
3
4
5
6function is32Bit(c) {
return c.codePointAt(0) > 0xFFFF;
}
is32Bit("𠮷"); // true
is32Bit("a") // false
✎ 正则表达式的扩展
RegExp 构造函数
如果
RegExp
构造函数第一个参数是一个正则对象,那么可以使用第二个参数指定修饰符。 而且,返回的正则表达式会忽略原有的正则表达式的修饰符,只使用新指定的修饰符
1 | new RegExp(/abc/ig, 'i').flags |
u 修饰符
ES6 对正则表达式添加了
u
修饰符,含义为 “Unicode
模式”,用来正确处理大于\uFFFF
的Unicode
字符。也就是说,会正确处理 4个字节 的UTF-16
编码1
2/^\uD83D/u.test('\uD83D\uDC2A'); // false
/^\uD83D/.test('\uD83D\uDC2A'); // true一旦加上u修饰符号,就会修改下面这些正则表达式的行为
点字符
对于码点大于
0xFFFF
的Unicode
字符,点字符不能识别,必须加上u
修饰符1
2
3
4let s = '𠮷';
/^.$/.test(s); // false
/^.$/u.test(s); // trueUnicode
字符表示法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 | Number.isInteger(25); // true |
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; |
函数的 length
属性
length
属性的含义是,该函数预期传入的参数个数。某个参数指定默认值以后,预期传入的参数个数就不包括这个参数了 指定了默认值以后,函数的length
属性,将返回没有指定默认值的参数个数。也就是说,指定了默认值后,length
属性将失真。
1 | (function (a) {}).length; // 1 |
函数定义
由于大括号被解释为代码块,所以如果箭头函数直接返回一个对象,必须在对象外面加上括号,否则会报错。
1 | // 报错 |
尾调用优化
定义:某个函数的最后一步是调用另一个函数
1 | function f(x){ |
上面代码中,函数 f 的最后一步是调用函数 g,这就叫尾调用
以下三种情况,都不属于尾调用:
1 | // 情况一 |
尾调用由于是函数的最后一步操作,所以不需要保留外层函数的调用帧,因为调用位置、内部变量等信息都不会再用到了,只要直接用内层函数的调用帧,取代外层函数的调用帧就可以了 “尾调用优化” 对递归操作意义重大,所以一些函数式编程语言将其写入了语言规格。 ES6 是如此,第一次明确规定,所有 ECMAScript 的实现,都必须部署 “尾调用优化”。 这就是说,ES6 中只要使用尾递归,就不会发生栈溢出,相对节省内存
✎ 数组的扩展
扩展运算符的应用
扩展运算符有一个重要的好处,那就是能够正确识别 4个字节 的
Unicode 字符
1 | 'x\uD83D\uDE80y'.length; // 4 |
上面代码的第一种写法,JavaScript会将 4个字节 的 Unicode
字符,识别为 2 个字符,采用扩展运算符就没有这个问题。 因此,正确返回字符串长度的函数,可以像下面这样写
1 | function length(str) { |
凡是涉及到操作4个字节的 Unicode
字符的函数,都有这个问题。因此,最好都用扩展运算符改写
1 | let str = 'x\uD83D\uDE80y'; |
上面代码中,如果不用扩展运算符,字符串的 reverse
操作就不正确
Array.from
Array.from()
的另一个应用是,将字符串转为数组,然后返回字符串的长度。因为它能正确处理各种Unicode
字符,可以避免JavaScript将大于\uFFFF
的Unicode
字符,算作两个字符的bug。
1 | function countSymbols(string) { |
Array.of
Array.of
方法用于将一组值,转换为数组
1 | Array.of(3, 11, 8); // [3, 11, 8] |
copyWithin()
组实例的
copyWithin
方法,在当前数组内部,将指定位置的成员复制到其他位置(会覆盖原有成员),然后返回当前数组。 也就是说,使用这个方法,会修改当前数组
1 | Array.prototype.copyWithin(target, start = 0, end = this.length); |
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 | ['a', 'b', 'c'].fill(7); |
entries()、keys() 和 values()
用于遍历数组。它们都返回一个遍历器对象(详见《Iterator》一章),可以用
for...of
循环进行遍历,唯一的区别是:
keys()
是对键名的遍历values()
是对键值的遍历entries()
是对键值对的遍历
includes()
Array.prototype.includes
方法返回一个布尔值,表示某个数组是否包含给定的值,与字符串的includes
方法类似。ES2016 引入了该方法
1 | [1, 2, 3].includes(2); // true |
数组的空位
数组的空位指,数组的某一个位置没有任何值。比如,
Array
构造函数返回的数组都是空位1
new Array(3) // [, , ,]
上面代码中,
Array(3)
返回一个具有 3 个空位的数组。注意,空位不是
undefined
,一个位置的值等于undefined
,依然是有值的。空位是没有任何值,in
运算符可以说明这一点1
20 in [undefined, undefined, undefined] // true
0 in [, , ,] // falseES5 对空位的处理,已经很不一致了,大多数情况下会忽略空位:
forEach()
,filter()
,every()
和some()
都会跳过空位map()
会跳过空位,但会保留这个值join()
和toString()
会将空位视为undefined
,而undefined
和null
会被处理成空字符串
ES6 则是明确将空位转为
undefined
:Array.from
、扩展运算符(...
) 方法会将数组的空位,转为undefined
,也就是说,这个方法不会忽略空位1
2Array.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
6let arr = [, ,];
for (let i of arr) {
console.log(1);
}
// 1
// 1entries()
、keys()
、values()
、find()
和findIndex()
会将空位处理成undefined
由于空位的处理规则非常不统一,所以建议避免出现空位
✎ 对象的扩展
属性名表达式
可用 表达式 作为对象的属性名,即把表达式放在方括号内:
1 | let propKey = 'foo'; |
表达式还可以用于定义方法名:
1 | let obj = { |
Object.is()
“Same-value equality”,同值相等,
Object.is
就是部署这个算法的新方法。 它用来比较两个值是否严格相等,与严格比较运算符(===
)的行为基本一致。不同之处只有两个:
+0
不等于-0
NaN
等于自身
1 | +0 === -0; //true |
Object.assign()
Object.assign
方法用于对象的合并,将源对象(source
)的所有可枚举属性,复制到目标对象(target
)如果目标对象与源对象有同名属性,或多个源对象有同名属性,则后面的属性会覆盖前面的属性1
2
3
4
5
6
7const 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
2const obj = {a: 1};
Object.assign(obj) === obj; // true如果该参数不是对象,则会先转成对象,然后返回。
1
typeof Object.assign(2); // "object"
数值、字符串和布尔值不在首参数,不会报错。但是,除了字符串会以数组形式,拷贝入目标对象,其他值都不会产生效果。因为只有字符串的包装对象,会产生可枚举属性。
1
2
3
4
5
6const v1 = 'abc';
const v2 = true;
const v3 = 10;
const obj = Object.assign({}, v1, v2, v3);
console.log(obj); // { "0": "a", "1": "b", "2": "c" }
注意:
Object.assign
方法实行的是浅拷贝,而不是深拷贝。也就是说,如果源对象某个属性的值是对象,那么目标对象拷贝得到的是这个对象的引用1
2
3
4
5const obj1 = {a: {b: 1}};
const obj2 = Object.assign({}, obj1);
obj1.a.b = 2;
obj2.a.b // 2同名属性的替换
对于嵌套的对象,一旦遇到同名属性,
Object.assign
的处理方法是替换,而不是添加1
2
3
4const target = { a: { b: 'c', d: 'e' } };
const source = { a: { b: 'hello' } };
Object.assign(target, source);
// { a: { b: 'hello' } }数组的处理
Object.assign
可以用来处理数组,但是会把数组视为对象1
2
3let arr1 = [1, 2, 3, 4];
Object.assign(arr1, [4, 5]);
arr1 // [4, 5, 3, 4]取值函数的处理
Object.assign
只能进行值的复制,如果要复制的值是一个取值函数,那么将求值后再复制1
2
3
4
5
6
7const source = {
get foo() { return 1 }
};
const target = {};
Object.assign(target, source)
// { foo: 1 }上面代码中,
source
对象的foo
属性是一个取值函数,Object.assign
不会复制这个取值函数,只会拿到值以后,将这个值复制过去
属性的遍历
属性遍历的次序规则:
- 首先遍历所有数值键,按照数值升序排列
- 其次遍历所有字符串键,按照加入时间升序排列
- 最后遍历所有
Symbol
键,按照加入时间升序排列
super 关键字
指向当前对象的原型对象
super
关键字表示原型对象时,只能用在对象的方法之中,用在其他地方都会报错。 目前,只有对象方法的简写法可以让 JavaScript 引擎确认,定义的是对象的方法
1 | // 报错 |
对象的扩展运算符
解构赋值
对象的解构赋值用于从一个对象取值,相当于将所有可遍历的、但尚未被读取的属性,分配到指定的对象上面。 所有的键和它们的值,都会拷贝到新对象上面。
1 | let { x, y, ...z } = { x: 1, y: 2, a: 3, b: 4 }; |
扩展运算符的解构赋值,不能复制继承自原型对象的属性。
1 | let o1 = { a: 1 }; |
扩展运算符
扩展运算符(
...
)用于取出参数对象的所有可遍历属性,拷贝到当前对象之中。
扩展运算符可以用于合并两个对象
1 | let ab = { ...a, ...b }; |
与数组的扩展运算符一样,对象的扩展运算符后面可以跟表达式
1 | const obj = { |
扩展运算符的参数对象之中,如果有取值函数 get
,这个函数是会执行的
1 | // 并不会抛出错误,因为 x 属性只是被定义,但没执行 |
✎ 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 // falseSymbol
值不能与其他类型的值进行运算1
2
3
4let sym = Symbol('My symbol');
let b = "your symbol is " + sym;
// TypeError: Cannot convert a Symbol value to a stringSymbol
值可以显式转为字符串,也可以转为布尔值,但是不能转为数值1
2
3
4
5
6
7
8
9
10
11
12
13
14
15let 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...in
、for...of
循环中, 也不会被Object.keys()
、Object.getOwnPropertyNames()
、JSON.stringify()
返回。 但是,它也不是私有属性,有一个Object.getOwnPropertySymbols
方法,可以获取指定对象的所有Symbol
属性名
Reflect.ownKeys()
方法可以返回所有类型的键名,包括常规键名和 Symbol
键名
1 | let obj = { |
Symbol.for()
Symbol.for
方法接受一个字符串作为参数,然后搜索有没有以该参数作为名称的Symbol
值。 如果有,就返回这个Symbol
值,否则就新建并返回一个以该字符串为名称的Symbol
值
1 | let s1 = Symbol.for('foo'); |
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 |
Symbol.keyFor()
Symbol.keyFor
方法返回一个已登记的Symbol
类型值的key
1 | let s1 = Symbol.for("foo"); |
上面代码中,变量 s2
属于未登记的 Symbol
值,所以返回 undefined
需要注意的是,Symbol.for
为 Symbol
值登记的名字,是全局环境的,可以在不同的 iframe
或 service worker
中取到同一个值
1 | iframe = document.createElement('iframe'); |
上面代码中,iframe
窗口生成的 Symbol
值,可以在主页面得到
✎ Set 和 Map 数据结构
Set
ES6 提供了新的数据结构
Set
。它类似于数组,但是成员的值都是唯一的,没有重复的值
WeakSet
WeakSet
结构与Set
类似,也是不重复的值的集合。但是,它与Set
有两个区别:首先,
WeakSet
的成员只能是对象,而不能是其他类型的值1
2
3
4
5const 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
3const a = [[1, 2], [3, 4]];
const ws = new WeakSet(a);
// WeakSet {[1, 2], [3, 4]}WeakSet
的一个用处,是储存DOM
节点,而不用担心这些节点从文档移除时,会引发内存泄漏
Map
ES6
提供了Map
数据结构。它类似于对象,也是键值对的集合,但是 “键” 的范围不限于字符串,各种类型的值(包括对象)都可以当作键。 也就是说,Object
结构提供了 “字符串 — 值” 的对应,Map
结构提供了 “值 — 值” 的对应,是一种更完善的Hash
结构实现。 如果你需要 “键值对” 的数据结构,Map
比Object
更合适
作为构造函数,Map
也可以接受一个数组作为参数。该数组的成员是一个个表示键值对的数组
1 | const map = new Map([ |
WeakMap
WeakMap
的专用场合就是,它的键所对应的对象,可能会在将来消失。WeakMap
结构有助于防止内存泄漏
WeakMap
与 Map
的区别:
WeakMap
只接受对象作为键名(null
除外),不接受其他类型的值作为键名1
2
3
4const 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 keyWeakMap
的键名所指向的对象,不计入垃圾回收机制WeakMap
的设计目的在于,有时我们想在某个对象上面存放一些数据,但是这会形成对于这个对象的引用。WeakMap
就是为了解决这个问题而诞生的,它的键名所引用的对象都是弱引用,即垃圾回收机制不将该引用考虑在内。 因此,只要所引用的对象的其他引用都被清除,垃圾回收机制就会释放该对象所占用的内存。 也就是说,一旦不再需要,WeakMap
里面的键名对象和所对应的键值对会自动消失,不用手动删除引用。1
2
3
4
5const wm = new WeakMap();
const el = document.getElementById('example');
wm.set(el, 'some information');
wm.get(el) // "some information"WeakMap
与Map
在 API 上的区别主要是两个:- 没有遍历操作(即没有
key()
、values()
和entries()
方法),也没有size
属性 - 无法清空,即不支持clear方法
- 没有遍历操作(即没有
✎ Proxy
概述
Proxy
用于修改某些操作的默认行为,等同于在语言层面做出修改,所以属于一种 “元编程”(meta programming),即对编程语言进行编程。Proxy
可以理解成,在目标对象之前架设一层 “拦截”,外界对该对象的访问,都必须先通过这层拦截, 因此提供了一种机制,可以对外界的访问进行过滤和改写1
2
3
4
5
6
7
8
9let proxy = new Proxy({}, {
get: function(target, property) {
return 35;
}
});
proxy.time // 35
proxy.name // 35
proxy.title // 35如果
handler
没有设置任何拦截,那就等同于直接通向原对象1
2
3
4
5let target = {};
let handler = {};
let proxy = new Proxy(target, handler);
proxy.a = 'b';
target.a // "b"
Proxy 实例的方法
get()
如果一个属性不可配置(
configurable
)和不可写(writable
),则该属性不能被代理,通过Proxy
对象访问该属性会报错
1 | const target = Object.defineProperties({}, { |
this
问题
虽然
Proxy
可以代理针对目标对象的访问,但它不是目标对象的透明代理,即不做任何拦截的情况下,也无法保证与目标对象的行为一致。 主要原因就是在Proxy
代理的情况下,目标对象内部的this
关键字会指向Proxy
代理
1 | const target = { |
✎ Reflect
概述
Reflect
对象与Proxy
对象一样,也是ES6
为了操作对象而提供的新 APIReflect
对象的设计目的有这样几个:将
Object
对象的一些明显属于语言内部的方法(比如Object.defineProperty
),放到Reflect
对象上。 现阶段,某些方法同时在Object
和Reflect
对象上部署,未来的新方法将只部署在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 obj
和delete obj[name]
, 而Reflect.has(obj, name)
和Reflect.deleteProperty(obj, name)
让它们变成了函数行为1
2
3
4
5// 老写法
'assign' in Object; // true
// 新写法
Reflect.has(Object, 'assign'); // trueReflect
对象的方法与Proxy
对象的方法一一对应,只要是Proxy
对象的方法,就能在Reflect
对象上找到对应的方法1
2
3
4
5
6
7
8
9Proxy(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
13const 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调用
resolve
或reject
并不会终结Promise
的参数函数的执行1
2
3
4
5
6
7
8
9new Promise((resolve, reject) => {
resolve(1);
console.log(2);
}).then(r => {
console.log(r);
});
// 2
// 1说明:立即
resolved
的Promise
是在本轮事件循环的末尾执行,总是晚于本轮循环的同步任务
Promise.prototype.then()
采用链式的
then
,可以指定一组按照次序调用的回调函数。 这时,前一个回调函数,有可能返回的还是一个Promise
对象(即有异步操作), 这时后一个回调函数,就会等待该Promise
对象的状态发生变化,才会被调用
1 | getJSON("/post/1.json") |
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([ |
上面代码中,如果 5 秒之内 fetch
方法无法返回结果,变量 p
的状态就会变为 rejected
,从而触发 catch
方法指定的回调函数
Promise.resolve()
将现有对象转为
Promise
对象
Promise.resolve
方法的参数分成四种情况:
参数是一个
Promise
实例如果参数是
Promise
实例,那么Promise.resolve
将不做任何修改、原封不动地返回这个实例参数是一个
thenable
对象thenable
对象指的是具有then
方法的对象1
2
3
4
5let thenable = {
then: function(resolve, reject) {
resolve(42);
}
};Promise.resolve
方法会将这个对象转为Promise
对象,然后就立即执行thenable
对象的then
方法参数不是具有
then
方法的对象,或根本就不是对象如果参数是一个原始值,或者是一个不具有
then
方法的对象,则Promise.resolve
方法返回一个新的Promise
对象,状态为resolved
1
2
3
4
5
6const p = Promise.resolve('Hello');
p.then(function (s){
console.log(s)
});
// Hello不带有任何参数
Promise.resolve
方法允许调用时不带参数,直接返回一个resolved
状态的Promise
对象注意:立即
resolve
的Promise
对象,是在本轮 “事件循环”(event loop
)的结束时,而不是在下一轮 “事件循环” 的开始时1
2
3
4
5
6
7
8
9
10
11
12
13setTimeout(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 | const thenable = { |
上面代码中,Promise.reject
方法的参数是一个 thenable
对象,执行以后,后面 catch
方法的参数不是 reject
抛出的 “出错了” 这个字符串,而是 thenable
对象。
附加方法
done()
1 | Promise.prototype.done = Promise.prototype.done || function (onFulfilled, onRejected) { |
finally()
1 | Promise.prototype.finally = Promise.prototype.finally || function (callback) { |
✎ Iterator 和 for...of 循环
Iterator(遍历器)的概念
遍历器(
Iterator
)是一种接口,为各种不同的数据结构提供统一的访问机制。 任何数据结构只要部署Iterator
接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)
Iterator
的作用:
- 为各种数据结构,提供一个统一的、简便的访问接口
- 使得数据结构的成员能够按某种次序排列
- 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
25class 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
10let 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
接口的场合
解构赋值
对
Array
和Set
结构进行解构赋值时,会默认调用Symbol.iterator
方法1
2let set = new Set().add('a').add('b').add('c');
let [x, y] = set; // x='a'; y='b'扩展运算符
1
2let str = 'hello';
[...str]; // ['h','e','l','l','o']yield*
1
2
3
4
5
6
7
8
9
10
11
12
13
14let 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
29function 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
循环可以使用的范围包括Array
、Set
和Map
结构、某些类似数组的对象(比如arguments
对象、DOM NodeList
对象)、Generator
对象,以及字符串
与
for...in
的区别:for...in
循环,只能获得对象的键名,不能直接获取键值。ES6 提供for...of
循环,允许遍历获得键值for...of
循环调用遍历器接口,数组的遍历器接口只返回具有数字索引的属性
1
2
3
4
5
6
7
8
9
10let 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 | for (let x of 'a\uD83D\uDC0A') { |
✎ Generator 函数的语法
简介
Generator
函数是ES6
提供的一种异步编程解决方案Generator
函数是一个状态机,封装了多个内部状态Generator
函数执行后返回一个遍历器对象,也就是说,Generator
函数除了状态机,还是一个遍历器对象生成函数
语法:
1 | function * foo(x, y) { /*···*/ } |
总结:
调用
Generator
函数,返回一个遍历器对象,代表Generator
函数的内部指针。 以后,每次调用遍历器对象的next
方法,就会返回一个有着value
和done
两个属性的对象:value
属性表示当前的内部状态的值,是yield
表达式后面那个表达式的值;done
属性是一个布尔值,表示是否遍历结束
yield 表达式
yield
表达式 表示遍历器对象的暂停标识yield
表达式 如果用在另一个表达式之中,必须放在 圆括号 里面yield
表达式 本身没有返回值,或者说总是返回undefined
- 紧跟在
yield
后面的那个表达式的值,作为next()
返回的对象的value
属性值 return
后表示遍历结束状态时,返回值作为遍历后value
的值
1 | function* Gen() { |
与 Iterator 接口的关系
任意一个对象的
Symbol.iterator
方法,等于该对象的遍历器生成函数,调用该函数会返回该对象的一个遍历器对象
由于 Generator
函数就是遍历器生成函数,因此可以把 Generator
赋值给对象的 Symbol.iterator
属性,从而使得该对象具有 Iterator
接口
1 | let myIterable = {}; |
next() 方法的参数
next
方法可以带一个参数,该参数就会被当作 上一个yield
表达式的返回值
注意: 由于 next
方法的参数表示 上一个 yield
表达式的返回值,所以在 第一次 使用 next
方法时,传递参数是无效的。 从语义上讲,第一个 next
方法用来启动遍历器对象,所以不用带有参数
1 | function* foo(x) { |
for...of 循环
1 | function *foo() { |
上面代码使用 for...of
循环,依次显示 2 个 yield
表达式的值。 这里需要注意,一旦 next
方法的返回对象的 done
属性为 true
,for...of
循环就会中止,且不包含该返回对象, 所以上面代码的 return
语句返回的 3
和之后的 4
,不包括在 for...of
循环之中
除了
for...of
循环以外,扩展运算符(...
)、解构赋值和Array.from
方法内部调用的,都是遍历器接口。 这意味着,它们都可以将Generator
函数返回的Iterator
对象,作为参数 自注: 一旦执行了next()
之后,再进行遍历操作(解构赋值、扩展运算等), 结果中将不包含next()
之前的返回结果,即从Generator
对象当前的状态开始遍历
1 | const Gen = function* () { |
for...of
的本质是一个while
循环,所以上面的代码实质上执行的是下面的逻辑
1 | let it = iterateJobs(jobs); |
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
14const 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
属性等于undefined
、done
属性等于true
的对象,即 JavaScript 引擎认为这个Generator
已经运行结束了
Generator.prototype.return()
return
方法,可以返回给定的值,并且终结遍历Generator
函数
如果 Generator
函数内部有 try...finally
代码块,那么 return
方法会推迟到 finally
代码块执行完再执行。
1 | function* numbers () { |
上面代码中,调用 return
方法后,就开始执行 finally
代码块,然后等到 finally
代码块执行完,再执行 return
方法
next()、throw()、return() 的共同点
next()
、throw()
、return()
这三个方法本质上是同一件事,可以放在一起理解。 它们的作用都是让Generator
函数恢复执行,并且使用不同的语句替换yield
表达式
1 | const g = function* (x, y) { |
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
30function* 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
10function* concat(iter1, iter2) {
yield* iter1;
}
// 等同于
function* concat(iter1, iter2) {
for (let value of iter1) {
yield value;
}
}实际上,任何数据结构只要有
Iterator
接口,就可以被yield*
遍历
✎ Generator 函数的异步调用
传统方法
- 回调函数
- 事件监听
- 发布/订阅
- Promise 对象
基本概念
所谓"异步",简单说就是一个任务不是连续完成的,可以理解成该任务被人为分成两段, 先执行第一段,然后转而执行其他任务,等做好了准备,再回过头执行第二段。 相应地,连续的执行就叫做同步。由于是连续执行,不能插入其他任务,所以操作系统从硬盘读取文件的这段时间,程序只能干等着
协程
"协程"(
coroutine
),意思是多个线程互相协作,完成异步任务
运行流程大致如下:
- 协程
A
开始执行 - 协程
A
执行到一半,进入暂停,执行权转移到协程B
- (一段时间后)协程
B
交还执行权 - 协程
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 | async function asyncPrint(val, delay) { |
语法
async
函数返回一个Promise
对象async
函数内部return
语句返回的值,会成为then
方法回调函数的参数1
2
3
4
5
6async 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
7async 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
对象。如果不是,会被转成一个立即resolve
的Promise
对象1
2
3
4
5
6async function f() {
return await 123;
}
f().then(v => console.log(v));
// 123只要一个
await
语句后面的Promise
变为reject
,那么整个async
函数都会中断执行。(个人理解:reject()
改变了整个函数async
返回的promise
对象的状态(rejected),抛出了异常,从而中断函数体的继续执行)1
2
3
4async function f() {
await Promise.reject('出错了');
await Promise.resolve('hello world'); // 不会执行
}解决办法是,将第一个
await
放在try...catch
里面,或用catch
方法处理异常
使用注意点
await
命令后面的Promise
对象,运行结果可能是rejected
,所以最好把await
命令放在try...catch
代码块中多个
await
命令后面的异步操作,如果不存在继发关系,最好让它们同时触发1
2
3
4
5
6
7
8
9
10
11let 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;await
命令只能用在async
函数之中,如果用在普通函数,就会报错
async 函数的实现原理
async
函数的实现原理,就是将Generator
函数和自动执行器,包装在一个函数里
1 | async function fn(args) { |
所有的 async
函数都可以写成上面的第二种形式,其中的 spawn
函数就是自动执行器
✎ Class 的基本语法
基本语法
- 类的方法都定义在
prototype
对象上面 - 类的内部所有定义的方法,都是不可枚举的(
non-enumerable
) - 类和模块的内部,默认就是严格模式,所以不需要使用
use strict
指定运行模式。只要你的代码写在类或模块之中,就只有严格模式可用
constructor 方法
constructor
方法是类的默认方法,通过new
命令生成对象实例时,自动调用该方法。一个类必须有constructor
方法,如果没有显式定义,一个空的constructor
方法会被默认添加1
2
3
4
5
6
7class Point {
}
// 等同于
class Point {
constructor() {}
}类必须使用
new
调用,否则会报错。这是它跟普通构造函数的一个主要区别,后者不用new
也可以执行1
2
3
4
5
6
7
8class Foo {
constructor() {
return Object.create(null);
}
}
Foo();
// TypeError: Class constructor Foo cannot be invoked without 'new'
不存在变量提升
类不存在变量提升(
hoist
),这一点与ES5
完全不同1
2new Foo(); // ReferenceError
class Foo {}
Class 的静态方法
类相当于实例的原型,所有在类中定义的方法,都会被实例继承。如果在一个方法前,加上
static
关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为 “静态方法”。注意: 如果静态方法包含this
关键字,这个this
指的是类,而不是实例静态方法可以与非静态方法重名
1
2
3
4
5
6
7
8
9
10
11
12
13class 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
13class 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
12class 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
14class 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
14class 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
9class ColorPoint extends Point {
}
// 等同于
class ColorPoint extends Point {
constructor(...args) {
super(...args);
}
}
super 关键字
super
这个关键字,既可以当作函数使用,也可以当作对象使用:super
作为函数调用时,代表父类的构造函数。ES6
要求,子类的构造函数必须执行一次super
函数1
2
3
4
5
6
7
8
9
10
11
12class 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()
只能用在子类的构造函数之中,用在其他地方就会报错super
作为对象时,在普通方法中,指向父类的原型对象;在静态方法中,指向父类1
2
3
4
5
6
7
8
9
10
11
12
13
14class 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
8class A {}
class B extends A {
constructor() {
super();
console.log(super); // 报错
}
}
类的 prototype
属性和 __proto__
属性
类的继承是按照下面的模式实现的:
1 | class A { |
可以这样理解:
- 作为一个对象,子类(B)的原型(
__proto__
属性)是父类(A); - 作为一个构造函数,子类(B)的原型对象(
prototype
属性)是父类的原型对象(prototype
属性)的实例
1 | Object.create(A.prototype); |
原生构造函数的继承
ES6
可以自定义原生数据结构(比如Array
、String
等)的子类,这是ES5
无法做到的1
2
3
4
5
6
7
8
9
10
11
12class 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
7class 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
不会在它的外层作用域引入变量eval
和arguments
不能被重新赋值arguments
不会自动反映函数参数的变化- 不能使用
arguments.callee
- 不能使用
arguments.caller
- 禁止
this
指向全局对象(ES6
模块之中,顶层的this
指向undefined
,即不应该在顶层代码使用this
) - 不能使用
fn.caller
和fn.arguments
获取函数调用的堆栈 - 增加了保留字(比如
protected
、static
和interface
)
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
,还是直接输出1
。1
只是一个值,不是接口。正确的写法是下面这样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
2export let foo = 'bar';
setTimeout(() => foo = 'baz', 500);上面代码输出变量
foo
,值为bar
,500
毫秒之后变成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
4import * as circle from './circle';
console.log('圆面积:' + circle.area(4));
console.log('圆周长:' + circle.circumference(14));注意,模块整体加载所在的那个对象(上例是
circle
),应该是可以静态分析的,所以不允许运行时改变。下面的写法都是不允许的1
2
3
4
5import * as circle from './circle';
// 下面两行都是不允许的
circle.foo = 'hello';
circle.area = function () {};
export default 命令
export default
命令用于指定模块的默认输出。显然,一个模块只能有一个默认输出,因此export default
命令只能使用一次所以,import
命令后面才不用加大括号,因为只可能对应一个方法本质上,
export default
就是输出一个叫做default
的变量或方法,然后系统允许你为它取任意名字(export default
本质是将该命令后面的值,赋给default
变量以后再默认)
1 | // modules.js |
✎ 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
,就会生成一个只读引用。 等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。 换句话说,ES6
的import
有点像Unix
系统的 “符号连接”,原始值变了,import
加载的值也会跟着变。 因此,ES6
模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块
1 | // lib.js |
CommonJS 模块的加载原理
CommonJS
的一个模块,就是一个脚本文件。require
命令第一次加载该脚本,就会执行整个脚本,然后在内存生成一个对象
1 | { |
以后需要用到这个模块的时候,就会到 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
这个属性上面。Node
的import
命令加载CommonJS
模块,Node
会自动将module.exports
属性,当作模块的默认输出,即等同于export default xxx
1 | // a.js |
1 | // c.js |
上面代码中,bar
本身是一个对象,不能当作函数调用,只能通过 bar.default
调用
CommonJS 模块加载 ES6 模块
CommonJS
模块加载ES6
模块,不能使用require
命令,而要使用import()
函数。ES6
模块的所有输出接口,会成为输入对象的属性
1 | // es.js |
循环加载
CommonJS 模块的循环加载
CommonJS
模块的重要特性是加载时执行,即脚本代码在require
的时候,就会全部执行。 一旦出现某个模块被"循环加载",就只输出已经执行的部分,还未执行的部分不会输出
ES6 模块的循环加载
ES6
模块是动态引用,如果使用import
从一个模块加载变量(即import foo from 'foo'
), 那些变量不会被缓存,而是成为一个指向被加载模块的引用,需要开发者自己保证,真正取值的时候能够取到值
✎ 编程风格
✎ 读懂规格
✎ ArrayBuffer
ArrayBuffer
对象、TypedArray
视图和DataView
视图是 JavaScript 操作二进制数据的一个接口
二进制数组由三类对象组成:
ArrayBuffer
对象:代表内存之中的一段二进制数据,可以通过 “视图” 进行操作。“视图” 部署了数组接口,这意味着,可以用数组的方法操作内存TypedArray
视图:共包括 9 种类型的视图,比如Uint8Array
(无符号 8 位整数)数组视图,Int16Array
(16 位整数)数组视图,Float32Array
(32 位浮点数)数组视图等等DataView
视图:可以自定义复合格式的视图,比如第一个字节是Uint8
(无符号 8 位整数)、第二、三个字节是Int16
(16 位整数)、第四个字节开始是Float32
(32 位浮点数)等等,此外还可以自定义字节序列
简单说,ArrayBuffer
对象代表原始的二进制数据,TypedArray
视图用来读写简单类型的二进制数据,DataView
视图用来读写复杂类型的二进制数据。
注意:二进制数组并不是真正的数组,而是类似数组的对象
很多浏览器操作的 API
,用到了二进制数组操作二进制数据,下面是其中的几个:
File API
XMLHttpRequest
Fetch API
Canvas
WebSockets