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