概述
前言
之前对ES6也有过学习使用,但是都没有深入系统去看看文档书籍,这里开始对一些ES6语法进行整理总结。
本文基于阮一峰ES6入门,线上地址:阮一峰ES6入门
let和const命令
1、let命令
- 如果使用
let
,声明的变量仅在块级作用域内有效
var a = [];
for (let i = 0; i < 10; i++) { //如果这里用var,则下面的输出为10
a[i] = function () {
console.log(i);
};
}
a[6](); // 6
- for循环有一个特别之处,就是设置循环变量的那部分是一个父作用域,而循环体内部是一个单独的子作用域。
for (let i = 0; i < 3; i++) {
let i = 'abc';
console.log(i);
}
// abc
// abc
// abc
- 不存在变量提升
- 暂时性死区,相当于
var tmp = 123;
if (true) {
tmp = 'abc'; // ReferenceError
let tmp;
}
function bar(x = y, y = 2) {
return [x, y];
}
bar(); // 报错
// 不报错
var x = x;
// 报错
let x = x;
// ReferenceError: x is not defined
暂时性死区的本质就是,只要一进入当前作用域,所要使用的变量就已经存在了,但是不可获取,只有等到声明变量的那一行代码出现,才可以获取和使用该变量。
- 不允许重复声明
2、块级作用域
块级作用域与函数声明
- ES5 规定,函数只能在顶层作用域和函数作用域之中声明,不能在块级作用域声明。
- ES6 引入了块级作用域,明确允许在块级作用域之中声明函数。ES6 规定,块级作用域之中,函数声明语句的行为类似于
let
,在块级作用域之外不可引用。
function f() { console.log('I am outside!'); }
(function () {
if (false) {
// 重复声明一次函数f
function f() { console.log('I am inside!'); }
}
f();
}());
上面代码在 ES5 中运行,会得到“I am inside!”,因为在if内声明的函数f会被提升到函数头部
ES6 就完全不一样了,理论上会得到“I am outside!”。因为块级作用域内声明的函数类似于let
,对作用域之外没有影响。但是,如果你真的在 ES6 浏览器中运行一下上面的代码,是会报错的,因为如果改变了块级作用域内声明的函数的处理规则,显然会对老代码产生很大影响。为了减轻因此产生的不兼容问题,ES6 在附录 B里面规定,浏览器的实现可以不遵守上面的规定,有自己的行为方式。
1、 允许在块级作用域内声明函数。
2、函数声明类似于var
,即会提升到全局作用域或函数作用域的头部。
3、同时,函数声明还会提升到所在的块级作用域的头部。
考虑到环境导致的行为差异太大,应该避免在块级作用域内声明函数。如果确实需要,也应该写成函数表达式,而不是函数声明语句。
- 另外,还有一个需要注意的地方。ES6 的块级作用域必须有大括号,如果没有大括号,JavaScript 引擎就认为不存在块级作用域。
3、const命令
const
声明一个只读的常量。一旦声明,常量的值就不能改变。const
的作用域与let
命令相同:只在声明所在的块级作用域内有效。const
命令声明的常量也是不提升,同样存在暂时性死区,只能在声明的位置后面使用。const
声明的常量,也与let
一样不可重复声明。
本质
const实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动。如果真的想将对象冻结,应该使用Object.freeze方法。
var constantize = (obj) => {
Object.freeze(obj);
Object.keys(obj).forEach( (key, i) => {
if ( typeof obj[key] === 'object' ) {
constantize( obj[key] );
}
});
};
- 顶层对象的属性
var a = 1;
// 如果在 Node 的 REPL 环境,可以写成 global.a
// 或者采用通用方法,写成 this.a
window.a // 1
let b = 1;
window.b // undefined
- globalThis对象
变量的解构赋值
数组的解构赋值
- 对于 Set 结构,也可以使用数组的解构赋值。
let [x, y, z] = new Set(['a', 'b', 'c']);
x // "a"
- 事实上,只要某种数据结构具有 Iterator 接口,都可以采用数组形式的解构赋值。
- 默认值
let [foo = true] = []; //foo = true
let [x, y = 'b'] = ['a']; // x='a', y='b'
let [x, y = 'b'] = ['a', undefined]; // x='a', y='b'
1、注意,ES6 内部使用严格相等运算符(===),判断一个位置是否有值。
所以,只有当一个数组成员严格等于undefined,默认值才会生效。
function f() {
console.log('aaa');
}
let [x = f()] = [1];
2、默认值可以引用解构赋值的其他变量,但该变量必须已经声明。
let [x = 1, y = x] = []; // x=1; y=1
let [x = 1, y = x] = [2]; // x=2; y=2
let [x = 1, y = x] = [1, 2]; // x=1; y=2
let [x = y, y = 1] = []; // ReferenceError: y is not defined
对象的解构赋值
- 对象的解构与数组有一个重要的不同。数组的元素是按次序排列的,变量的取值由它的位置决定;而对象的属性没有次序,变量必须与属性同名,才能取到正确的值。
let { baz } = { foo: 'aaa', bar: 'bbb' };
baz // undefined
- 如果变量名与属性名不一致,必须写成下面这样。
let { foo: baz } = { foo: 'aaa', bar: 'bbb' };
baz // "aaa"
- 对象的解构赋值是下面形式的简写。也就是说,对象的解构赋值的内部机制,是先找到同名属性,然后再赋给对应的变量。真正被赋值的是后者,而不是前者。
let { foo: foo, bar: bar } = { foo: 'aaa', bar: 'bbb' };
- 与数组一样,解构也可以用于嵌套结构的对象。
const node = {
loc: {
start: {
line: 1,
column: 5
}
}
};
let { loc, loc: { start }, loc: { start: { line }} } = node;
line // 1
loc // Object {start: Object}
start // Object {line: 1, column: 5}
上面代码有三次解构赋值,分别是对loc、start、line三个属性的解构赋值。注意,最后一次对line属性的解构赋值之中,只有line是变量,loc和start都是模式,不是变量。
// 报错
let {foo: {bar}} = {baz: 'baz'};
上面代码中,等号左边对象的foo属性,对应一个子对象。该子对象的bar属性,解构时会报错。原因很简单,因为foo这时等于undefined,再取子属性就会报错。
- 注意,对象的解构赋值可以取到继承的属性。
- 默认值
- 注意点
(1)如果要将一个已经声明的变量用于解构赋值,必须非常小心。
// 错误的写法
let x;
{x} = {x: 1}; //不能加var或let,因为x已经声明过了
// SyntaxError: syntax error
上面代码的写法会报错,因为 JavaScript 引擎会将{x}理解成一个代码块,从而发生语法错误。
只有不将大括号写在行首,避免 JavaScript 将其解释为代码块,才能解决这个问题。
// 正确的写法
let x;
({x} = {x: 1});
(2)解构赋值允许等号左边的模式之中,不放置任何变量名。
(3)由于数组本质是特殊的对象,因此可以对数组进行对象属性的解构。
let arr = [1, 2, 3];
let {0 : first, [arr.length - 1] : last} = arr;
first // 1
last // 3
字符串的解构赋值
- 字符串也可以解构赋值。这是因为此时,字符串被转换成了一个类似数组的对象。
const [a, b] = 'hello';
a // "h"
b // "e"
- 类似数组的对象都有一个length属性,因此还可以对这个属性解构赋值。
let {length : len} = 'hello';
len // 5
数值和布尔值的解构赋值
- 解构赋值时,如果等号右边是数值和布尔值,则会先转为对象。
let {toString: s} = 123;
s === Number.prototype.toString // true
let {toString: s} = true;
s === Boolean.prototype.toString // true
上面代码中,数值和布尔值的包装对象都有toString属性,因此变量s都能取到值。
- 解构赋值的规则是,只要等号右边的值不是对象或数组,就先将其转为对象。由于undefined和null无法转为对象,所以对它们进行解构赋值,都会报错。
函数参数的解构赋值
function add([x, y]){
return x + y;
}
add([1, 2]); // 3
[[1, 2], [3, 4]].map(([a, b]) => a + b);
// [ 3, 7 ]
- 函数参数的解构也可以使用默认值。
function move({x = 0, y = 0} = {}) {
return [x, y];
}
move({x: 3, y: 8}); // [3, 8]
move({x: 3}); // [3, 0]
move({}); // [0, 0]
move(); // [0, 0]
function move({x, y} = { x: 0, y: 0 }) {
return [x, y];
}
move({x: 3, y: 8}); // [3, 8]
move({x: 3}); // [3, undefined]
move({}); // [undefined, undefined]
move(); // [0, 0]
undefined
就会触发函数参数的默认值。
[1, undefined, 3].map((x = 'yes') => x);
// [ 1, 'yes', 3 ]
圆括号问题
不能使用圆括号情况
(1)变量声明语句
// 全部报错
let [(a)] = [1];
let {x: (c)} = {};
let ({x: c}) = {};
let {(x: c)} = {};
let {(x): c} = {};
let { o: ({ p: p }) } = { o: { p: 2 } };
(2)函数参数
函数参数也属于变量声明,因此不能带有圆括号。
// 报错
function f([(z)]) { return z; }
// 报错
function f([z,(x)]) { return x; }
(3)赋值语句的模式
// 全部报错
({ p: a }) = { p: 42 };
([a]) = [5];
上面代码将整个模式放在圆括号之中,导致报错。
// 报错
[({ p: a }), { x: c }] = [{}, {}];
可以使用圆括号的情况
可以使用圆括号的情况只有一种:赋值语句的非模式部分,可以使用圆括号。
[(b)] = [3]; // 正确
({ p: (d) } = {}); // 正确
[(parseInt.prop)] = [3]; // 正确
用途
(1)交换变量的值
let x = 1;
let y = 2;
[x, y] = [y, x];
(2)从函数返回多个值
函数只能返回一个值,如果要返回多个值,只能将它们放在数组或对象里返回。有了解构赋值,取出这些值就非常方便。
(3)函数参数的定义
解构赋值可以方便地将一组参数与变量名对应起来。
// 参数是一组有次序的值
function f([x, y, z]) { ... }
f([1, 2, 3]);
// 参数是一组无次序的值
function f({x, y, z}) { ... }
f({z: 3, y: 2, x: 1});
(4)提取 JSON 数据
(5)函数参数的默认值
(6)遍历 Map 结构
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) {
// ...
}
(7)输入模块的指定方法
加载模块时,往往需要指定输入哪些方法。解构赋值使得输入语句非常清晰。
const { SourceMapConsumer, SourceNode } = require("source-map");
字符串的拓展
字符串的Unicode表示法
- ES6 加强了对 Unicode 的支持,允许采用
uxxxx
形式表示一个字符,其中xxxx表示字符的 Unicode 码点。但是,这种表示法只限于码点在u0000~uFFFF
之间的字符。超出这个范围的字符,必须用两个双字节的形式表示。 - ES6 对这一点做出了改进,只要将码点放入大括号,就能正确解读该字符。
"u{20BB7}"
// "????"
'u{1F680}' === 'uD83DuDE80'
// true
- 有了这种表示法之后,JavaScript 共有 6 种方法可以表示一个字符。
'z' === 'z' // true
'172' === 'z' // true
'x7A' === 'z' // true
'u007A' === 'z' // true
'u{7A}' === 'z' // true
字符串的遍历器接口
ES6 为字符串添加了遍历器接口(详见《Iterator》一章),使得字符串可以被for…of循环遍历。
for (let codePoint of 'foo') {
console.log(codePoint)
}
// "f"
// "o"
// "o"
- 除了遍历字符串,这个遍历器最大的优点是可以识别大于0xFFFF的码点,传统的for循环无法识别这样的码点。
let text = String.fromCodePoint(0x20BB7);
for (let i = 0; i < text.length; i++) {
console.log(text[i]);
}
// " "
// " "
for (let i of text) {
console.log(i);
}
// "????"
上面代码中,字符串text只有一个字符,但是for循环会认为它包含两个字符(都不可打印),而for…of循环会正确识别出这一个字符。
直接输入 U+2028 和 U+2029
JavaScript 规定有5个字符,不能在字符串里面直接使用,只能使用转义形式。
- U+005C:反斜杠(reverse solidus)
- U+000D:回车(carriage return)
- U+2028:行分隔符(line separator)
- U+2029:段分隔符(paragraph separator)
- U+000A:换行符(line feed)
举例来说,字符串里面不能直接包含反斜杠,一定要转义写成或者u005c。
问题
JSON 格式允许字符串里面直接使用 U+2028(行分隔符)和 U+2029(段分隔符)。这样一来,服务器输出的 JSON 被JSON.parse解析,就有可能直接报错。为了消除这个报错,ES2019 允许 JavaScript 字符串直接输入 U+2028(行分隔符)和 U+2029(段分隔符)。注意,模板字符串现在就允许直接输入这两个字符。另外,正则表达式依然不允许直接输入这两个字符,这是没有问题的,因为 JSON 本来就不允许直接包含正则表达式。
JSON.stringify() 的改造
模板字符串
- 传统的 JavaScript 语言,输出模板通常是这样写的(下面使用了 jQuery 的方法)。
$('#result').append(
'There are <b>' + basket.count + '</b> ' +
'items in your basket, '
);
- 如果在模板字符串中需要使用反引号,则前面要用反斜杠转义。
- 如果使用模板字符串表示多行字符串,所有的空格和缩进都会被保留在输出之中。如果你不想要这个换行,可以使用trim方法消除它。
- 模板字符串中嵌入变量,需要将变量名写在
${}
之中。大括号内部可以放入任意的 JavaScript 表达式,可以进行运算,以及引用对象属性。 - 模板字符串之中还能调用函数。
- 如果大括号中的值不是字符串,将按照一般的规则转为字符串。比如,大括号中是一个对象,将默认调用对象的toString方法。
- 模板字符串甚至还能嵌套。
字符串的新增方法
String.fromCodePoint()
- ES5 提供
String.fromCharCode()
方法,用于从 Unicode 码点返回对应字符,但是这个方法不能识别码点大于0xFFFF的字符。 - ES6 提供了String.fromCodePoint()方法,可以识别大于0xFFFF的字符,弥补了
String.fromCharCode()
方法的不足。 - 如果
String.fromCodePoint
方法有多个参数,则它们会被合并成一个字符串返回。 - 注意,
fromCodePoint
方法定义在String对象上,而codePointAt
方法定义在字符串的实例对象上。
String.raw()
实例方法
1、codePointAt()
2、normalize()
3、includes(), startsWith(), endsWith()
- 这三个方法都支持第二个参数,表示开始搜索的位置。
- 使用第二个参数n时,
endsWith
的行为与其他两个方法有所不同。它针对前n个字符,而其他两个方法针对从第n个位置直到字符串结束。
4、repeat()
- repeat方法返回一个新字符串,表示将原字符串重复n次。
5、padStart(),padEnd()
- ES2017 引入了字符串补全长度的功能。如果某个字符串不够指定长度,会在头部或尾部补全。
padStart()
用于头部补全,padEnd()
用于尾部补全。
6、trimStart(),trimEnd()
- 它们返回的都是新字符串,不会修改原始字符串。
- 浏览器还部署了额外的两个方法,trimLeft()是trimStart()的别名,trimRight()是trimEnd()的别名。
7、matchAll()
正则的扩展
正则的扩展
- ES5 不允许此时使用第二个参数添加修饰符,否则会报错。
var regex = new RegExp(/xyz/, 'i');
// Uncaught TypeError: Cannot supply flags when constructing one RegExp from another
- ES6 改变了这种行为。如果RegExp构造函数第一个参数是一个正则对象,那么可以使用第二个参数指定修饰符。而且,返回的正则表达式会忽略原有的正则表达式的修饰符,只使用新指定的修饰符。
new RegExp(/abc/ig, 'i').flags
// "i"
后行断言
- “先行断言”:
/x(?=y)/
/x(?!y)/
- “后行断言”:
/(?<=$)d+/
/(?<!y)x/
- “后行断言”的实现,需要先匹配/(?<=y)x/的x,然后再回到左边,匹配y的部分。这种“先右后左”的执行顺序,与所有其他正则操作相反,导致了一些不符合预期的行为
/(?<=(d+)(d+))$/.exec('1053') // ["", "1", "053"]
/^(d+)(d+)$/.exec('1053') // ["1053", "105", "3"]
- 其次,“后行断言”的反斜杠引用,也与通常的顺序相反,必须放在对应的那个括号之前。
String.prototype.matchAll()
它返回的是一个遍历器(Iterator),而不是数组。
正则匹配索引
exec()方法的返回结果加上indices属性,在这个属性上面可以拿到匹配的开始位置和结束位置。
修饰符
1、u 修饰符,RegExp.prototype.unicode 属性
2、y修饰符,RegExp.prototype.sticky 属性
3、RegExp.prototype.flags 属性
4、s 修饰符:dotAll 模式
5、Unicode 属性类
6、具名组匹配
const RE_DATE = /(?<year>d{4})-(?<month>d{2})-(?<day>d{2})/;
可以在exec方法返回结果的groups属性上引用该组名。
7、解构赋值和替换
let {groups: {one, two}} = /^(?<one>.*):(?<two>.*)$/u.exec('foo:bar');
one // foo
two // bar
- 字符串替换时,使用$<组名>引用具名组。
let re = /(?<year>d{4})-(?<month>d{2})-(?<day>d{2})/u;
'2015-01-02'.replace(re, '$<day>/$<month>/$<year>')
// '02/01/2015'
- replace方法的第二个参数也可以是函数,该函数的参数序列如下。
'2015-01-02'.replace(re, (
matched, // 整个匹配结果 2015-01-02
capture1, // 第一个组匹配 2015
capture2, // 第二个组匹配 01
capture3, // 第三个组匹配 02
position, // 匹配开始的位置 0
S, // 原字符串 2015-01-02
groups // 具名组构成的一个对象 {year, month, day}
) => {
let {day, month, year} = groups;
return `${day}/${month}/${year}`;
});
8、引用
如果要在正则表达式内部引用某个“具名组匹配”,可以使用k<组名>的写法。
数值的拓展
二进制和八进制表示法
- ES6 提供了二进制和八进制数值的新的写法,分别用前缀0b(或0B)和0o(或0O)表示。
- 从 ES5 开始,在严格模式之中,八进制就不再允许使用前缀0表示,ES6 进一步明确,要使用前缀0o表示。
- 如果要将0b和0o前缀的字符串数值转为十进制,要使用Number方法。
Number.isFinite(), Number.isNaN()
- 如果参数类型不是数值,
Number.isFinite
一律返回false。 - 它们与传统的全局方法isFinite()和isNaN()的区别在于,传统方法先调用Number()将非数值的值转为数值,再进行判断,而这两个新方法只对数值有效
其他方法
1、Number.parseInt(), Number.parseFloat()
2、Number.isInteger()
- JavaScript 内部,整数和浮点数采用的是同样的储存方法,所以 25 和 25.0 被视为同一个值。
- 如果参数不是数值,
Number.isInteger
返回false。 - 小数的精度达到了小数点后16个十进制位,转成二进制位超过了53个二进制位,导致最后的那个2被丢弃
Number.isInteger(3.0000000000000002) // true
- 如果一个数值的绝对值小于Number.MIN_VALUE(5E-324),即小于 JavaScript 能够分辨的最小值,会被自动转为 0。
3、Number.EPSILON
- 等于 2 的 -52 次方
Number.EPSILON
实际上是 JavaScript 能够表示的最小精度。
4、安全整数和Number.isSafeInteger()
Math对象的拓展
1、Math.trunc()
Math.trunc
方法用于去除一个数的小数部分,返回整数部分。- 对于非数值,
Math.trunc
内部使用Number
方法将其先转为数值。 - 对于空值和无法截取整数的值,返回NaN。
2、Math.sign()
- 它会返回五种值。
参数为正数,返回+1;
参数为负数,返回-1;
参数为 0,返回0;
参数为-0,返回-0;
其他值,返回NaN。 - 如果参数是非数值,会自动转为数值。对于那些无法转为数值的值,会返回NaN。
3、Math.cbrt()
Math.cbrt()
方法用于计算一个数的立方根。对于非数值,Math.cbrt()
方法内部也是先使用Number()
方法将其转为数值。
4、Math.clz32()
Math.clz32()
方法将参数转为 32 位无符号整数的形式,然后返回这个 32 位值里面有多少个前导 0。- 对于小数,Math.clz32方法只考虑整数部分。
- 对于空值或其他类型的值,Math.clz32方法会将它们先转为数值,然后再计算。
5、Math.imul()
Math.imul
方法返回两个数以 32 位带符号整数形式相乘的结果,返回的也是一个 32 位的带符号整数。
6、Math.fround()
Math.fround
方法返回一个数的32位单精度浮点数形式。
7、Math.hypot()
Math.hypot
方法返回所有参数的平方和的平方根。
8、Math.expm1()
Math.expm1(x)
返回 ex - 1,即Math.exp(x) - 1
。
9、Math.log1p()
Math.log1p(x)
方法返回1 + x的自然对数,即Math.log(1 + x)
。如果x小于-1,返回NaN。
10、Math.log10()
Math.log10(x)
返回以 10 为底的x的对数。如果x小于 0,则返回 NaN。
11、Math.log2()
Math.log2(x)
返回以 2 为底的x的对数。如果x小于 0,则返回 NaN。
12、双曲函数方法
ES6 新增了 6 个双曲函数方法。
Math.sinh(x)
返回x的双曲正弦(hyperbolic sine)Math.cosh(x)
返回x的双曲余弦(hyperbolic cosine)Math.tanh(x)
返回x的双曲正切(hyperbolic tangent)Math.asinh(x)
返回x的反双曲正弦(inverse hyperbolic sine)Math.acosh(x)
返回x的反双曲余弦(inverse hyperbolic cosine)Math.atanh(x)
返回x的反双曲正切(inverse hyperbolic tangent)
13、指数运算符
- ES2016 新增了一个指数运算符(**)。这个运算符的一个特点是右结合,而不是常见的左结合。多个指数运算符连用时,是从最右边开始计算的。
// 相当于 2 ** (3 ** 2)
2 ** 3 ** 2
// 512
指数运算符可以与等号结合,形成一个新的赋值运算符(**=)。
BigInt 数据类型
简介
JavaScript 所有数字都保存成 64 位浮点数,这给数值的表示带来了两大限制。一是数值的精度只能到 53 个二进制位(相当于 16 个十进制位),大于这个范围的整数,JavaScript 是无法精确表示的,这使得 JavaScript 不适合进行科学和金融方面的精确计算。二是大于或等于2的1024次方的数值,JavaScript 无法表示,会返回Infinity
。
- 为了与 Number 类型区别,BigInt 类型的数据必须添加后缀n。
- BigInt 与普通整数是两种值,它们之间并不相等。
typeof
运算符对于 BigInt 类型的数据返回bigint。- BigInt 可以使用负号(-),但是不能使用正号(+),因为会与 asm.js 冲突。
BigInt 对象
BigInt(
)构造函数必须有参数,而且参数必须可以正常转为数值,下面的用法都会报错。
new BigInt() // TypeError
BigInt(undefined) //TypeError
BigInt(null) // TypeError
BigInt('123n') // SyntaxError
BigInt('abc') // SyntaxError
- 参数如果是小数,也会报错。
- 方法
BigInt.prototype.toString()
BigInt.prototype.valueOf()
- 转换规则
可以使用Boolean()、Number()和String()
这三个方法,将 BigInt 可以转为布尔值、数值和字符串类型。
Boolean(0n) // false
Boolean(1n) // true
Number(1n) // 1
String(1n) // "1"
上面代码中,注意最后一个例子,转为字符串时后缀n会消失。
另外,取反运算符(!)也可以将 BigInt 转为布尔值。
- 数学运算
几乎所有的数值运算符都可以用在 BigInt,但是有两个例外。
不带符号的右移位运算符>>>
一元的求正运算符+ - BigInt 不能与普通数值进行混合运算。
- 比较运算符(比如>)和相等运算符(==)允许 BigInt 与其他类型的值混合计算,因为这样做不会损失精度。
函数的拓展
函数参数的默认值
基本用法
- 参数变量是默认声明的,所以不能用let或const再次声明。
function foo(x = 5) {
let x = 1; // error
const x = 2; // error
}
- 使用参数默认值时,函数不能有同名参数。
// 不报错
function foo(x, x, y) {
// ...
}
// 报错
function foo(x, x, y = 1) {
// ...
}
- 另外,一个容易忽略的地方是,参数默认值不是传值的,而是每次都重新计算默认值表达式的值。也就是说,参数默认值是惰性求值的。
参数默认值的位置
- 通常情况下,定义了默认值的参数,应该是函数的尾参数。因为这样比较容易看出来,到底省略了哪些参数。如果非尾部的参数设置默认值,实际上这个参数是没法省略的。
函数的 length 属性
- 指定了默认值以后,函数的length属性,将返回没有指定默认值的参数个数。也就是说,指定了默认值后,length属性将失真。
(function(...args) {}).length // 0
- 如果设置了默认值的参数不是尾参数,那么length属性也不再计入后面的参数了。
(function (a = 0, b, c) {}).length // 0
(function (a, b = 1, c) {}).length // 1
作用域
- 一旦设置了参数的默认值,函数进行声明初始化时,参数会形成一个单独的作用域(context)。等到初始化结束,这个作用域就会消失。这种语法行为,在不设置参数默认值时,是不会出现的。
let x = 1;
function f(y = x) {
let x = 2;
console.log(y);
}
f() // 1
上面代码中,函数f调用时,参数y = x形成一个单独的作用域。这个作用域里面,变量x本身没有定义,所以指向外层的全局变量x。函数调用时,函数体内部的局部变量x影响不到默认值变量x。
- 参数
x = x
形成一个单独作用域。实际执行的是let x = x
,由于暂时性死区的原因,这行代码会报错”x 未定义“。
var x = 1;
function foo(x = x) {
// ...
}
foo() // ReferenceError: x is not defined
- 如果参数的默认值是一个函数,该函数的作用域也遵守这个规则。
var x = 1;
function foo(x, y = function() { x = 2; }) {
var x = 3;
y();
console.log(x);
}
foo() // 3
x // 1
应用
利用参数默认值,可以指定某一个参数不得省略,如果省略就抛出一个错误。
rest 参数
- ES6 引入 rest 参数(形式为…变量名),用于获取函数的多余参数,这样就不需要使用arguments对象了。rest 参数搭配的变量是一个数组,该变量将多余的参数放入数组中。
- arguments对象不是数组,而是一个类似数组的对象。所以为了使用数组的方法,必须使用Array.prototype.slice.call先将其转为数组。
- 注意,rest 参数之后不能再有其他参数(即只能是最后一个参数),否则会报错。
- 函数的
length
属性,不包括 rest 参数。
严格模式
- ES2016规定只要函数参数使用了默认值、解构赋值、或者扩展运算符,那么函数内部就不能显式设定为严格模式,否则会报错。
name 属性
- 函数的name属性,返回该函数的函数名。
- 如果将一个匿名函数赋值给一个变量,ES5 的name属性,会返回空字符串,而 ES6 的name属性会返回实际的函数名。
var f = function () {};
// ES5
f.name // ""
// ES6
f.name // "f"
- 如果将一个具名函数赋值给一个变量,则 ES5 和 ES6 的name属性都返回这个具名函数原本的名字。
const bar = function baz() {};
// ES5
bar.name // "baz"
// ES6
bar.name // "baz"
- Function构造函数返回的函数实例,name属性的值为anonymous。
(new Function).name // "anonymous"
- bind返回的函数,name属性值会加上bound前缀。
箭头函数
基本用法
- 由于大括号被解释为代码块,所以如果箭头函数直接返回一个对象,必须在对象外面加上括号,否则会报错。
// 报错
let getTempItem = id => { id: id, name: "Temp" };
// 不报错
let getTempItem = id => ({ id: id, name: "Temp" });
- 下面是一种特殊情况,虽然可以运行,但会得到错误的结果。
let foo = () => { a: 1 };
foo() // undefined
使用注意点
(1)函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象。
(2)不可以当作构造函数,也就是说,不可以使用new命令,否则会抛出一个错误。
(3)不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。
(4)不可以使用yield命令,因此箭头函数不能用作 Generator 函数。
- 上面四点中,第一点尤其值得注意。this对象的指向是可变的,但是在箭头函数中,它是固定的。
function foo() {
return () => {
return () => {
return () => {
console.log('id:', this.id);
};
};
};
}
var f = foo.call({id: 1});
var t1 = f.call({id: 2})()(); // id: 1
var t2 = f().call({id: 3})(); // id: 1
var t3 = f()().call({id: 4}); // id: 1
- 除了this,以下三个变量在箭头函数之中也是不存在的,指向外层函数的对应变量:arguments、super、new.target。
- 由于箭头函数没有自己的this,所以当然也就不能用call()、apply()、bind()这些方法去改变this的指向
不适用场合
const cat = {
lives: 9,
jumps: () => {
this.lives--;
}
}
var button = document.getElementById('press');
button.addEventListener('click', () => {
this.classList.toggle('on');
});
尾调用优化
- 注意,目前只有 Safari 浏览器支持尾调用优化,Chrome 和 Firefox 都不支持。
- 尾递归
严格模式
- ES6 的尾调用优化只在严格模式下开启,正常模式是无效的。
这是因为在正常模式下,函数内部有两个变量,可以跟踪函数的调用栈。
func.arguments:返回调用时函数的参数。
func.caller:返回调用当前函数的那个函数。
尾调用优化发生时,函数的调用栈会改写,因此上面两个变量就会失真。严格模式禁用这两个变量,所以尾调用模式仅在严格模式下生效。
- 正常模式下实现尾递归优化。
蹦床函数(trampoline)可以将递归执行转为循环执行。
function trampoline(f) {
while (f && f instanceof Function) {
f = f();
}
return f;
}
然后,要做的就是将原来的递归函数,改写为每一步返回另一个函数。
function sum(x, y) {
if (y > 0) {
return sum.bind(null, x + 1, y - 1);
} else {
return x;
}
}
- Function.prototype.toString()
数组的扩展
扩展运算符
含义
- 扩展运算符(spread)是三个点(…)。它好比 rest 参数的逆运算,将一个数组转为用逗号分隔的参数序列。
- 扩展运算符后面还可以放置表达式。
const arr = [
...(x > 0 ? ['a'] : []),
'b',
];
- 如果扩展运算符后面是一个空数组,则不产生任何效果。
作用
- 字符串
扩展运算符还可以将字符串转为真正的数组。
[...'hello']
// [ "h", "e", "l", "l", "o" ]
上面的写法,有一个重要的好处,那就是能够正确识别四个字节的 Unicode 字符。
- 实现了 Iterator 接口的对象
任何定义了遍历器(Iterator)接口的对象(参阅 Iterator 一章),都可以用扩展运算符转为真正的数组。
方法
Array.from()
Array.from
方法用于将两类对象转为真正的数组:类似数组的对象(array-like object)和可遍历(iterable)的对象(包括 ES6 新增的数据结构 Set 和 Map)。实际应用中,常见的类似数组的对象是 DOM 操作返回的 NodeList 集合,以及函数内部的arguments对象。所谓类似数组的对象,本质特征只有一点,即必须有length属性。Array.from
还可以接受第二个参数,作用类似于数组的map方法,用来对每个元素进行处理,将处理后的值放入返回的数组。
Array.of()
- Array.of方法用于将一组值,转换为数组。
Array() // []
Array(3) // [, , ,]
Array(3, 11, 8) // [3, 11, 8]
- Array.of方法可以用下面的代码模拟实现。
function ArrayOf(){
return [].slice.call(arguments);
}
数组实例的 copyWithin()
数组实例的 find() 和 findIndex()
- 如果没有符合条件的成员,则返回undefined。
- 如果所有成员都不符合条件,则返回-1。
- 这两个方法都可以接受第二个参数,用来绑定回调函数的this对象。
- 另外,这两个方法都可以发现NaN,弥补了数组的indexOf方法的不足。
findIndex
方法可以借助Object.is
方法做到。
[NaN].indexOf(NaN)
// -1
[NaN].findIndex(y => Object.is(NaN, y))
// 0
数组实例的 fill()
- 如果填充的类型为对象,那么被赋值的是同一个内存地址的对象,而不是深拷贝对象。
数组实例
entries(),keys() 和 values()
- 它们都返回一个遍历器对象
for (let [index, elem] of ['a', 'b'].entries()) {
console.log(index, elem);
}
// 0 "a"
// 1 "b"
//如果不使用for...of循环,可以手动调用遍历器对象的next方法,进行遍历。
let letter = ['a', 'b', 'c'];
let entries = letter.entries();
console.log(entries.next().value); // [0, 'a']
console.log(entries.next().value); // [1, 'b']
console.log(entries.next().value); // [2, 'c']
includes()
- 该方法的第二个参数表示搜索的起始位置,默认为0。如果第二个参数为负数,则表示倒数的位置,如果这时它大于数组长度(比如第二个参数为-4,但数组长度为3),则会重置为从0开始。
- indexOf
[NaN].indexOf(NaN)
// -1
//includes使用的是不一样的判断算法,就没有这个问题。
[NaN].includes(NaN)
// true
- 另外,Map 和 Set 数据结构有一个has方法,需要注意与includes区分。
Map 结构的has方法,是用来查找键名的
Set 结构的has方法,是用来查找值的
flat(),flatMap()
- 该方法返回一个新数组,对原数据没有影响。
- 如果原数组有空位,flat()方法会跳过空位。
[1, 2, , 4, 5].flat()
// [1, 2, 4, 5]
flatMap()
方法对原数组的每个成员执行一个函数(相当于执行Array.prototype.map()
),然后对返回值组成的数组执行flat()方法。该方法返回一个新数组,不改变原数组。
// 相当于 [[2, 4], [3, 6], [4, 8]].flat()
[2, 3, 4].flatMap((x) => [x, x * 2])
// [2, 4, 3, 6, 4, 8]
flatMap()
只能展开一层数组。
// 相当于 [[[2]], [[4]], [[6]], [[8]]].flat()
[1, 2, 3, 4].flatMap(x => [[x * 2]])
// [[2], [4], [6], [8]]
flatMap()
方法的参数是一个遍历函数,该函数可以接受三个参数,分别是当前数组成员、当前数组成员的位置(从零开始)、原数组。
arr.flatMap(function callback(currentValue[, index[, array]]) {
// ...
}[, thisArg])
flatMap()
方法还可以有第二个参数,用来绑定遍历函数里面的this。
数组的空位
- 空位不是undefined,一个位置的值等于undefined,依然是有值的。空位是没有任何值,in运算符可以说明这一点。
0 in [undefined, undefined, undefined] // true
0 in [, , ,] // false
- ES5 对空位的处理,已经很不一致了,大多数情况下会忽略空位。
forEach(), filter(), reduce(), every()
和some()
都会跳过空位。
map()
会跳过空位,但会保留这个值
join()
和toString()
会将空位视为undefined,而undefined和null会被处理成空字符串。 - ES6 则是明确将空位转为undefined。
Array.from
方法会将数组的空位,转为undefined,也就是说,这个方法不会忽略空位。
对象的拓展
属性的简洁表示法
//属性
const foo = 'bar';
const baz = {foo};
//方法
const o = {
method() {
return "Hello!";
}
};
//属性的赋值器(setter)和取值器(getter),事实上也是采用这种写法。
const cart = {
_wheels: 4,
get wheels () {
return this._wheels;
},
set wheels (value) {
if (value < this._wheels) {
throw new Error('数值太小了!');
}
this._wheels = value;
}
}
- 注意,简写的对象方法不能用作构造函数,会报错。不简写可以
const obj = {
f() {
this.foo = 'bar';
}
};
new obj.f() // 报错
属性名表达式
- ES6 允许字面量定义对象时,用方法二(表达式)作为对象的属性名,即把表达式放在方括号内。
let lastWord = 'last word';
const a = {
'first word': 'hello',
[lastWord]: 'world'
};
a['first word'] // "hello"
a[lastWord] // "world"
a['last word'] // "world"
- 表达式还可以用于定义方法名。
let obj = {
['h' + 'ello']() {
return 'hi';
}
};
obj.hello() // hi
- 注意,属性名表达式与简洁表示法,不能同时使用,会报错。
- 注意,属性名表达式如果是一个对象,默认情况下会自动将对象转为字符串[object Object],这一点要特别小心。
方法的 name 属性
- 函数的name属性,返回函数名。对象方法也是函数,因此也有name属性。
const person = {
sayName() {
console.log('hello!');
},
};
person.sayName.name // "sayName"
- 如果对象的方法使用了取值函数(getter)和存值函数(setter),则name属性不是在该方法上面,而是该方法的属性的描述对象的get和set属性上面,返回值是方法名前加上get和set。
const obj = {
get foo() {},
set foo(x) {}
};
obj.foo.name
// TypeError: Cannot read property 'name' of undefined
const descriptor = Object.getOwnPropertyDescriptor(obj, 'foo');
descriptor.get.name // "get foo"
descriptor.set.name // "set foo"
- 有两种特殊情况:bind方法创造的函数,name属性返回bound加上原函数的名字;Function构造函数创造的函数,name属性返回anonymous。
- 如果对象的方法是一个 Symbol 值,那么name属性返回的是这个 Symbol 值的描述。
const key1 = Symbol('description');
const key2 = Symbol();
let obj = {
[key1]() {},
[key2]() {},
};
obj[key1].name // "[description]"
obj[key2].name // ""
属性的可枚举性和遍历
可枚举性
- 有四个操作会忽略enumerable为false的属性。
1、for...in
循环:只遍历对象自身的和继承的可枚举的属性。
2、Object.keys()
:返回对象自身的所有可枚举的属性的键名。
3、JSON.stringify()
:只串行化对象自身的可枚举的属性。
4、Object.assign()
: 忽略enumerable为false的属性,只拷贝对象自身的可枚举的属性。
只有for…in会返回继承的属性,其他三个方法都会忽略继承的属性 - 总的来说,操作中引入继承的属性会让问题复杂化,大多数时候,我们只关心对象自身的属性。所以,尽量不要用for…in循环,而用Object.keys()代替
- 另外,ES6 规定,所有 Class 的原型的方法都是不可枚举的。
属性的遍历
- ES6 一共有 5 种方法可以遍历对象的属性。
(1)for...in
for…in循环遍历对象自身的和继承的可枚举属性(不含 Symbol 属性)。
(2)Object.keys(obj)
Object.keys返回一个数组,包括对象自身的(不含继承的)所有可枚举属性(不含 Symbol 属性)的键名。
(3)Object.getOwnPropertyNames(obj)
Object.getOwnPropertyNames返回一个数组,包含对象自身的所有属性(不含 Symbol 属性,但是包括不可枚举属性)的键名。
(4)Object.getOwnPropertySymbols(obj)
Object.getOwnPropertySymbols返回一个数组,包含对象自身的所有 Symbol 属性的键名。
(5)Reflect.ownKeys(obj)
Reflect.ownKeys返回一个数组,包含对象自身的(不含继承的)所有键名,不管键名是 Symbol 或字符串,也不管是否可枚举。 - 以上的 5 种方法遍历对象的键名,都遵守同样的属性遍历的次序规则。
首先遍历所有数值键,按照数值升序排列。
其次遍历所有字符串键,按照加入时间升序排列。
最后遍历所有 Symbol 键,按照加入时间升序排列。
Reflect.ownKeys({ [Symbol()]:0, b:0, 10:0, 2:0, a:0 })
// ['2', '10', 'b', 'a', Symbol()]
super 关键字
- this关键字总是指向函数所在的当前对象,ES6 又新增了另一个类似的关键字
super
,指向当前对象的原型对象。 - 注意,
super
关键字表示原型对象时,只能用在对象的方法之中,用在其他地方都会报错。
// 报错
const obj = {
foo: super.foo
}
// 报错
const obj = {
foo: () => super.foo
}
// 报错
const obj = {
foo: function () {
return super.foo
}
}
上面三种super的用法都会报错,因为对于 JavaScript 引擎来说,这里的super都没有用在对象的方法之中。第一种写法是super用在属性里面,第二种和第三种写法是super用在一个函数里面,然后赋值给foo属性。目前,只有对象方法的简写法可以让 JavaScript 引擎确认,定义的是对象的方法。
- JavaScript 引擎内部,super.foo等同于Object.getPrototypeOf(this).foo(属性)或Object.getPrototypeOf(this).foo.call(this)(方法)。
- 下面代码中,super.foo指向原型对象proto的foo方法,但是绑定的this却还是当前对象obj,因此输出的就是world。
const proto = {
x: 'hello',
foo() {
console.log(this.x);
},
};
const obj = {
x: 'world',
foo() {
super.foo();
}
}
Object.setPrototypeOf(obj, proto);
obj.foo() // "world"
对象的扩展运算符
解构赋值
- 对象的解构赋值用于从一个对象取值,相当于将目标对象自身的所有可遍历的(enumerable)、但尚未被读取的属性,分配到指定的对象上面。
- 由于解构赋值要求等号右边是一个对象,所以如果等号右边是undefined或null,就会报错,因为它们无法转为对象。
- 解构赋值必须是最后一个参数,否则会报错。
- 注意,解构赋值的拷贝是浅拷贝,即如果一个键的值是复合类型的值(数组、对象、函数)、那么解构赋值拷贝的是这个值的引用,而不是这个值的副本。
- 另外,扩展运算符的解构赋值,不能复制继承自原型对象的属性。
- 下面代码中,变量x是单纯的解构赋值,所以可以读取对象o继承的属性;变量y和z是扩展运算符的解构赋值,只能读取对象o自身的属性
const o = Object.create({ x: 1, y: 2 });
o.z = 3;
let { x, ...newObj } = o;
let { y, z } = newObj;
x // 1
y // undefined
z // 3
ES6 规定,变量声明语句之中,如果使用解构赋值,扩展运算符后面必须是一个变量名,而不能是一个解构赋值表达式,所以上面代码引入了中间变量newObj,如果写成下面这样会报错。
let { x, ...{ y, z } } = o;
// SyntaxError:
扩展运算符
let z = { a: 3, b: 4 };
let n = { ...z };
n // { a: 3, b: 4 }
- 由于数组是特殊的对象,所以对象的扩展运算符也可以用于数组。
let foo = { ...['a', 'b', 'c'] };
foo
// {0: "a", 1: "b", 2: "c"}
- 如果扩展运算符后面是一个空对象,则没有任何效果。
{...{}, a: 1}
// { a: 1 }
- 如果扩展运算符后面不是对象,则会自动将其转为对象。
// 等同于 {...Object(1)}
{...1} // {}
上面代码中,扩展运算符后面是整数1,会自动转为数值的包装对象Number{1}。由于该对象没有自身属性,所以返回一个空对象。
下面的例子都是类似的道理。
// 等同于 {...Object(true)}
{...true} // {}
// 等同于 {...Object(undefined)}
{...undefined} // {}
// 等同于 {...Object(null)}
{...null} // {}
- 但是,如果扩展运算符后面是字符串,它会自动转成一个类似数组的对象,因此返回的不是空对象。
{...'hello'}
// {0: "h", 1: "e", 2: "l", 3: "l", 4: "o"}
- 对象的扩展运算符等同于使用Object.assign()方法。
let aClone = { ...a };
// 等同于
let aClone = Object.assign({}, a);
- 如果想完整克隆一个对象,还拷贝对象原型的属性,可以采用下面的写法。
const clone2 = Object.assign(
Object.create(Object.getPrototypeOf(obj)),
obj
);
// 写法三
const clone3 = Object.create(
Object.getPrototypeOf(obj),
Object.getOwnPropertyDescriptors(obj)
)
- 扩展运算符的参数对象之中,如果有取值函数get,这个函数是会执行的。
let a = {
get x() {
throw new Error('not throw yet');
}
}
let aWithXGetter = { ...a }; // 报错
上面例子中,取值函数get在扩展a对象时会自动执行,导致报错。
链判断运算符
const fooInput = myForm.querySelector('input[name=foo]')
- 上面例子中,必须先判断fooInput是否存在,才能读取fooInput.value。
这样的层层判断非常麻烦,因此 ES2020 引入了“链判断运算符”(optional chaining operator)?.,简化上面的写法。
const firstName = message?.body?.user?.firstName || 'default';
- 链判断运算符有三种用法。
obj?.prop // 对象属性
obj?.[expr] // 同上
func?.(...args) // 函数或对象方法的调用
- 使用这个运算符,有几个注意点。
(1)短路机制
?.
运算符相当于一种短路机制,只要不满足条件,就不再往下执行。
(2)delete 运算符
delete a?.b
// 等同于
a == null ? undefined : delete a.b
上面代码中,如果a是undefined或null,会直接返回undefined,而不会进行delete运算。
(3)括号的影响
如果属性链有圆括号,链判断运算符对圆括号外部没有影响,只对圆括号内部有影响。
(a?.b).c
// 等价于
(a == null ? undefined : a.b).c
上面代码中,?.对圆括号外部没有影响,不管a对象是否存在,圆括号后面的.c总是会执行。一般来说,使用?.运算符的场合,不应该使用圆括号。
(4)报错场合
以下写法是禁止的,会报错。
// 构造函数
new a?.()
new a?.b()
// 链判断运算符的右侧有模板字符串
a?.`{b}`
a?.b`{c}`
// 链判断运算符的左侧是 super
super?.()
super?.foo
// 链运算符用于赋值运算符左侧
a?.b = c
(5)右侧不得为十进制数值
Null 判断运算符
- 它的行为类似
||
,但是只有运算符左侧的值为null或undefined时,才会返回右侧的值。
const headerText = response.settings.headerText ?? 'Hello, world!';
??
有一个运算优先级问题,它与&&和||的优先级孰高孰低。现在的规则是,如果多个逻辑运算符一起使用,必须用括号表明优先级,否则会报错。
对象的新增方法
Object.is()
- 它用来比较两个值是否严格相等,与严格比较运算符(===)的行为基本一致。不同之处只有两个:一是+0不等于-0,二是NaN等于自身。
Object.is('foo', 'foo')
// true
Object.is({}, {})
// false
Object.assign()
基本用法
Object.assign()
方法用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target)。- 如果只有一个参数,
Object.assign()
会直接返回该参数。 - 如果该参数不是对象,则会先转成对象,然后返回。由于undefined和null无法转成对象,所以如果它们作为参数,就会报错。
- 如果非对象参数出现在源对象的位置(即非首参数),那么处理规则有所不同。首先,这些参数都会转成对象,如果无法转成对象,就会跳过。这意味着,如果undefined和null不在首参数,就不会报错。
- 其他类型的值(即数值、字符串和布尔值)不在首参数,也不会报错。但是,除了字符串会以数组形式,拷贝入目标对象,其他值都不会产生效果。
Object(true) // {[[PrimitiveValue]]: true}
Object(10) // {[[PrimitiveValue]]: 10}
Object('abc') // {0: "a", 1: "b", 2: "c", length: 3, [[PrimitiveValue]]: "abc"}
Object.assign()
拷贝的属性是有限制的,只拷贝源对象的自身属性(不拷贝继承属性),也不拷贝不可枚举的属性(enumerable: false
)。- 属性名为 Symbol 值的属性,也会被
Object.assign()
拷贝。
注意点
(1)浅拷贝
(2)同名属性的替换
(3)数组的处理
Object.assign()
可以用来处理数组,但是会把数组视为对象。
Object.assign([1, 2, 3], [4, 5])
// [4, 5, 3]
(4)取值函数的处理
Object.assign()
只能进行值的复制,如果要复制的值是一个取值函数,那么将求值后再复制。
const source = {
get foo() { return 1 }
};
const target = {};
Object.assign(target, source)
// { foo: 1 }
常见用途
(1)为对象添加属性
(2)为对象添加方法
(3)克隆对象
不过,采用这种方法克隆,只能克隆原始对象自身的值,不能克隆它继承的值。如果想要保持继承链,可以采用下面的代码。
function clone(origin) {
let originProto = Object.getPrototypeOf(origin);
return Object.assign(Object.create(originProto), origin);
}
(4)合并多个对象
(5)为属性指定默认值
Object.getOwnPropertyDescriptors()
Object.getOwnPropertyDescriptors()
方法,返回指定对象所有自身属性(非继承属性)的描述对象。- 该方法的实现非常容易。
function getOwnPropertyDescriptors(obj) {
const result = {};
for (let key of Reflect.ownKeys(obj)) {
result[key] = Object.getOwnPropertyDescriptor(obj, key);
}
return result;
}
- 该方法的引入目的,主要是为了解决
Object.assign()
无法正确拷贝get属性和set属性的问题。Object.getOwnPropertyDescriptors()
方法配合Object.defineProperties()
方法,就可以实现正确拷贝。
const source = {
set foo(value) {
console.log(value);
}
};
const target2 = {};
Object.defineProperties(target2, Object.getOwnPropertyDescriptors(source));
Object.getOwnPropertyDescriptor(target2, 'foo')
// { get: undefined,
// set: [Function: set foo],
// enumerable: true,
// configurable: true }
Object.getOwnPropertyDescriptors()
方法的另一个用处,是配合Object.create()
方法,将对象属性克隆到一个新对象。这属于浅拷贝。
const clone = Object.create(Object.getPrototypeOf(obj),
Object.getOwnPropertyDescriptors(obj));
- 另外,
Object.getOwnPropertyDescriptors()
方法可以实现一个对象继承另一个对象。
const obj = Object.create(
prot,
Object.getOwnPropertyDescriptors({
foo: 123,
})
);
Object.getOwnPropertyDescriptors()
也可以用来实现 Mixin(混入)模式。
__proto__属性,Object.setPrototypeOf(),Object.getPrototypeOf()
__proto__属性
Object.setPrototypeOf()
(写操作)、Object.getPrototypeOf()
(读操作)、Object.create()
(生成操作)- 实现上,
__proto__
调用的是Object.prototype.__proto__
,具体实现如下。
Object.defineProperty(Object.prototype, '__proto__', {
get() {
let _thisObj = Object(this);
return Object.getPrototypeOf(_thisObj);
},
set(proto) {
if (this === undefined || this === null) {
throw new TypeError();
}
if (!isObject(this)) {
return undefined;
}
if (!isObject(proto)) {
return undefined;
}
let status = Reflect.setPrototypeOf(this, proto);
if (!status) {
throw new TypeError();
}
},
});
function isObject(value) {
return Object(value) === value;
}
Object.setPrototypeOf()
Object.setPrototypeOf
方法的作用与__proto__
相同,用来设置一个对象的原型对象(prototype
),返回参数对象本身。- 如果第一个参数不是对象,会自动转为对象。但是由于返回的还是第一个参数,所以这个操作不会产生任何效果。
- 由于undefined和null无法转为对象,所以如果第一个参数是undefined或null,就会报错。
Object.getPrototypeOf()
- 如果参数不是对象,会被自动转为对象。
- 如果参数是undefined或null,它们无法转为对象,所以会报错。
Object.keys(),Object.values(),Object.entries()
Object.keys()
- 返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(
enumerable
)属性的键名。
Object.values()
Object.values
方法返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable
)属性的键值。- 返回数组的成员顺序,与本章的《属性的遍历》部分介绍的排列规则一致。
Object.values
只返回对象自身的可遍历属性。
const obj = Object.create({}, {p: {value: 42}});
Object.values(obj) // []
Object.values
会过滤属性名为 Symbol 值的属性。- 如果
Object.values
方法的参数是一个字符串,会返回各个字符组成的一个数组。 - 如果参数不是对象,
Object.values
会先将其转为对象。由于数值和布尔值的包装对象,都不会为实例添加非继承的属性。所以,Object.values
会返回空数组。
Object.entries()
- 除了返回值不一样,该方法的行为与
Object.values
基本一致。 - 如果原对象的属性名是一个 Symbol 值,该属性会被忽略。
Object.entries
方法的另一个用处是,将对象转为真正的Map结构。
const obj = { foo: 'bar', baz: 42 };
const map = new Map(Object.entries(obj));
map // Map { foo: "bar", baz: 42 }
- 自己实现
Object.entries
方法,非常简单。
// Generator函数的版本
function* entries(obj) {
for (let key of Object.keys(obj)) {
yield [key, obj[key]];
}
}
// 非Generator函数的版本
function entries(obj) {
let arr = [];
for (let key of Object.keys(obj)) {
arr.push([key, obj[key]]);
}
return arr;
}
Object.fromEntries()
Object.fromEntries()
方法是Object.entries()
的逆操作,用于将一个键值对数组转为对象。
Object.fromEntries([
['foo', 'bar'],
['baz', 42]
])
// { foo: "bar", baz: 42 }
- 该方法的主要目的,是将键值对的数据结构还原为对象,因此特别适合将 Map 结构转为对象。
const entries = new Map([
['foo', 'bar'],
['baz', 42]
]);
Object.fromEntries(entries)
// { foo: "bar", baz: 42 }
- 该方法的一个用处是配合
URLSearchParams
对象,将查询字符串转为对象。
Object.fromEntries(new URLSearchParams('foo=bar&baz=qux'))
// { foo: "bar", baz: "qux" }
Symbol
概述
- 注意,Symbol函数前不能使用new命令,否则会报错。这是因为生成的 Symbol 是一个原始类型的值,不是对象。
- 如果 Symbol 的参数是一个对象,就会调用该对象的
toString
方法,将其转为字符串,然后才生成一个 Symbol 值。 - Symbol 值不能与其他类型的值进行运算,会报错。
- 但是,Symbol 值可以显式转为字符串。另外,Symbol 值也可以转为布尔值,但是不能转为数值。
let sym = Symbol('My symbol');
String(sym) // 'Symbol(My symbol)'
sym.toString() // 'Symbol(My symbol)'
let sym = Symbol();
Boolean(sym) // true
!sym // false
if (sym) {
// ...
}
Number(sym) // TypeError
sym + 2 // TypeError
Symbol.prototype.description
- ES2019 提供了一个实例属性
description
,直接返回 Symbol 的描述。
作为属性名的 Symbol
- 注意,Symbol 值作为对象属性名时,不能用点运算符。
- 因为点运算符后面总是字符串,所以不会读取mySymbol作为标识名所指代的那个值,导致a的属性名实际上是一个字符串,而不是一个 Symbol 值。同理,在对象的内部,使用 Symbol 值定义属性时,Symbol 值必须放在方括号之中。
let s = Symbol();
let obj = {
[s]: function (arg) { ... }
};
obj[s](123);
属性名的遍历
- Symbol 作为属性名,遍历对象的时候,该属性不会出现在
for...in、for...of
循环中,也不会被Object.keys()、Object.getOwnPropertyNames()、JSON.stringify()
返回。 - 有一个
Object.getOwnPropertySymbols()
方法,可以获取指定对象的所有 Symbol 属性名。该方法返回一个数组,成员是当前对象的所有用作属性名的 Symbol 值。 Reflect.ownKeys()
方法可以返回所有类型的键名,包括常规键名和 Symbol 键名。
Symbol.for(),Symbol.keyFor()
Symbol.for()
方法接受一个字符串作为参数,然后搜索有没有以该参数作为名称的 Symbol 值。如果有,就返回这个 Symbol 值,否则就新建一个以该字符串为名称的 Symbol 值,并将其注册到全局。Symbol.for()
与Symbol()
这两种写法,都会生成新的 Symbol。它们的区别是,前者会被登记在全局环境中供搜索,后者不会。Symbol.keyFor()
方法返回一个已登记的 Symbol 类型值的key。- 注意,
Symbol.for()
为 Symbol 值登记的名字,是全局环境的,不管有没有在全局环境运行。 Symbol.for()
的这个全局登记特性,可以用在不同的 iframe 或 service worker 中取到同一个值。
内置的 Symbol 值
Symbol.hasInstance
- 对象的
Symbol.hasInstance
属性,指向一个内部方法。当其他对象使用instanceof
运算符,判断是否为该对象的实例时,会调用这个方法。比如,foo instanceof Foo
在语言内部,实际调用的是Foo[Symbol.hasInstance](foo)
。
class MyClass {
[Symbol.hasInstance](foo) {
return foo instanceof Array;
}
}
[1, 2, 3] instanceof new MyClass() // true
Symbol.isConcatSpreadable
- 对象的
Symbol.isConcatSpreadable
属性等于一个布尔值,表示该对象用于Array.prototype.concat()
时,是否可以展开。 - 数组的默认行为是可以展开,
Symbol.isConcatSpreadable
默认等于undefined。该属性等于true时,也有展开的效果。类似数组的对象正好相反,默认不展开。
let arr1 = ['c', 'd'];
['a', 'b'].concat(arr1, 'e') // ['a', 'b', 'c', 'd', 'e']
arr1[Symbol.isConcatSpreadable] // undefined
obj[Symbol.isConcatSpreadable] = true;
['a', 'b'].concat(obj, 'e') // ['a', 'b', 'c', 'd', 'e']
Symbol.isConcatSpreadable
属性也可以定义在类里面。
class A1 extends Array {
constructor(args) {
super(args);
this[Symbol.isConcatSpreadable] = true;
}
}
class A2 extends Array {
constructor(args) {
super(args);
}
get [Symbol.isConcatSpreadable] () { //必须有get,不然没有效果
return false;
}
}
let a1 = new A1();
a1[0] = 3;
a1[1] = 4;
let a2 = new A2();
a2[0] = 5;
a2[1] = 6;
[1, 2].concat(a1).concat(a2)
// [1, 2, 3, 4, [5, 6]]
- 注意,
Symbol.isConcatSpreadable
的位置差异,A1是定义在实例上,A2是定义在类本身,效果相同。
Symbol.species
- 对象的
Symbol.species
属性,指向一个构造函数。创建衍生对象时,会使用该属性。
class MyArray extends Array {
static get [Symbol.species]() { return Array; }
}
- 上面代码中,由于定义了
Symbol.species
属性,创建衍生对象时就会使用这个属性返回的函数,作为构造函数。这个例子也说明,定义Symbol.species
属性要采用get取值器。默认的Symbol.species
属性等同于下面的写法。
static get [Symbol.species]() {
return this;
}
Symbol.match
- 对象的
Symbol.match
属性,指向一个函数。当执行str.match(myObject)
时,如果该属性存在,会调用它,返回该方法的返回值。
String.prototype.match(regexp)
// 等同于
regexp[Symbol.match](this)
class MyMatcher {
[Symbol.match](string) {
return 'hello world'.indexOf(string);
}
}
'e'.match(new MyMatcher()) // 1
Symbol.replace
Symbol.search
Symbol.split
Symbol.iterator
Symbol.toPrimitive
Symbol.toStringTag
Symbol.unscopables
Set 和 Map 数据结构
Set
基本用法
- Set函数可以接受一个数组(或者具有 iterable 接口的其他数据结构)作为参数,用来初始化。
- 一种去除数组重复成员的方法。
// 去除数组的重复成员
[...new Set(array)]
- 上面的方法也可以用于,去除字符串里面的重复字符。
[...new Set('ababbc')].join('')
// "abc"
- 向 Set 加入值的时候,不会发生类型转换,所以5和"5"是两个不同的值。Set 内部判断两个值是否不同,使用的算法叫做“Same-value-zero equality”,它类似于精确相等运算符(===),主要的区别是向 Set 加入值时认为NaN等于自身,而精确相等运算符认为NaN不等于自身。
Set 实例的属性和方法
-
Set 结构的实例有以下属性。
Set.prototype.constructor:
构造函数,默认就是Set函数。
Set.prototype.size:
返回Set实例的成员总数。 -
Set 实例的方法分为两大类:操作方法(用于操作数据)和遍历方法(用于遍历成员)。
-
下面先介绍四个操作方法。
Set.prototype.add(value):
添加某个值,返回 Set 结构本身。
Set.prototype.delete(value):
删除某个值,返回一个布尔值,表示删除是否成功。
Set.prototype.has(value):
返回一个布尔值,表示该值是否为Set的成员。
Set.prototype.clear():
清除所有成员,没有返回值。 -
Array.from
方法可以将 Set 结构转为数组。
遍历操作
- Set 结构的实例有四个遍历方法,可以用于遍历成员。
Set.prototype.keys():
返回键名的遍历器
Set.prototype.values():
返回键值的遍历器
Set.prototype.entries():
返回键值对的遍历器
Set.prototype.forEach():
使用回调函数遍历每个成员 - 需要特别指出的是,Set的遍历顺序就是插入顺序。这个特性有时非常有用,比如使用 Set 保存一个回调函数列表,调用时就能保证按照添加顺序调用。
(1)keys(),values(),entries()
- keys方法、values方法、entries方法返回的都是遍历器对象(详见《Iterator 对象》一章)。由于 Set 结构没有键名,只有键值(或者说键名和键值是同一个值),所以keys方法和values方法的行为完全一致。
- Set 结构的实例默认可遍历,它的默认遍历器生成函数就是它的values方法。
Set.prototype[Symbol.iterator] === Set.prototype.values
// true
- 这意味着,可以省略values方法,直接用for…of循环遍历 Set。
let set = new Set(['red', 'green', 'blue']);
for (let x of set) {
console.log(x);
}
// red
// green
// blue
(2)forEach()
- Set 结构的实例与数组一样,也拥有forEach方法,用于对每个成员执行某种操作,没有返回值。
- 该函数的参数与数组的forEach一致,依次为键值、键名、集合本身
- 另外,forEach方法还可以有第二个参数,表示绑定处理函数内部的this对象。
(3)遍历的应用
- 扩展运算符(…)内部使用for…of循环,所以也可以用于 Set 结构。
- 数组的map和filter方法也可以间接用于 Set 。因此使用 Set 可以很容易地实现并集(Union)、交集(Intersect)和差集(Difference)。
- 如果想在遍历操作中,同步改变原来的 Set 结构,目前没有直接的方法,但有两种变通方法。一种是利用原 Set 结构映射出一个新的结构,然后赋值给原来的 Set 结构;另一种是利用Array.from方法。
// 方法一
let set = new Set([1, 2, 3]);
set = new Set([...set].map(val => val * 2));
// set的值是2, 4, 6
// 方法二
let set = new Set([1, 2, 3]);
set = new Set(Array.from(set, val => val * 2)); //!!!
// set的值是2, 4, 6
WeakSet
含义
- 它与 Set 有两个区别。
1、首先,WeakSet 的成员只能是对象,而不能是其他类型的值。
2、其次,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]}
- 数组的成员只能是对象。
const b = [3, 4];
const ws = new WeakSet(b);
// Uncaught TypeError: Invalid value used in weak set(…)
- WeakSet 结构有以下三个方法。
WeakSet.prototype.add(value):
向 WeakSet 实例添加一个新成员。
WeakSet.prototype.delete(value):
清除 WeakSet 实例的指定成员。
WeakSet.prototype.has(value):
返回一个布尔值,表示某个值是否在 WeakSet 实例之中。 - WeakSet 没有
size
属性,没有办法遍历它的成员。
Map
含义和基本用法
- 对象只接受字符串作为键名
- 作为构造函数,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"
- Map构造函数接受数组作为参数,实际上执行的是下面的算法。
const items = [
['name', '张三'],
['title', 'Author']
];
const map = new Map();
items.forEach(
([key, value]) => map.set(key, value)
);
- 不仅仅是数组,任何具有 Iterator 接口、且每个成员都是一个双元素的数组的数据结构(详见《Iterator》一章)都可以当作Map构造函数的参数。这就是说,Set和Map都可以用来生成新的 Map。
- 如果对同一个键多次赋值,后面的值将覆盖前面的值。
- 如果读取一个未知的键,则返回undefined。
- 注意,只有对同一个对象的引用,Map 结构才将其视为同一个键。这一点要非常小心。
const map = new Map();
map.set(['a'], 555);
map.get(['a']) // undefined
- 如果 Map 的键是一个简单类型的值(数字、字符串、布尔值),则只要两个值严格相等,Map 将其视为一个键,比如0和-0就是一个键(+0也是),布尔值true和字符串true则是两个不同的键。另外,undefined和null也是两个不同的键。虽然NaN不严格相等于自身,但 Map 将其视为同一个键。
实例的属性和操作方法
(1)size
属性
(2)Map.prototype.set(key, value)
set方法返回的是当前的Map对象,因此可以采用链式写法。
(3)Map.prototype.get(key)
get方法读取key对应的键值,如果找不到key,返回undefined。
(4)Map.prototype.has(key)
(5)Map.prototype.delete(key)
delete方法删除某个键,返回true。如果删除失败,返回false。
(6)Map.prototype.clear()
clear方法清除所有成员,没有返回值。
遍历方法
- Map 结构原生提供三个遍历器生成函数和一个遍历方法。
Map.prototype.keys():
返回键名的遍历器。
Map.prototype.values():
返回键值的遍历器。
Map.prototype.entries():
返回所有成员的遍历器。
Map.prototype.forEach():
遍历 Map 的所有成员。 - 需要特别注意的是,Map 的遍历顺序就是插入顺序。
// 等同于使用map.entries()
for (let [key, value] of map) {
console.log(key, value);
}
// "F" "no"
// "T" "yes"
- 表示 Map 结构的默认遍历器接口(
Symbol.iterator
属性),就是entries
方法。
map[Symbol.iterator] === map.entries
// true
- Map 结构转为数组结构,比较快速的方法是使用扩展运算符(…)。
[...map.keys()]
// [1, 2, 3]
[...map.values()]
// ['one', 'two', 'three']
[...map.entries()]
// [[1,'one'], [2, 'two'], [3, 'three']]
[...map]
// [[1,'one'], [2, 'two'], [3, 'three']]
- 结合数组的map方法、filter方法,可以实现 Map 的遍历和过滤(Map 本身没有map和filter方法)。
const map1 = new Map(
[...map0].filter(([k, v]) => k < 3)
);
- 此外,Map 还有一个
forEach
方法,与数组的forEach
方法类似,也可以实现遍历。
map.forEach(function(value, key, map) {
console.log("Key: %s, Value: %s", key, value);
});
forEach
方法还可以接受第二个参数,用来绑定this
与其他数据结构的互相转换
(1)Map 转为数组
前面已经提过,Map 转为数组最方便的方法,就是使用扩展运算符(...
)。
(2)数组 转为 Map
将数组传入 Map 构造函数,就可以转为 Map。
(3)Map 转为对象
如果所有 Map 的键都是字符串,它可以无损地转为对象。
如果有非字符串的键名,那么这个键名会被转成字符串,再作为对象的键名。
function strMapToObj(strMap) {
let obj = Object.create(null);
for (let [k,v] of strMap) {
obj[k] = v;
}
return obj;
}
const myMap = new Map()
.set('yes', true)
.set('no', false);
strMapToObj(myMap)
// { yes: true, no: false }
如果有非字符串的键名,那么这个键名会被转成字符串,再作为对象的键名。
(4)对象转为 Map
对象转为 Map 可以通过Object.entries()。
let obj = {"a":1, "b":2};
let map = new Map(Object.entries(obj));
(5)Map 转为 JSON
- Map 转为 JSON 要区分两种情况。一种情况是,Map 的键名都是字符串,这时可以选择转为对象 JSON。
function strMapToJson(strMap) {
return JSON.stringify(strMapToObj(strMap));
}
let myMap = new Map().set('yes', true).set('no', false);
strMapToJson(myMap)
// '{"yes":true,"no":false}'
- 另一种情况是,Map 的键名有非字符串,这时可以选择转为数组 JSON。
function mapToArrayJson(map) {
return JSON.stringify([...map]);
}
let myMap = new Map().set(true, 7).set({foo: 3}, ['abc']);
mapToArrayJson(myMap)
// '[[true,7],[{"foo":3},["abc"]]]'
(6)JSON 转为 Map
JSON 转为 Map,正常情况下,所有键名都是字符串。
function jsonToStrMap(jsonStr) {
return objToStrMap(JSON.parse(jsonStr)); //先转成对象,再转换为Map
}
jsonToStrMap('{"yes": true, "no": false}')
// Map {'yes' => true, 'no' => false}
但是,有一种特殊情况,整个 JSON 就是一个数组,且每个数组成员本身,又是一个有两个成员的数组。这时,它可以一一对应地转为 Map。这往往是 Map 转为数组 JSON 的逆操作。
function jsonToMap(jsonStr) {
return new Map(JSON.parse(jsonStr)); //如果是数组就直接new Map()
}
jsonToMap('[[true,7],[{"foo":3},["abc"]]]')
// Map {true => 7, Object {foo: 3} => ['abc']}
WeakMap
含义
- WeakMap与Map的区别有两点。
1、首先,WeakMap只接受对象作为键名(null除外),不接受其他类型的值作为键名。
2、其次,WeakMap的键名所指向的对象,不计入垃圾回收机制。 - WeakMap的设计目的在于,有时我们想在某个对象上面存放一些数据,但是这会形成对于这个对象的引用。
- 基本上,如果你要往对象上添加数据,又不想干扰垃圾回收机制,就可以使用 WeakMap。一个典型应用场景是,在网页的 DOM 元素上添加数据,就可以使用WeakMap结构。当该 DOM 元素被清除,其所对应的WeakMap记录就会自动被移除。
注意,WeakMap 弱引用的只是键名,而不是键值。键值依然是正常引用。
const wm = new WeakMap();
let key = {};
let obj = {foo: 1};
wm.set(key, obj);
obj = null;
wm.get(key)
// Object {foo: 1}
上面代码中,键值obj是正常引用。所以,即使在 WeakMap 外部消除了obj的引用,WeakMap 内部的引用依然存在。
WeakMap 的语法
- WeakMap 与 Map 在 API 上的区别主要是两个,一是没有遍历操作(即没有
keys()、values()和entries()
方法),也没有size
属性。 - 二是无法清空,即不支持
clear
方法。因此,WeakMap只有四个方法可用:get()、set()、has()、delete()。
WeakMap 的用途
- WeakMap 应用的典型场合就是 DOM 节点作为键名。
- WeakMap 的另一个用处是部署私有属性。
Proxy(建议先看Reflect)
概述
- ES6 原生提供 Proxy 构造函数,用来生成 Proxy 实例。
var proxy = new Proxy(target, handler);
- Proxy 对象的所有用法,都是上面这种形式,不同的只是handler参数的写法。其中,
new Proxy()
表示生成一个Proxy实例,target参数表示所要拦截的目标对象,handler参数也是一个对象,用来定制拦截行为。如果handler没有设置任何拦截,那就等同于直接通向原对象。
var target = {};
var handler = {};
var proxy = new Proxy(target, handler);
proxy.a = 'b';
target.a // "b"
- 一个技巧是将 Proxy 对象,设置到
object.proxy
属性,从而可以在object对象上调用。
var object = { proxy: new Proxy(target, handler) };
- Proxy 实例也可以作为其他对象的原型对象。
var proxy = new Proxy({}, {
get: function(target, propKey) {
return 35;
}
});
let obj = Object.create(proxy);
obj.time // 35
上面代码中,proxy对象是obj对象的原型,obj对象本身并没有time属性,所以根据原型链,会在proxy对象上读取该属性,导致被拦截。
var handler = {
get: function(target, name) {
if (name === 'prototype') {
return Object.prototype;
}
return 'Hello, ' + name;
},
apply: function(target, thisBinding, args) {
return args[0];
},
construct: function(target, args) {
return {value: args[1]};
}
};
var fproxy = new Proxy(function(x, y) {
return x + y;
}, handler);
fproxy(1, 2) // 1
new fproxy(1, 2) // {value: 2}
fproxy.prototype === Object.prototype // true
fproxy.foo === "Hello, foo" // true
- 下面是 Proxy 支持的拦截操作一览,一共 13 种。
get(target, propKey, receiver):
拦截对象属性的读取,比如proxy.foo和proxy['foo']
。
set(target, propKey, value, receiver):
拦截对象属性的设置,比如proxy.foo = v
或proxy['foo'] = v
,返回一个布尔值。
has(target, propKey):
拦截propKey in proxy
的操作,返回一个布尔值。
deleteProperty(target, propKey):
拦截delete proxy[propKey]
的操作,返回一个布尔值。
ownKeys(target):
拦截Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy)、for...in
循环,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而Object.keys()
的返回结果仅包括目标对象自身的可遍历属性。
getOwnPropertyDescriptor(target, propKey):
拦截Object.getOwnPropertyDescriptor(proxy, propKey)
,返回属性的描述对象。
defineProperty(target, propKey, propDesc):
拦截Object.defineProperty(proxy, propKey, propDesc)、Object.defineProperties(proxy, propDescs)
,返回一个布尔值。
preventExtensions(target):
拦截Object.preventExtensions(proxy)
,返回一个布尔值。
getPrototypeOf(target):
拦截Object.getPrototypeOf(proxy)
,返回一个对象。
isExtensible(target):
拦截Object.isExtensible(proxy)
,返回一个布尔值。
setPrototypeOf(target, proto):
拦截Object.setPrototypeOf(proxy, proto),
返回一个布尔值。 - 如果目标对象是函数,那么还有两种额外操作可以拦截。
apply(target, object, args):
拦截 Proxy 实例作为函数调用的操作,比如proxy(...args)、proxy.call(object, ...args)、proxy.apply(...)。
construct(target, args):
拦截 Proxy 实例作为构造函数调用的操作,比如new proxy(...args)
Proxy 实例的方法
get()
- get方法可以继承。
let proto = new Proxy({}, {
get(target, propertyKey, receiver) {
console.log('GET ' + propertyKey);
return target[propertyKey];
}
});
let obj = Object.create(proto);
obj.foo // "GET foo"
- 利用 Proxy,可以将读取属性的操作(get),转变为执行某个函数,从而实现属性的链式操作。
var pipe = function (value) {
var funcStack = [];
var oproxy = new Proxy({} , {
get : function (pipeObject, fnName) {
if (fnName === 'get') {
return funcStack.reduce(function (val, fn) {
return fn(val);
},value);
}
funcStack.push(window[fnName]);
return oproxy;
}
});
return oproxy;
}
var double = n => n * 2;
var pow = n => n * n;
var reverseInt = n => n.toString().split("").reverse().join("") | 0;
pipe(3).double.pow.reverseInt.get; // 63
- 下面是一个get方法的第三个参数的例子,它总是指向原始的读操作所在的那个对象,一般情况下就是 Proxy 实例。
const proxy = new Proxy({}, {
get: function(target, key, receiver) {
return receiver;
}
});
proxy.getReceiver === proxy // true
上面代码中,proxy对象的getReceiver
属性是由proxy对象提供的,所以receiver指向proxy对象。
const proxy = new Proxy({}, {
get: function(target, key, receiver) {
return receiver;
}
});
const d = Object.create(proxy);
d.a === d // true
上面代码中,d对象本身没有a属性,所以读取d.a的时候,会去d的原型proxy对象找。这时,receiver就指向d,代表原始的读操作所在的那个对象。
- 如果一个属性不可配置(
configurable
)且不可写(writable
),则 Proxy 不能修改该属性,否则通过 Proxy 对象访问该属性会报错。
const target = Object.defineProperties({}, {
foo: {
value: 123,
writable: false,
configurable: false
},
});
const handler = {
get(target, propKey) {
return 'abc'; //这里如果是console.log,则会输出再报错
}
};
const proxy = new Proxy(target, handler);
proxy.foo
// TypeError: Invariant check failed
set()
- set方法用来拦截某个属性的赋值操作,可以接受四个参数,依次为目标对象、属性名、属性值和 Proxy 实例本身,其中最后一个参数可选。
- set方法的第四个参数receiver,指的是原始的操作行为所在的那个对象,一般情况下是proxy实例本身
const handler = {
set: function(obj, prop, value, receiver) {
obj[prop] = receiver;
}
};
const proxy = new Proxy({}, handler);
const myObj = {};
Object.setPrototypeOf(myObj, proxy);
myObj.foo = 'bar';
myObj.foo === myObj // true
上面代码中,设置myObj.foo
属性的值时,myObj并没有foo属性,因此引擎会到myObj的原型链去找foo属性。myObj的原型对象proxy是一个 Proxy 实例,设置它的foo属性会触发set方法。这时,第四个参数receiver就指向原始赋值行为所在的对象myObj。
- 注意,如果目标对象自身的某个属性,不可写且不可配置,那么set方法将不起作用。
const obj = {};
Object.defineProperty(obj, 'foo', {
value: 'bar',
writable: false,
});
const handler = {
set: function(obj, prop, value, receiver) {
obj[prop] = 'baz';
}
};
const proxy = new Proxy(obj, handler);
proxy.foo = 'baz';
proxy.foo // "bar"
- 注意,严格模式下,set代理如果没有返回true,就会报错。严格模式下,set代理返回false或者undefined,都会报错。
apply()
- apply方法拦截函数的调用、call和apply操作。
- apply方法可以接受三个参数,分别是目标对象、目标对象的上下文对象(this)和目标对象的参数数组。
var twice = {
apply (target, ctx, args) {
return Reflect.apply(...arguments) * 2;
}
};
function sum (left, right) {
return left + right;
};
var proxy = new Proxy(sum, twice);
proxy(1, 2) // 6
proxy.call(null, 5, 6) // 22
proxy.apply(null, [7, 8]) // 30
上面代码中,每当执行proxy函数(直接调用或call和apply调用),就会被apply方法拦截。
- 另外,直接调用Reflect.apply方法,也会被拦截。(上面的不会)
Reflect.apply(proxy, null, [9, 10]) // 38
has()
- has()方法用来拦截
HasProperty
操作,即判断对象是否具有某个属性时,这个方法会生效。典型的操作就是in
运算符。 has()
方法可以接受两个参数,分别是目标对象、需查询的属性名。- 如果原对象不可配置或者禁止扩展,这时
has()
拦截会报错。
var obj = { a: 10 };
Object.preventExtensions(obj);
var p = new Proxy(obj, {
has: function(target, prop) {
return false;
}
});
'a' in p // TypeError is thrown
- 值得注意的是,
has()
方法拦截的是HasProperty
操作,而不是HasOwnProperty
操作,即has()
方法不判断一个属性是对象自身的属性,还是继承的属性。 - 另外,虽然
for...in
循环也用到了in
运算符,但是has()
拦截对for...in
循环不生效
construct()
- construct()方法用于拦截new命令,下面是拦截对象的写法。
const handler = {
construct (target, args, newTarget) {
return new target(...args);
}
};
- construct()方法可以接受三个参数。
target:
目标对象。
args:
构造函数的参数数组。
newTarget:
创造实例对象时,new命令作用的构造函数(下面例子的p)。
const p = new Proxy(function () {}, {
construct: function(target, args) {
console.log('called: ' + args.join(', '));
return { value: args[0] * 10 };
}
});
(new p(1)).value
// "called: 1"
// 10
construct()
方法返回的必须是一个对象,否则会报错。- 另外,由于
construct()
拦截的是构造函数,所以它的目标对象必须是函数,否则就会报错。 - 注意,
construct()
方法中的this
指向的是handler
,而不是实例对象。
const handler = {
construct: function(target, args) {
console.log(this === handler);
return new target(...args);
}
}
let p = new Proxy(function () {}, handler);
new p() // true
deleteProperty()
deleteProperty
方法用于拦截delete
操作,如果这个方法抛出错误或者返回false
,当前属性就无法被delete
命令删除。- 注意,目标对象自身的不可配置(
configurable
)的属性,不能被deleteProperty
方法删除,否则报错。
defineProperty()
defineProperty()
方法拦截了Object.defineProperty()
操作。
var handler = {
defineProperty (target, key, descriptor) {
return false;
}
};
var target = {};
var proxy = new Proxy(target, handler);
proxy.foo = 'bar' // 不会生效
- 上面代码中,
defineProperty()
方法内部没有任何操作,只返回false,导致添加新属性总是无效。注意,这里的false只是用来提示操作失败,本身并不能阻止添加新属性。 - 注意,如果目标对象不可扩展(
non-extensible
),则defineProperty()
不能增加目标对象上不存在的属性,否则会报错。 - 另外,如果目标对象的某个属性不可写(
writable
)或不可配置(configurable
),则defineProperty()
方法不得改变这两个设置。
getOwnPropertyDescriptor()
getOwnPropertyDescriptor()
方法拦截Object.getOwnPropertyDescriptor()
,返回一个属性描述对象或者undefined。
getPrototypeOf()
getPrototypeOf()
方法主要用来拦截获取对象原型。具体来说,拦截下面这些操作。
Object.prototype.__proto__
Object.prototype.isPrototypeOf()
Object.getPrototypeOf()
Reflect.getPrototypeOf()
instanceof
- 注意,
getPrototypeOf()
方法的返回值必须是对象或者null,否则报错。另外,如果目标对象不可扩展(non-extensible
),getPrototypeOf()
方法必须返回目标对象的原型对象。
isExtensible()
- 注意,该方法只能返回布尔值,否则返回值会被自动转为布尔值。
- 这个方法有一个强限制,它的返回值必须与目标对象的
isExtensible
属性保持一致,否则就会抛出错误。
ownKeys()
ownKeys()
方法用来拦截对象自身属性的读取操作。具体来说,拦截以下操作。
Object.getOwnPropertyNames()
Object.getOwnPropertySymbols()
Object.keys()
for...in循环
- 注意,使用
Object.keys()
方法时,有三类属性会被ownKeys()
方法自动过滤,不会返回。
1、目标对象上不存在的属性
2、属性名为 Symbol 值
3、不可遍历(enumerable)的属性
let target = {
a: 1,
b: 2,
c: 3,
[Symbol.for('secret')]: '4',
};
Object.defineProperty(target, 'key', {
enumerable: false,
configurable: true,
writable: true,
value: 'static'
});
let handler = {
ownKeys(target) {
return ['a', 'd', Symbol.for('secret'), 'key'];
}
};
let proxy = new Proxy(target, handler);
Object.keys(proxy)
// ['a']
- ownKeys()方法还可以拦截Object.getOwnPropertyNames()
- for…in循环也受到ownKeys()方法的拦截。
const obj = { hello: 'world' };
const proxy = new Proxy(obj, {
ownKeys: function () {
return ['a', 'b'];
}
});
for (let key in proxy) {
console.log(key); // 没有任何输出
}
上面代码中,ownkeys()
指定只返回a和b属性,由于obj没有这两个属性,因此for...in
循环不会有任何输出。
ownKeys()
方法返回的数组成员,只能是字符串或Symbol
值。如果有其他类型的值,或者返回的根本不是数组,就会报错。
var obj = {};
var p = new Proxy(obj, {
ownKeys: function(target) {
return [123, true, undefined, null, {}, []];
}
});
Object.getOwnPropertyNames(p)
// Uncaught TypeError: 123 is not a valid property name
- 如果目标对象自身包含不可配置的属性,则该属性必须被
ownKeys()
方法返回,否则报错。 - 另外,如果目标对象是不可扩展的(
non-extensible
),这时ownKeys()
方法返回的数组之中,必须包含原对象的所有属性,且不能包含多余的属性,否则报错。
preventExtensions()
preventExtensions()
方法拦截Object.preventExtensions()
。该方法必须返回一个布尔值,否则会被自动转为布尔值。- 这个方法有一个限制,只有目标对象不可扩展时(即
Object.isExtensible(proxy)
为false
),proxy.preventExtensions
才能返回true
,否则会报错。
为了防止出现这个问题,通常要在proxy.preventExtensions()
方法里面,调用一次Object.preventExtensions()。
var proxy = new Proxy({}, {
preventExtensions: function(target) {
console.log('called');
Object.preventExtensions(target);
return true;
}
});
Object.preventExtensions(proxy)
// "called"
// Proxy {}
setPrototypeOf()
setPrototypeOf()
方法主要用来拦截Object.setPrototypeOf()
方法。- 注意,该方法只能返回布尔值,否则会被自动转为布尔值。另外,如果目标对象不可扩展(
non-extensible
),setPrototypeOf()
方法不得改变目标对象的原型。
Proxy.revocable()
Proxy.revocable()
方法返回一个可取消的 Proxy 实例。
let target = {};
let handler = {};
let {proxy, revoke} = Proxy.revocable(target, handler);
proxy.foo = 123;
proxy.foo // 123
revoke();
proxy.foo // TypeError: Revoked
Proxy.revocable()
方法返回一个对象,该对象的proxy属性是Proxy实例,revoke属性是一个函数,可以取消Proxy实例。Proxy.revocable()
的一个使用场景是,目标对象不允许直接访问,必须通过代理访问,一旦访问结束,就收回代理权,不允许再次访问。
this 问题
- 正常情况下,Proxy代理的钩子函数中的this指向的是Proxy代理实例(construct钩子函数除外,该钩子函数中this指向的是handler)
const target = {
m: function () {
console.log(this === proxy);
}
};
const handler = {};
const proxy = new Proxy(target, handler);
target.m() // false
proxy.m() // true
- 下面是一个例子,由于this指向的变化,导致 Proxy 无法代理目标对象。
const _name = new WeakMap();
class Person {
constructor(name) {
_name.set(this, name);
}
get name() {
return _name.get(this);
}
}
const jane = new Person('Jane');
jane.name // 'Jane'
const proxy = new Proxy(jane, {});
proxy.name // undefined
上面代码中,目标对象jane的name属性,实际保存在外部WeakMap对象_name上面,通过this键区分。由于通过proxy.name
访问时,this指向proxy,导致无法取到值,所以返回undefined。
- 此外,有些原生对象的内部属性,只有通过正确的this才能拿到,所以 Proxy 也无法代理这些原生对象的属性。
const target = new Date();
const handler = {};
const proxy = new Proxy(target, handler);
proxy.getDate();
// TypeError: this is not a Date object.
上面代码中,getDate()
方法只能在Date对象实例上面拿到,如果this不是Date对象实例就会报错。这时,this绑定原始对象,就可以解决这个问题。
const target = new Date('2015-01-01');
const handler = {
get(target, prop) {
if (prop === 'getDate') {
return target.getDate.bind(target); //必须要bind,否则内部this是Proxy
}
return Reflect.get(target, prop);
}
};
const proxy = new Proxy(target, handler);
proxy.getDate() // 1
Reflect
概述
- Reflect对象的设计目的有这样几个。
(1) 将Object对象的一些明显属于语言内部的方法(比如Object.defineProperty
),放到Reflect对象上。现阶段,某些方法同时在Object和Reflect对象上部署,未来的新方法将只部署在Reflect对象上。也就是说,从Reflect对象上可以拿到语言内部的方法。
(2) 修改某些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
}
(3) 让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
(4)Reflect对象的方法与Proxy对象的方法一一对应,只要是Proxy对象的方法,就能在Reflect对象上找到对应的方法。这就让Proxy对象可以方便地调用对应的Reflect方法,完成默认行为,作为修改行为的基础。也就是说,不管Proxy怎么修改默认行为,你总可以在Reflect上获取默认行为。
Proxy(target, {
set: function(target, name, value, receiver) {
var success = Reflect.set(target, name, value, receiver);
if (success) {
console.log('property ' + name + ' on ' + target + ' set to ' + value);
}
return success;
}
});
上面代码中,Proxy方法拦截target对象的属性赋值行为。它采用Reflect.set
方法将值赋值给对象的属性,确保完成原有的行为,然后再部署额外的功能。
下面是另一个例子。
var loggedObj = new Proxy(obj, {
get(target, name) {
console.log('get', target, name);
return Reflect.get(target, name);
},
deleteProperty(target, name) {
console.log('delete' + name);
return Reflect.deleteProperty(target, name);
},
has(target, name) {
console.log('has' + name);
return Reflect.has(target, name);
}
});
上面代码中,每一个Proxy对象的拦截操作(get、delete、has
),内部都调用对应的Reflect方法,保证原生行为能够正常执行。添加的工作,就是将每一个操作输出一行日志。
- 有了Reflect对象以后,很多操作会更易读。
// 老写法
Function.prototype.apply.call(Math.floor, undefined, [1.75]) // 1
// 新写法
Reflect.apply(Math.floor, undefined, [1.75]) // 1
静态方法
- Reflect对象一共有 13 个静态方法。
Reflect.apply(target, thisArg, args)
Reflect.construct(target, args)
Reflect.get(target, name, receiver)
Reflect.set(target, name, value, receiver)
Reflect.defineProperty(target, name, desc)
Reflect.deleteProperty(target, name)
Reflect.has(target, name)
Reflect.ownKeys(target)
Reflect.isExtensible(target)
Reflect.preventExtensions(target)
Reflect.getOwnPropertyDescriptor(target, name)
Reflect.getPrototypeOf(target)
Reflect.setPrototypeOf(target, prototype)
Reflect.get(target, name, receiver)
Reflect.get
方法查找并返回target对象的name属性,如果没有该属性,则返回undefined
。- 如果name属性部署了读取函数(getter),则读取函数的this绑定receiver。
- 如果第一个参数不是对象,
Reflect.get
方法会报错。
Reflect.set(target, name, value, receiver)
Reflect.set
方法设置target对象的name属性等于value。- 如果name属性设置了赋值函数,则赋值函数的this绑定receiver。
- 注意,如果 Proxy对象和 Reflect对象联合使用,前者拦截赋值操作,后者完成赋值的默认行为,而且传入了receiver,那么
Reflect.set
会触发Proxy.defineProperty
拦截。
let p = {
a: 'a'
};
let handler = {
set(target, key, value, receiver) {
console.log('set');
Reflect.set(target, key, value, receiver)
},
defineProperty(target, key, attribute) {
console.log('defineProperty');
Reflect.defineProperty(target, key, attribute);
}
};
let obj = new Proxy(p, handler);
obj.a = 'A';
// set
// defineProperty
上面代码中,Proxy.set
拦截里面使用了Reflect.set
,而且传入了receiver
,导致触发Proxy.defineProperty
拦截。这是因为Proxy.set
的receiver
参数总是指向当前的 Proxy实例(即上例的obj),而Reflect.set
一旦传入receiver,就会将属性赋值到receiver上面(即obj),导致触发defineProperty
拦截。如果Reflect.set
没有传入receiver,那么就不会触发defineProperty
拦截。
- 如果第一个参数不是对象,
Reflect.set
会报错。
Reflect.has(obj, name)
Reflect.has
方法对应name in obj
里面的in运算符。- 如果
Reflect.has()
方法的第一个参数不是对象,会报错。
Reflect.deleteProperty(obj, name)
Reflect.deleteProperty
方法等同于delete obj[name]
,用于删除对象的属性。- 该方法返回一个布尔值。如果删除成功,或者被删除的属性不存在,返回true;删除失败,被删除的属性依然存在,返回false。
- 如果
Reflect.deleteProperty()
方法的第一个参数不是对象,会报错。
Reflect.construct(target, args)
Reflect.construct
方法等同于new target(...args)
,这提供了一种不使用new,来调用构造函数的方法。
function Greeting(name) {
this.name = name;
}
// new 的写法
const instance = new Greeting('张三');
// Reflect.construct 的写法
const instance = Reflect.construct(Greeting, ['张三']);
- 如果
Reflect.construct()
方法的第一个参数不是函数,会报错。
Reflect.getPrototypeOf(obj)
Reflect.getPrototypeOf
方法用于读取对象的__proto__
属性,对应Object.getPrototypeOf(obj)。
Reflect.getPrototypeOf
和Object.getPrototypeOf
的一个区别是,如果参数不是对象,Object.getPrototypeOf
会将这个参数转为对象,然后再运行,而Reflect.getPrototypeOf
会报错。
Reflect.setPrototypeOf(obj, newProto)
Reflect.setPrototypeOf
方法用于设置目标对象的原型(prototype
),对应Object.setPrototypeOf(obj, newProto)
方法。它返回一个布尔值,表示是否设置成功。- 如果无法设置目标对象的原型(比如,目标对象禁止扩展),
Reflect.setPrototypeOf
方法返回false
。
Reflect.setPrototypeOf({}, null)
// true
Reflect.setPrototypeOf(Object.freeze({}), null)
// false
如果第一个参数不是对象,Object.setPrototypeOf
会返回第一个参数本身,而Reflect.setPrototypeOf
会报错。
- 如果第一个参数是undefined或null,
Object.setPrototypeOf
和Reflect.setPrototypeOf
都会报错。
Reflect.apply(func, thisArg, args)
Reflect.apply
方法等同于Function.prototype.apply.call(func, thisArg, args)
,用于绑定this对象后执行给定函数。
const ages = [11, 33, 12, 54, 18, 96];
// 旧写法
const youngest = Math.min.apply(Math, ages);
const oldest = Math.max.apply(Math, ages);
const type = Object.prototype.toString.call(youngest);
// 新写法
const youngest = Reflect.apply(Math.min, Math, ages);
const oldest = Reflect.apply(Math.max, Math, ages);
const type = Reflect.apply(Object.prototype.toString, youngest, []);
Reflect.defineProperty(target, propertyKey, attributes)
Reflect.defineProperty
方法基本等同于Object.defineProperty
,用来为对象定义属性。未来,后者会被逐渐废除,请从现在开始就使用Reflect.defineProperty
代替它。
这个方法可以与Proxy.defineProperty
配合使用。
const p = new Proxy({}, {
defineProperty(target, prop, descriptor) {
console.log(descriptor);
return Reflect.defineProperty(target, prop, descriptor);
}
});
p.foo = 'bar';
// {value: "bar", writable: true, enumerable: true, configurable: true}
p.foo // "bar"
//PS:如果Proxy里又set,那么除非Reflect传入reciever,否则不会触发defineProperty
Reflect.getOwnPropertyDescriptor(target, propertyKey)
Reflect.getOwnPropertyDescriptor
和Object.getOwnPropertyDescriptor
的一个区别是,如果第一个参数不是对象,Object.getOwnPropertyDescriptor(1, 'foo')
不报错,返回undefined,而Reflect.getOwnPropertyDescriptor(1, 'foo')
会抛出错误,表示参数非法。
Reflect.isExtensible (target)
Reflect.isExtensible
方法对应Object.isExtensible
,返回一个布尔值,表示当前对象是否可扩展。- 如果参数不是对象,
Object.isExtensible
会返回false,因为非对象本来就是不可扩展的,而Reflect.isExtensible
会报错。
Reflect.preventExtensions(target)
Reflect.preventExtensions
对应Object.preventExtensions
方法,用于让一个对象变为不可扩展。它返回一个布尔值,表示是否操作成功。- 如果参数不是对象,
Object.preventExtensions
在 ES5 环境报错,在 ES6 环境返回传入的参数,而Reflect.preventExtensions
会报错。
Reflect.ownKeys (target)
Reflect.ownKeys
方法用于返回对象的所有属性,基本等同于Object.getOwnPropertyNames
与Object.getOwnPropertySymbols
之和。- 如果
Reflect.ownKeys()
方法的第一个参数不是对象,会报错。
简单观察者实例
const queuedObservers = new Set();
const observe = fn => queuedObservers.add(fn);
const observable = obj => new Proxy(obj, {set});
function set(target, key, value, receiver) {
const result = Reflect.set(target, key, value, receiver);
queuedObservers.forEach(observer => observer());
return result;
}
Promise
Promise 的含义
- Promise对象有以下两个特点。
(1)对象的状态不受外界影响。Promise对象代表一个异步操作,有三种状态:pending
(进行中)、fulfilled
(已成功)和rejected
(已失败)。
(2)一旦状态改变,就不会再变,任何时候都可以得到这个结果。 - Promise也有一些缺点。
1、无法取消Promise,一旦新建它就会立即执行,无法中途取消。
2、如果不设置回调函数,Promise内部抛出的错误,不会反应到外部。
3、当处于pending
状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。
如果某些事件不断地反复发生,一般来说,使用 Stream 模式是比部署Promise更好的选择。
基本用法
- Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolve和reject。
- Promise实例生成以后,可以用then方法分别指定resolved状态和rejected状态的回调函数。
- Promise 新建后就会立即执行。
let promise = new Promise(function(resolve, reject) {
console.log('Promise');
resolve();
});
promise.then(function() {
console.log('resolved.');
});
console.log('Hi!');
// Promise
// Hi!
// resolved
- reject函数的参数通常是Error对象的实例,表示抛出的错误;resolve函数的参数除了正常的值以外,还可能是另一个 Promise 实例,比如像下面这样。
const p1 = new Promise(function (resolve, reject) {
// ...
});
const p2 = new Promise(function (resolve, reject) {
// ...
resolve(p1);
})
注意,这时p1的状态就会传递给p2,也就是说,p1的状态决定了p2的状态。如果p1的状态是pending
,那么p2的回调函数就会等待p1的状态改变;如果p1的状态已经是resolved或者rejected
,那么p2的回调函数将会立刻执行。
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.log(error))
// Error: fail
- 上面代码中,p1是一个 Promise,3 秒之后变为
rejected
。p2的状态在 1 秒之后改变,resolve
方法返回的是p1。由于p2返回的是另一个 Promise,导致p2自己的状态无效了,由p1的状态决定p2的状态。所以,后面的then
语句都变成针对后者(p1)。又过了 2 秒,p1变为rejected
,导致触发catch
方法指定的回调函数。最后p1,p2状态都是reject
- 如果p2是
reject
,p1是resolve
,会直接进入回调再等p1 - 注意,调用
resolve
或reject
并不会终结 Promise 的参数函数的执行。
new Promise((resolve, reject) => {
resolve(1);
console.log(2);
}).then(r => {
console.log(r);
});
// 2
// 1
上面代码中,调用resolve(1)
以后,后面的console.log(2)
还是会执行,并且会首先打印出来。这是因为立即 resolved
的 Promise 是在本轮事件循环的末尾执行,总是晚于本轮循环的同步任务。
- 一般来说,调用
resolve
或reject
以后,Promise 的使命就完成了,后继操作应该放到then
方法里面,而不应该直接写在resolve
或reject
的后面。所以,最好在它们前面加上return
语句,这样就不会有意外。
new Promise((resolve, reject) => {
return resolve(1);
// 后面的语句不会执行
console.log(2);
})
Promise.prototype.then()
- Promise 实例具有
then
方法,也就是说,then方法是定义在原型对象Promise.prototype
上的。 then
方法返回的是一个新的Promise实例(注意,不是原来那个Promise实例)。因此可以采用链式写法,即then
方法后面再调用另一个then
方法。
Promise.prototype.catch()
Promise.prototype.catch()
方法是.then(null, rejection)
或.then(undefined, rejection)
的别名,用于指定发生错误时的回调函数。- 另外,
then()
方法指定的回调函数,如果运行中抛出错误,也会被catch()
方法捕获。
const promise = new Promise(function(resolve, reject) {
throw new Error('test');
});
promise.catch(function(error) {
console.log(error);
});
// Error: test
上面代码中,promise抛出一个错误,就被catch()
方法指定的回调函数捕获。注意,上面的写法与下面两种写法是等价的。
// 写法一
const promise = new Promise(function(resolve, reject) {
try {
throw new Error('test');
} catch(e) {
reject(e);
}
});
promise.catch(function(error) {
console.log(error);
});
// 写法二
const promise = new Promise(function(resolve, reject) {
reject(new Error('test'));
});
promise.catch(function(error) {
console.log(error);
});
比较上面两种写法,可以发现reject()
方法的作用,等同于抛出错误。
如果 Promise 状态已经变成resolved
,再抛出错误是无效的。
const promise = new Promise(function(resolve, reject) {
resolve('ok');
throw new Error('test');
});
promise
.then(function(value) { console.log(value) })
.catch(function(error) { console.log(error) });
// ok
上面代码中,Promise 在resolve
语句后面,再抛出错误,不会被捕获,等于没有抛出。因为 Promise 的状态一旦改变,就永久保持该状态,不会再变了。
- Promise 对象的错误具有“冒泡”性质,会一直向后传递,直到被捕获为止。也就是说,错误总是会被下一个
catch
语句捕获。
getJSON('/post/1.json').then(function(post) {
return getJSON(post.commentURL);
}).then(function(comments) {
// some code
}).catch(function(error) {
// 处理前面三个Promise产生的错误
});
上面代码中,一共有三个 Promise 对象:一个由getJSON()
产生,两个由then()
产生。它们之中任何一个抛出的错误,都会被最后一个catch()
捕获。
- 一般来说,不要在
then()
方法里面定义Reject
状态的回调函数(即then
的第二个参数),总是使用catch
方法。
// bad
promise
.then(function(data) {
// success
}, function(err) {
// error
});
// good
promise
.then(function(data) { //cb
// success
})
.catch(function(err) {
// error
});
上面代码中,第二种写法要好于第一种写法,理由是第二种写法可以捕获前面then
方法执行中的错误,也更接近同步的写法(try/catch
)。因此,建议总是使用catch()
方法,而不使用then()
方法的第二个参数。
- 跟传统的
try/catch
代码块不同的是,如果没有使用catch()
方法指定错误处理的回调函数,Promise 对象抛出的错误不会传递到外层代码,即不会有任何反应。
const someAsyncThing = function() {
return new Promise(function(resolve, reject) {
// 下面一行会报错,因为x没有声明
resolve(x + 2); //会进入rejected状态
});
};
someAsyncThing().then(function() {
console.log('everything is great');
});
setTimeout(() => { console.log(123) }, 2000);
// Uncaught (in promise) ReferenceError: x is not defined
// 123
上面代码中,someAsyncThing()
函数产生的 Promise 对象,内部有语法错误。浏览器运行到这一行,会打印出错误提示ReferenceError: x is not defined
,但是不会退出进程、终止脚本执行,2 秒之后还是会输出123。这就是说,Promise 内部的错误不会影响到 Promise 外部的代码,通俗的说法就是“Promise 会吃掉错误”。
- 这个脚本放在服务器执行,退出码就是0(即表示执行成功)。不过,Node.js 有一个
unhandledRejection
事件,专门监听未捕获的reject
错误,上面的脚本会触发这个事件的监听函数,可以在监听函数里面抛出错误。
process.on('unhandledRejection', function (err, p) {
throw err;
});
上面代码中,unhandledRejection
事件的监听函数有两个参数,第一个是错误对象,第二个是报错的 Promise 实例,它可以用来了解发生错误的环境信息。
- 注意,Node 有计划在未来废除
unhandledRejection
事件。如果 Promise 内部有未捕获的错误,会直接终止进程,并且进程的退出码不为 0。
const promise = new Promise(function (resolve, reject) {
resolve('ok');
setTimeout(function () { throw new Error('test') }, 0)
});
promise.then(function (value) { console.log(value) });
// ok
// Uncaught Error: test
- 上面代码中,Promise 指定在下一轮“事件循环”再抛出错误。到了那个时候,Promise 的运行已经结束了,所以这个错误是在 Promise 函数体外抛出的,会冒泡到最外层,成了未捕获的错误。
- 一般总是建议,Promise 对象后面要跟
catch()
方法,这样可以处理 Promise 内部发生的错误。catch()
方法返回的还是一个 Promise 对象,因此后面还可以接着调用then()
方法。
const someAsyncThing = function() {
return new Promise(function(resolve, reject) {
// 下面一行会报错,因为x没有声明
resolve(x + 2);
});
};
someAsyncThing()
.catch(function(error) {
console.log('oh no', error);
})
.then(function() {
console.log('carry on');
});
// oh no [ReferenceError: x is not defined]
// carry on
上面代码运行完catch()
方法指定的回调函数,会接着运行后面那个then()
方法指定的回调函数。如果没有报错,则会跳过catch()
方法。
catch()
方法之中,还能再抛出错误。
const someAsyncThing = function() {
return new Promise(function(resolve, reject) {
// 下面一行会报错,因为x没有声明
resolve(x + 2);
});
};
someAsyncThing().then(function() {
return someOtherAsyncThing();
}).catch(function(error) {
console.log('oh no', error);
// 下面一行会报错,因为 y 没有声明
y + 2;
}).then(function() {
console.log('carry on');
});
// oh no [ReferenceError: x is not defined]
上面代码中,catch()
方法抛出一个错误,因为后面没有别的catch()
方法了,导致这个错误不会被捕获,也不会传递到外层。如果改写一下,结果就不一样了。
someAsyncThing().then(function() {
return someOtherAsyncThing();
}).catch(function(error) {
console.log('oh no', error);
// 下面一行会报错,因为y没有声明
y + 2;
}).catch(function(error) {
console.log('carry on', error);
});
// oh no [ReferenceError: x is not defined]
// carry on [ReferenceError: y is not defined]
上面代码中,第二个catch()
方法用来捕获前一个catch()
方法抛出的错误。
Promise.prototype.finally()
finally()
方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。该方法是 ES2018 引入标准的。finally
方法的回调函数不接受任何参数,这意味着没有办法知道,前面的 Promise 状态到底是fulfilled
还是rejected
。这表明,finally方法里面的操作,应该是与状态无关的,不依赖于 Promise 的执行结果。finally
本质上是then方法的特例。
promise
.finally(() => {
// 语句
});
// 等同于
promise
.then(
result => {
// 语句
return result;
},
error => {
// 语句
throw error;
}
);
- 它的实现也很简单。(没有参数传进来)
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 })
);
};
- 从上面的实现还可以看到,
finally
方法总是会返回原来的值。
Promise.all()
Promise.all()
方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。Promise.all()
方法接受一个数组作为参数,p1、p2、p3都是 Promise 实例,如果不是,就会先调用下面讲到的Promise.resolve
方法,将参数转为 Promise 实例,再进一步处理。另外,Promise.all()
方法的参数可以不是数组,但必须具有 Iterator 接口,且返回的每个成员都是 Promise 实例。- p的状态由p1、p2、p3决定,分成两种情况。
(1)只有p1、p2、p3的状态都变成fulfilled
,p的状态才会变成fulfilled
,此时p1、p2、p3的返回值组成一个数组,传递给p的回调函数。
(2)只要p1、p2、p3之中有一个被rejected
,p的状态就变成rejected
,此时第一个被reject
的实例的返回值,会传递给p的回调函数。 - 注意,如果作为参数的 Promise 实例,自己定义了
catch
方法,那么它一旦被rejected
,并不会触发Promise.all()
的catch
方法。
const p1 = new Promise((resolve, reject) => {
resolve('hello');
})
.then(result => result)
.catch(e => e);
const p2 = new Promise((resolve, reject) => {
throw new Error('报错了');
})
.then(result => result)
.catch(e => e);
Promise.all([p1, p2])
.then(result => console.log(result))
.catch(e => console.log(e));
// ["hello", Error: 报错了]
上面代码中,p1会resolved
,p2首先会rejected
,但是p2有自己的catch
方法,该方法返回的是一个新的 Promise 实例,p2指向的实际上是这个实例。该实例执行完catch
方法后,也会变成resolved
,导致Promise.all()
方法参数里面的两个实例都会resolved
,因此会调用then
方法指定的回调函数,而不会调用catch
方法指定的回调函数。
Promise.race()
Promise.race()
方法的参数与Promise.all()
方法一样,如果不是 Promise 实例,就会先调用下面讲到的Promise.resolve()
方法,将参数转为 Promise 实例,再进一步处理。
Promise.allSettled()
Promise.allSettled()
方法接受一组 Promise 实例作为参数,包装成一个新的 Promise 实例。只有等到所有这些参数实例都返回结果,不管是fulfilled
还是rejected
,包装实例才会结束。该方法由 ES2020 引入。- 该方法返回的新的 Promise 实例,一旦结束,状态总是
fulfilled
,不会变成rejected
。状态变成fulfilled
后,Promise 的监听函数接收到的参数是一个数组,每个成员对应一个传入Promise.allSettled()
的 Promise 实例。
const resolved = Promise.resolve(42);
const rejected = Promise.reject(-1);
const allSettledPromise = Promise.allSettled([resolved, rejected]);
allSettledPromise.then(function (results) {
console.log(results);
});
// [
// { status: 'fulfilled', value: 42 },
// { status: 'rejected', reason: -1 }
// ]
- 有时候,我们不关心异步操作的结果,只关心这些操作有没有结束。这时,
Promise.allSettled()
方法就很有用。如果没有这个方法,想要确保所有操作都结束,就很麻烦。Promise.all()
方法无法做到这一点。(一个reject,直接到后面)
Promise.any()
Promise.any()
方法接受一组 Promise 实例作为参数,包装成一个新的 Promise 实例。只要参数实例有一个变成fulfilled
状态,包装实例就会变成fulfilled
状态;如果所有参数实例都变成rejected
状态,包装实例就会变成rejected
状态。该方法目前是一个第三阶段的提案 。
Promise.resolve()
- 有时需要将现有对象转为 Promise 对象,
Promise.resolve()
方法就起到这个作用。 Promise.resolve()
等价于下面的写法。
Promise.resolve('foo')
// 等价于
new Promise(resolve => resolve('foo'))
ps:如果第一种方法传入x+1会报错,不会被捕获,第二种方法传入x+1可以被then里捕获
Promise.resolve()
方法的参数分成四种情况。
(1)参数是一个 Promise 实例
如果参数是 Promise 实例,那么Promise.resolve
将不做任何修改、原封不动地返回这个实例。
(2)参数是一个thenable对象
thenable对象指的是具有then方法的对象,比如下面这个对象。
let thenable = {
then: function(resolve, reject) {
resolve(42);
}
};
Promise.resolve()
方法会将这个对象转为 Promise 对象,然后就立即执行thenable对象的then()
方法。
let thenable = {
then: function(resolve, reject) {
resolve(42);
}
};
let p1 = Promise.resolve(thenable);
p1.then(function (value) {
console.log(value); // 42
});
上面代码中,thenable对象的then()
方法执行后,对象p1的状态就变为resolved
,从而立即执行最后那个then()
方法指定的回调函数,输出42。
(3)参数不是具有then()
方法的对象,或根本就不是对。如果参数是一个原始值,或者是一个不具有then()
方法的对象,则Promise.resolve()
方法返回一个新的 Promise 对象,状态为resolved
。
const p = Promise.resolve('Hello');
p.then(function (s) {
console.log(s)
});
// Hello
上面代码生成一个新的 Promise 对象的实例p。由于字符串Hello不属于异步操作(判断方法是字符串对象不具有 then 方法),返回 Promise 实例的状态从一生成就是resolved,所以回调函数会立即执行。Promise.resolve()
方法的参数,会同时传给回调函数。
(4)不带有任何参数
Promise.resolve()
方法允许调用时不带参数,直接返回一个resolved
状态的 Promise 对象。- 所以,如果希望得到一个 Promise 对象,比较方便的方法就是直接调用
Promise.resolve()
方法。
Promise.reject()
Promise.reject(reason)
方法也会返回一个新的 Promise 实例,该实例的状态为rejected
。
应用
加载图片
const preloadImage = function (path) {
return new Promise(function (resolve, reject) {
const image = new Image();
image.onload = resolve;
image.onerror = reject;
image.src = path;
});
};
Generator 函数与 Promise 的结合
function getFoo () {
return new Promise(function (resolve, reject){
resolve('foo');
});
}
const g = function* () {
try {
const foo = yield getFoo();
console.log(foo);
} catch (e) {
console.log(e);
}
};
function run (generator) {
const it = generator();
function go(result) {
if (result.done) return result.value;
return result.value.then(function (value) {
return go(it.next(value));
}, function (error) {
return go(it.throw(error));
});
}
go(it.next());
}
run(g);
Promise.try()
- 有没有一种方法,让同步函数同步执行,异步函数异步执行,并且让它们具有统一的 API 呢?回答是可以的,并且还有两种写法。第一种写法是用
async
函数来写。
const f = () => console.log('now');
(async () => f())();
console.log('next');
// now
// next
上面代码中,第二行是一个立即执行的匿名函数,会立即执行里面的async
函数,因此如果f是同步的,就会得到同步的结果;如果f是异步的,就可以用then
指定下一步,就像下面的写法。
(async () => f())()
.then(...)
- 需要注意的是,
async () => f()
会吃掉f()
抛出的错误。所以,如果想捕获错误,要使用promise.catch
方法。
(async () => f())()
.then(...)
.catch(...)
第二种写法是使用new Promise()
。
const f = () => console.log('now');
(
() => new Promise(
resolve => resolve(f())
)
)();
console.log('next');
// now
// next
上面代码也是使用立即执行的匿名函数,执行new Promise()
。这种情况下,同步函数也是同步执行的。
- 鉴于这是一个很常见的需求,所以现在有一个提案,提供
Promise.try
方法替代上面的写法。
const f = () => console.log('now');
Promise.try(f);
console.log('next');
Iterator 和 for…of 循环
Iterator(遍历器)的概念
Iterator
的作用有三个:
一、是为各种数据结构,提供一个统一的、简便的访问接口;
二、是使得数据结构的成员能够按某种次序排列;
三、是 ES6 创造了一种新的遍历命令for…of循环,Iterator 接口主要供for…of消费。- Iterator 的遍历过程是这样的。
(1)创建一个指针对象,指向当前数据结构的起始位置。也就是说,遍历器对象本质上,就是一个指针对象。
(2)第一次调用指针对象的next
方法,可以将指针指向数据结构的第一个成员。
(3)第二次调用指针对象的next
方法,指针就指向数据结构的第二个成员。
(4)不断调用指针对象的next
方法,直到它指向数据结构的结束位置。 - 每一次调用
next
方法,都会返回数据结构的当前成员的信息。具体来说,就是返回一个包含value
和done
两个属性的对象。其中,value
属性是当前成员的值,done
属性是一个布尔值,表示遍历是否结束。 - 对于遍历器对象来说,
done: false
和value: undefined
属性都是可以省略的
默认 Iterator 接口
- 当使用
for...of
循环遍历某种数据结构时,该循环会自动去寻找 Iterator 接口。 - ES6 规定,默认的 Iterator 接口部署在数据结构的
Symbol.iterator
属性,或者说,一个数据结构只要具有Symbol.iterator
属性,就可以认为是“可遍历的”(iterable)。 Symbol.iterator
属性本身是一个函数,就是当前数据结构默认的遍历器生成函数。执行这个函数,就会返回一个遍历器。至于属性名Symbol.iterator
,它是一个表达式,返回Symbol对象的iterator属性,这是一个预定义好的、类型为 Symbol 的特殊值,所以要放在方括号内- 原生具备 Iterator 接口的数据结构如下。
Array
Map
Set
String
TypedArray
函数的 arguments 对象
NodeList 对象
- 对象(
Object
)之所以没有默认部署 Iterator 接口,是因为对象的哪个属性先遍历,哪个属性后遍历是不确定的,需要开发者手动指定。 - 一个对象如果要具备可被
for...of
循环调用的 Iterator 接口,就必须在Symbol.iterator
的属性上部署遍历器生成方法(原型链上的对象具有该方法也可)。
class RangeIterator {
constructor(start, stop) {
this.value = start;
this.stop = stop;
}
[Symbol.iterator]() { return this; }
next() {
var value = this.value;
if (value < this.stop) {
this.value++;
return {done: false, value: value};
}
return {done: true, value: undefined};
}
}
function range(start, stop) {
return new RangeIterator(start, stop);
}
for (var value of range(0, 3)) {
console.log(value); // 0, 1, 2
}
- 下面是通过遍历器实现指针结构的例子。
function Obj(value) {
this.value = value;
this.next = null;
}
Obj.prototype[Symbol.iterator] = function() {
var iterator = { next: next };
var current = this;
function next() {
if (current) {
var value = current.value;
current = current.next;
return { done: false, value: value };
} else {
return { done: true };
}
}
return iterator;
}
var one = new Obj(1);
var two = new Obj(2);
var three = new Obj(3);
one.next = two;
two.next = three;
for (var i of one){
console.log(i); // 1, 2, 3
}
- 下面是另一个为对象添加 Iterator 接口的例子。
let obj = {
data: [ 'hello', 'world' ],
[Symbol.iterator]() {
const self = this;
let index = 0;
return {
next() {
if (index < self.data.length) {
return {
value: self.data[index++],
done: false
};
} else {
return { value: undefined, done: true };
}
}
};
}
};
- 对于类似数组的对象(存在数值键名和
length
属性),部署 Iterator 接口,有一个简便方法,就是Symbol.iterator
方法直接引用数组的 Iterator 接口。
NodeList.prototype[Symbol.iterator] = Array.prototype[Symbol.iterator];
// 或者
NodeList.prototype[Symbol.iterator] = [][Symbol.iterator];
[...document.querySelectorAll('div')] // 可以执行了
- 下面是另一个类似数组的对象调用数组的
Symbol.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'
}
- 注意,普通对象部署数组的
Symbol.iterator
方法,并无效果。
let iterable = {
a: 'a',
b: 'b',
c: 'c',
length: 3,
[Symbol.iterator]: Array.prototype[Symbol.iterator]
};
for (let item of iterable) {
console.log(item); // undefined, undefined, undefined
}
- 如果Symbol.iterator方法对应的不是遍历器生成函数(即会返回一个遍历器对象),解释引擎将会报错。
var obj = {};
obj[Symbol.iterator] = () => 1;
[...obj] // TypeError: [] is not a function
调用 Iterator 接口的场合
- 有一些场合会默认调用 Iterator 接口(即
Symbol.iterator
方法),除了下文会介绍的for...of
循环,还有几个别的场合。
(1)解构赋值
对数组和 Set 结构进行解构赋值时,会默认调用Symbol.iterator
方法。
let set = new Set().add('a').add('b').add('c');
let [x,y] = set;
// x='a'; y='b'
let [first, ...rest] = set;
// first='a'; rest=['b','c'];
(2)扩展运算符(...
)也会调用默认的 Iterator 接口。
// 例一
var str = 'hello';
[...str] // ['h','e','l','l','o']
// 例二
let arr = ['b', 'c'];
['a', ...arr, 'd']
// ['a', 'b', 'c', 'd']
上面代码的扩展运算符内部就调用 Iterator 接口。
- 实际上,这提供了一种简便机制,可以将任何部署了 Iterator 接口的数据结构,转为数组。也就是说,只要某个数据结构部署了 Iterator 接口,就可以对它使用扩展运算符,将其转为数组。
let arr = [...iterable];
(3)yield*
yield*
后面跟的是一个可遍历的结构,它会调用该结构的遍历器接口。
let generator = function* () {
yield 1;
yield* [2,3,4];
yield 5;
};
var 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 }
(4)其他场合
- 由于数组的遍历会调用遍历器接口,所以任何接受数组作为参数的场合,其实都调用了遍历器接口。下面是一些例子。
for...of
Array.from()
Map(), Set(), WeakMap(), WeakSet()(比如new Map([['a',1],['b',2]]))
Promise.all()
Promise.race()
字符串的 Iterator 接口
Iterator 接口与 Generator 函数
Symbol.iterator()
方法的最简单实现,还是使用Generator 函数。
let myIterable = {
[Symbol.iterator]: function* () {
yield 1;
yield 2;
yield 3;
}
};
[...myIterable] // [1, 2, 3]
// 或者采用下面的简洁写法
let obj = {
* [Symbol.iterator]() {
yield 'hello';
yield 'world';
}
};
for (let x of obj) {
console.log(x);
}
// "hello"
// "world"
遍历器对象的 return(),throw()
- 遍历器对象除了具有
next()
方法,还可以具有return()
方法和throw()
方法。如果你自己写遍历器对象生成函数,那么next()
方法是必须部署的,return()
方法和throw()
方法是否部署是可选的。 return()
方法的使用场合是,如果for...of
循环提前退出(通常是因为出错,或者有break
语句),就会调用return()
方法。如果一个对象在完成遍历前,需要清理或释放资源,就可以部署return()
方法。
function readLinesSync(file) {
return {
[Symbol.iterator]() {
return {
next() {
return { done: false };
},
return() {
file.close();
return { done: true };
}
};
},
};
}
- 下面的两种情况,都会触发执行return()方法。
// 情况一
for (let line of readLinesSync(fileName)) {
console.log(line);
break;
}
// 情况二
for (let line of readLinesSync(fileName)) {
console.log(line);
throw new Error();
}
- 注意,
return()
方法必须返回一个对象,这是 Generator 语法决定的。 throw()
方法主要是配合 Generator 函数使用,一般的遍历器对象用不到这个方法。
for…of 循环
for...of
循环可以使用的范围包括数组、Set 和 Map 结构、某些类似数组的对象(比如arguments对象、DOM NodeList 对象)、后文的 Generator 对象,以及字符串。
数组
数组原生具备iterator接口(即默认部署了Symbol.iterator属性),for...of
循环本质上就是调用这个接口产生的遍历器,可以用下面的代码证明。
const arr = ['red', 'green', 'blue'];
for(let v of arr) {
console.log(v); // red green blue
}
const obj = {};
obj[Symbol.iterator] = arr[Symbol.iterator].bind(arr);
for(let v of obj) {
console.log(v); // red green blue
}
for...of
循环调用遍历器接口,数组的遍历器接口只返回具有数字索引的属性。这一点跟for...in
循环也不一样。
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"
}
Set 和 Map 结构
- Set 和 Map 结构也原生具有 Iterator 接口,可以直接使用
for...of
循环。
var engines = new Set(["Gecko", "Trident", "Webkit", "Webkit"]);
for (var e of engines) {
console.log(e);
}
// Gecko
// Trident
// Webkit
var es6 = new Map();
es6.set("edition", 6);
es6.set("committee", "TC39");
es6.set("standard", "ECMA-262");
for (var [name, value] of es6) {
console.log(name + ": " + value);
}
// edition: 6
// committee: TC39
// standard: ECMA-262
- 值得注意的地方有两个,首先,遍历的顺序是按照各个成员被添加进数据结构的顺序。其次,Set 结构遍历时,返回的是一个值,而 Map 结构遍历时,返回的是一个数组,该数组的两个成员分别为当前 Map 成员的键名和键值。
计算生成的数据结构
- 有些数据结构是在现有数据结构的基础上,计算生成的。比如,ES6 的数组、Set、Map 都部署了以下三个方法,调用后都返回遍历器对象。
entries()
返回一个遍历器对象,用来遍历[键名, 键值]组成的数组。对于数组,键名就是索引值;对于 Set,键名与键值相同。Map 结构的 Iterator 接口,默认就是调用entries方法。keys()
返回一个遍历器对象,用来遍历所有的键名。values()
返回一个遍历器对象,用来遍历所有的键值。
类似数组的对象
- 类似数组的对象包括好几类。下面是
for...of
循环用于字符串、DOM NodeList 对象、arguments对象的例子。 - 对于字符串来说,
for...of
循环还有一个特点,就是会正确识别 32 位 UTF-16 字符。
for (let x of 'auD83DuDC0A') {
console.log(x);
}
// 'a'
// 'uD83DuDC0A'
- 并不是所有类似数组的对象都具有 Iterator 接口,一个简便的解决方法,就是使用
Array.from
方法将其转为数组。
对象
- 对于普通的对象,
for...of
结构不能直接使用,会报错,必须部署了 Iterator 接口后才能使用。 - 一种解决方法是,使用
Object.keys
方法将对象的键名生成一个数组,然后遍历这个数组。 - 另一个方法是使用 Generator 函数将对象重新包装一下。
function* entries(obj) {
for (let key of Object.keys(obj)) {
yield [key, obj[key]];
}
}
for (let [key, value] of entries(obj)) {
console.log(key, '->', value);
}
// a -> 1
// b -> 2
// c -> 3
与其他遍历语法的比较
- 以数组为例,JavaScript 提供多种遍历语法。最原始的写法就是for循环。这种写法比较麻烦,因此数组提供内置的
forEach
方法。这种写法的问题在于,无法中途跳出forEach
循环,break
命令或return
命令都不能奏效。 for...in
循环有几个缺点。
1、数组的键名是数字,但是for...in
循环是以字符串作为键名“0”、“1”、“2”等等。
2、for...in
循环不仅遍历数字键名,还会遍历手动添加的其他键,甚至包括原型链上的键。
3、某些情况下,for...in
循环会以任意顺序遍历键名。- 总之,
for...in
循环主要是为遍历对象而设计的,不适用于遍历数组。 for...of
循环相比上面几种做法,有一些显著的优点。
for (let value of myArray) {
console.log(value);
}
- 不同于forEach方法,它可以与break、continue和return配合使用。
Generator 函数的语法
简介
基本概念
- 调用 Generator 函数,返回一个遍历器对象,代表 Generator 函数的内部指针。以后,每次调用遍历器对象的next方法,就会返回一个有着value和done两个属性的对象。value属性表示当前的内部状态的值,是yield表达式后面那个表达式的值;done属性是一个布尔值,表示是否遍历结束。
- ES6 没有规定,function关键字与函数名之间的星号,写在哪个位置。这导致下面的写法都能通过。
function * foo(x, y) { ··· }
function *foo(x, y) { ··· }
function* foo(x, y) { ··· }
function*foo(x, y) { ··· }
一般的写法是上面的第三种
yield 表达式
- 遍历器对象的
next
方法的运行逻辑如下。
(1)遇到yield表达式,就暂停执行后面的操作,并将紧跟在yield后面的那个表达式的值,作为返回的对象的value属性值。
(2)下一次调用next方法时,再继续往下执行,直到遇到下一个yield表达式。
(3)如果没有再遇到新的yield表达式,就一直运行到函数结束,直到return语句为止,并将return语句后面的表达式的值,作为返回的对象的value属性值。
(4)如果该函数没有return语句,则返回的对象的value属性值为undefined。
- 需要注意的是,
yield
表达式后面的表达式,只有当调用next
方法、内部指针指向该语句时才会执行 - Generator 函数可以不用
yield
表达式,这时就变成了一个单纯的暂缓执行函数。 - 另外需要注意,
yield
表达式只能用在 Generator 函数里面,用在其他地方都会报错。嵌套函数里也不行 - 另外,
yield
表达式如果用在另一个表达式之中,必须放在圆括号里面。
function* demo() {
console.log('Hello' + yield); // SyntaxError
console.log('Hello' + yield 123); // SyntaxError
console.log('Hello' + (yield)); // OK
console.log('Hello' + (yield 123)); // OK
}
yield
表达式用作函数参数或放在赋值表达式的右边,可以不加括号。
function* demo() {
foo(yield 'a', yield 'b'); // OK
let input = yield; // OK
}
与 Iterator 接口的关系
- Generator 函数执行后,返回一个遍历器对象。该对象本身也具有
Symbol.iterator
属性,执行后返回自身。
next 方法的参数
yield
表达式本身没有返回值,或者说总是返回undefined。next
方法可以带一个参数,该参数就会被当作上一个yield
表达式的返回值。
function* f() {
for(var i = 0; true; i++) {
var reset = yield i;
if(reset) { i = -1; }
}
}
var g = f();
g.next() // { value: 0, done: false }
g.next() // { value: 1, done: false }
g.next(true) // { value: 0, done: false }
function* foo(x) {
var y = 2 * (yield (x + 1));
var z = yield (y / 3);
return (x + y + z);
}
var a = foo(5);
a.next() // Object{value:6, done:false}
a.next() // Object{value:NaN, done:false}
a.next() // Object{value:NaN, done:true}
var b = foo(5);
b.next() // { value:6, done:false }
b.next(12) // { value:8, done:false }
b.next(13) // { value:42, done:true }
- 注意,由于
next
方法的参数表示上一个yield
表达式的返回值,所以在第一次使用next
方法时,传递参数是无效的。V8 引擎直接忽略第一次使用next
方法时的参数,只有从第二次使用next
方法开始,参数才是有效的。从语义上讲,第一个next
方法用来启动遍历器对象,所以不用带有参数。
function* dataConsumer() {
console.log('Started');
console.log(`1. ${yield}`);
console.log(`2. ${yield}`);
return 'result';
}
let genObj = dataConsumer();
genObj.next();
// Started
genObj.next('a')
// 1. a
genObj.next('b')
// 2. b
for…of 循环
for...of
循环可以自动遍历 Generator 函数运行时生成的Iterator对象,且此时不再需要调用next
方法。
function* foo() {
yield 1;
yield 2;
return 6;
}
for (let v of foo()) {
console.log(v);
}
// 1 2
- 一旦
next
方法的返回对象的done属性为true,for...of
循环就会中止,且不包含该返回对象
function* objectEntries(obj) {
let propKeys = Reflect.ownKeys(obj);
for (let propKey of propKeys) {
yield [propKey, obj[propKey]];
}
}
let jane = { first: 'Jane', last: 'Doe' };
for (let [key, value] of objectEntries(jane)) {
console.log(`${key}: ${value}`);
}
// first: Jane
// last: Doe
上面代码中,对象jane原生不具备 Iterator 接口,无法用for...of
遍历。这时,我们通过 Generator 函数objectEntries
为它加上遍历器接口,就可以用for...of
遍历了。加上遍历器接口的另一种写法是,将 Generator 函数加到对象的Symbol.iterator
属性上面。
function* objectEntries() {
let propKeys = Object.keys(this);
for (let propKey of propKeys) {
yield [propKey, this[propKey]];
}
}
let jane = { first: 'Jane', last: 'Doe' };
jane[Symbol.iterator] = objectEntries;
for (let [key, value] of jane) {
console.log(`${key}: ${value}`);
}
// first: Jane
// last: Doe
除了for...of
循环以外,扩展运算符(...
)、解构赋值和Array.from
方法内部调用的,都是遍历器接口。这意味着,它们都可以将 Generator 函数返回的 Iterator 对象,作为参数。
Generator.prototype.throw()
- Generator 函数返回的遍历器对象,都有一个
throw
方法,可以在函数体外抛出错误,然后在 Generator 函数体内捕获。
var g = function* () {
try {
yield;
} catch (e) {
console.log('内部捕获', e);
}
};
var i = g();
i.next();
try {
i.throw('a');
i.throw('b');
} catch (e) {
console.log('外部捕获', e);
}
// 内部捕获 a
// 外部捕获 b
上面代码中,遍历器对象i连续抛出两个错误。第一个错误被 Generator 函数体内的catch
语句捕获。i第二次抛出错误,由于 Generator 函数内部的catch语句已经执行过了,不会再捕捉到这个错误了,所以这个错误就被抛出了 Generator 函数体,被函数体外的catch
语句捕获。
throw
方法可以接受一个参数,该参数会被catch
语句接收,建议抛出Error
对象的实例。
try {
throw new Error('a');
throw new Error('b');
} catch (e) {
console.log('外部捕获', e);
}
// 外部捕获 [Error: a]
- 上面代码之所以只捕获了a,是因为函数体外的
catch
语句块,捕获了抛出的a错误以后,就不会再继续try
代码块里面剩余的语句了。 - 如果 Generator 函数内部没有部署
try...catch
代码块,那么throw
方法抛出的错误,将被外部try...catch
代码块捕获。
var g = function* () {
while (true) {
yield;
console.log('内部捕获', e);
}
};
var i = g();
i.next();
try {
i.throw('a');
i.throw('b');
} catch (e) {
console.log('外部捕获', e);
}
// 外部捕获 a
- 如果 Generator 函数内部和外部,都没有部署
try...catch
代码块,那么程序将报错,直接中断执行。 throw
方法抛出的错误要被内部捕获,前提是必须至少执行过一次next
方法。
function* gen() {
try {
yield 1;
} catch (e) {
console.log('内部捕获');
}
}
var g = gen();
g.throw(1);
// Uncaught 1
- 上面代码中,
g.throw(1)
执行时,next
方法一次都没有执行过。这时,抛出的错误不会被内部捕获,而是直接在外部抛出,导致程序出错。这种行为其实很好理解,因为第一次执行next
方法,等同于启动执行 Generator 函数的内部代码,否则 Generator 函数还没有开始执行,这时throw
方法抛错只可能抛出在函数外部。 throw
方法被捕获以后,会附带执行下一条yield
表达式。也就是说,会附带执行一次next
方法。
var gen = function* gen(){
try {
yield console.log('a');
} catch (e) {
// ...
}
yield console.log('b');
yield console.log('c');
}
var g = gen();
g.next() // a
g.throw() // b
g.next() // c
上面代码中,可以看到,只要 Generator 函数内部部署了try...catch
代码块,那么遍历器的throw
方法抛出的错误,不影响下一次遍历。
- Generator 函数体内抛出的错误,可以被函数体外的
catch
捕获。
function* foo() {
var x = yield 3;
var y = x.toUpperCase();
yield y;
}
var it = foo();
it.next(); // { value:3, done:false }
try {
it.next(42);
} catch (err) {
console.log(err);
}
- 一旦 Generator 执行过程中抛出错误,且没有被内部捕获,就不会再执行下去了。如果此后还调用
next
方法,将返回一个value属性等于undefined、done属性等于true的对象,即 JavaScript 引擎认为这个 Generator 已经运行结束了。
Generator.prototype.return()
- Generator 函数返回的遍历器对象,还有一个
return
方法,可以返回给定的值,并且终结遍历 Generator 函数。
function* gen() {
yield 1;
yield 2;
yield 3;
}
var g = gen();
g.next() // { value: 1, done: false }
g.return('foo') // { value: "foo", done: true }
g.next() // { value: undefined, done: true }
如果return
方法调用时,不提供参数,则返回值的value属性为undefined。
- 如果 Generator 函数内部有
try...finally
代码块,且正在执行try
代码块,那么return方法会导致立刻进入finally
代码块,执行完以后,整个函数才会结束。
function* numbers () {
yield 1;
try {
yield 2;
yield 3;
} finally {
yield 4;
yield 5;
}
yield 6;
}
var 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 }
next()、throw()、return() 的共同点
next()
是将yield表达式替换成一个值。
throw()
是将yield表达式替换成一个throw
语句。
return()
是将yield表达式替换成一个return
语句。
yield* 表达式
- 如果在 Generator 函数内部,调用另一个 Generator 函数。需要在前者的函数体内部,自己手动完成遍历。
function* foo() {
yield 'a';
yield 'b';
}
function* bar() {
yield 'x';
// 手动遍历 foo()
for (let i of foo()) {
console.log(i);
}
yield 'y';
}
for (let v of bar()){
console.log(v);
}
// x
// a
// b
// y
- ES6 提供了
yield*
表达式,作为解决办法,用来在一个 Generator 函数里面执行另一个 Generator 函数。
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"
function* inner() {
yield 'hello!';
}
function* outer1() {
yield 'open';
yield inner();
yield 'close';
}
var gen = outer1()
gen.next().value // "open"
gen.next().value // 返回一个遍历器对象
gen.next().value // "close"
function* outer2() {
yield 'open'
yield* inner()
yield 'close'
}
var gen = outer2()
gen.next().value // "open"
gen.next().value // "hello!"
gen.next().value // "close"
yield*
后面的 Generator 函数(没有return
语句时),不过是for...of
的一种简写形式,完全可以用后者替代前者。反之,在有return
语句时,则需要用var value = yield* iterator
的形式获取return
语句的值。- 任何数据结构只要有
Iterator
接口,就可以被yield*
遍历。 - 如果被代理的 Generator 函数有
return
语句,那么就可以向代理它的 Generator 函数返回数据。
function* foo() {
yield 2;
yield 3;
return "foo";
}
function* bar() {
yield 1;
var v = yield* foo();
console.log("v: " + v);
yield 4;
}
var it = bar();
it.next()
// {value: 1, done: false}
it.next()
// {value: 2, done: false}
it.next()
// {value: 3, done: false}
it.next();
// "v: foo"
// {value: 4, done: false}
it.next()
// {value: undefined, done: true}
- 下面是一个稍微复杂的例子,使用
yield*
语句遍历完全二叉树。
// 下面是二叉树的构造函数,
// 三个参数分别是左树、当前节点和右树
function Tree(left, label, right) {
this.left = left;
this.label = label;
this.right = right;
}
// 下面是中序(inorder)遍历函数。
// 由于返回的是一个遍历器,所以要用generator函数。
// 函数体内采用递归算法,所以左树和右树要用yield*遍历
function* inorder(t) {
if (t) {
yield* inorder(t.left);
yield t.label;
yield* inorder(t.right);
}
}
// 下面生成二叉树
function make(array) {
// 判断是否为叶节点
if (array.length == 1) return new Tree(null, array[0], null);
return new Tree(make(array[0]), array[1], make(array[2]));
}
let tree = make([[['a'], 'b', ['c']], 'd', [['e'], 'f', ['g']]]);
// 遍历二叉树
var result = [];
for (let node of inorder(tree)) {
result.push(node);
}
result
// ['a', 'b', 'c', 'd', 'e', 'f', 'g']
作为对象属性的 Generator 函数
如果一个对象的属性是 Generator 函数,可以简写成下面的形式。
let obj = {
* myGeneratorMethod() {
···
}
};
Generator 函数的this
- Generator 函数总是返回一个遍历器,ES6 规定这个遍历器是 Generator 函数的实例,也继承了 Generator 函数的
prototype
对象上的方法。
function* g() {}
g.prototype.hello = function () {
return 'hi!';
};
let obj = g();
obj instanceof g // true
obj.hello() // 'hi!'
- 如果把g当作普通的构造函数,并不会生效,因为g返回的总是遍历器对象,而不是
this
对象。
function* g() {
this.a = 11;
}
let obj = g();
obj.next();
obj.a // undefined
- Generator 函数也不能跟
new
命令一起用,会报错。 - 那么,有没有办法让 Generator 函数返回一个正常的对象实例,既可以用
next
方法,又可以获得正常的this
? - 下面是一个变通方法。首先,生成一个空对象,使用
call
方法绑定 Generator 函数内部的this
。这样,构造函数调用以后,这个空对象就是 Generator 函数的实例对象了。
function* F() {
this.a = 1;
yield this.b = 2;
yield this.c = 3;
}
var obj = {};
var f = F.call(obj);
f.next(); // Object {value: 2, done: false}
f.next(); // Object {value: 3, done: false}
f.next(); // Object {value: undefined, done: true}
obj.a // 1
obj.b // 2
obj.c // 3
- 上面代码中,执行的是遍历器对象f,但是生成的对象实例是obj,有没有办法将这两个对象统一呢?一个办法就是将obj换成F.prototype。
function* F() {
this.a = 1;
yield this.b = 2;
yield this.c = 3;
}
var f = F.call(F.prototype);
f.next(); // Object {value: 2, done: false}
f.next(); // Object {value: 3, done: false}
f.next(); // Object {value: undefined, done: true}
f.a // 1
f.b // 2
f.c // 3
- 再将F改成构造函数,就可以对它执行
new
命令了。
function* gen() {
this.a = 1;
yield this.b = 2;
yield this.c = 3;
}
function F() {
return gen.call(gen.prototype);
}
var f = new F();
f.next(); // Object {value: 2, done: false}
f.next(); // Object {value: 3, done: false}
f.next(); // Object {value: undefined, done: true}
f.a // 1
f.b // 2
f.c // 3
含义
Generator 与状态机
var clock = function* () {
while (true) {
console.log('Tick!');
yield;
console.log('Tock!');
yield;
}
};
Generator 与协程
(1)协程与子例程的差异
(2)协程与普通线程的差异
Generator 与上下文
应用
(1)异步操作的同步化表达
(2)控制流管理
(3)部署 Iterator 接口
(4)作为数据结构
Generator 函数的异步应用
传统方法
- ES6 诞生以前,异步编程的方法,大概有下面四种。
1、回调函数
2、事件监听
3、发布/订阅
4、Promise 对象
基本概念
异步
回调函数
Promise
Generator 函数
协程
- 协程有点像函数,又有点像线程。它的运行流程大致如下。
第一步,协程A开始执行。
第二步,协程A执行到一半,进入暂停,执行权转移到协程B。
第三步,(一段时间后)协程B交还执行权。
第四步,协程A恢复执行。
协程的 Generator 函数实现
Generator 函数的数据交换和错误处理
异步任务的封装
var fetch = require('node-fetch');
function* gen(){
var url = 'https://api.github.com/users/github';
var result = yield fetch(url);
console.log(result.bio);
}
var g = gen();
var result = g.next();
result.value.then(function(data){
return data.json();
}).then(function(data){
g.next(data);
});
Thunk 函数
- Thunk 函数是自动执行 Generator 函数的一种方法。
参数的求值策略
- “传值调用”
- “传名调用”
Thunk 函数的含义
- 编译器的“传名调用”实现,往往是将参数放到一个临时函数之中,再将这个临时函数传入函数体。这个临时函数就叫做 Thunk 函数。
JavaScript 语言的 Thunk 函数
- JavaScript 语言是传值调用,它的 Thunk 函数含义有所不同。在 JavaScript 语言中,Thunk 函数替换的不是表达式,而是多参数函数,将其替换成一个只接受回调函数作为参数的单参数函数。
// 正常版本的readFile(多参数版本)
fs.readFile(fileName, callback);
// Thunk版本的readFile(单参数版本)
var Thunk = function (fileName) {
return function (callback) {
return fs.readFile(fileName, callback);
};
};
var readFileThunk = Thunk(fileName);
readFileThunk(callback);
上面代码中,fs模块的readFile方法是一个多参数函数,两个参数分别为文件名和回调函数。经过转换器处理,它变成了一个单参数函数,只接受回调函数作为参数。这个单参数版本,就叫做 Thunk 函数。
Thunkify 模块
Generator 函数的流程管理
- Thunk 函数现在可以用于 Generator 函数的自动流程管理。
co 模块
基本用法
- co 模块是著名程序员 TJ Holowaychuk 于 2013 年 6 月发布的一个小工具,用于 Generator 函数的自动执行。
- co 模块可以让你不用编写 Generator 函数的执行器。
var co = require('co');
co(gen);
上面代码中,Generator 函数只要传入co函数,就会自动执行。
co函数返回一个Promise对象,因此可以用then方法添加回调函数。
co(gen).then(function (){
console.log('Generator 函数执行完成');
});
上面代码中,等到 Generator 函数执行结束,就会输出一行提示。
co 模块的原理
Generator 就是一个异步操作的容器。它的自动执行需要一种机制,当异步操作有了结果,能够自动交回执行权。
两种方法可以做到这一点。
(1)回调函数。将异步操作包装成 Thunk 函数,在回调函数里面交回执行权。
(2)Promise 对象。将异步操作包装成 Promise 对象,用then方法交回执行权。
co 模块其实就是将两种自动执行器(Thunk 函数和 Promise 对象),包装成一个模块。使用 co 的前提条件是,Generator 函数的yield命令后面,只能是 Thunk 函数或 Promise 对象。如果数组或对象的成员,全部都是 Promise 对象,也可以使用 co
基于 Promise 对象的自动执行
co 模块的源码
处理并发的异步操作
async 函数
含义
async
函数是什么?一句话,它就是 Generator 函数的语法糖。async
函数对 Generator 函数的改进,体现在以下四点。
(1)内置执行器。
Generator 函数的执行必须靠执行器,所以才有了co模块,而async
函数自带执行器。也就是说,async
函数的执行,与普通函数一模一样,只要一行。
(2)更好的语义。
(3)更广的适用性。
async
函数的await
命令后面,可以是 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时会自动转成立即 resolved 的 Promise 对象)。
(4)返回值是 Promise。
基本用法
async
函数返回一个 Promise 对象,可以使用then
方法添加回调函数。当函数执行的时候,一旦遇到await
就会先返回,等到异步操作完成,再接着执行函数体内后面的语句。
语法
async
函数的语法规则总体上比较简单,难点是错误处理机制。
返回 Promise 对象
async
函数返回一个 Promise 对象。async
函数内部return语句返回的值,会成为then
方法回调函数的参数。async
函数内部抛出错误,会导致返回的 Promise 对象变为reject
状态。
Promise 对象的状态变化
async
函数返回的 Promise 对象,必须等到内部所有await
命令后面的 Promise 对象执行完,才会发生状态改变,除非遇到return语句或者抛出错误。
await 命令
- 正常情况下,
await
命令后面是一个 Promise 对象,返回该对象的结果。如果不是 Promise 对象,就直接返回对应的值。
async function f() {
// 等同于
// return 123;
return await 123;
}
f().then(v => console.log(v))
// 123
- 另一种情况是,
await
命令后面是一个thenable对象(即定义了then方法的对象),那么await
会将其等同于 Promise 对象。(会进入then
方法,传入resolve
和reject
,如果reject
,需要catch
,需要resolve
才能赋值到sleepTime
,后面的语句才能执行)
class Sleep {
constructor(timeout) {
this.timeout = timeout;
}
then(resolve, reject) {
const startTime = Date.now();
setTimeout(
() => resolve(Date.now() - startTime),
this.timeout
);
}
}
(async () => {
const sleepTime = await new Sleep(1000);
console.log(sleepTime);
})();
// 1000
- 简化的sleep实现。
function sleep(interval) {
return new Promise(resolve => {
setTimeout(resolve, interval);
})
}
// 用法
async function one2FiveInAsync() {
for(let i = 1; i <= 5; i++) {
console.log(i);
await sleep(1000);
}
}
one2FiveInAsync();
await
命令后面的 Promise 对象如果变为reject
状态,则reject
的参数会被catch
方法的回调函数接收到。await
语句前面没有return,但是reject
方法的参数依然传入了catch
方法的回调函数。这里如果在await
前面加上return,效果是一样的。- 任何一个
await
语句后面的 Promise 对象变为reject
状态,那么整个async
函数都会中断执行。
async function f() {
await Promise.reject('出错了');
await Promise.resolve('hello world'); // 不会执行
}
- 有时,我们希望即使前一个异步操作失败,也不要中断后面的异步操作。这时可以将第一个
await
放在try...catch
结构里面,这样不管这个异步操作是否成功,第二个await
都会执行。 - 另一种方法是
await
后面的 Promise 对象再跟一个catch
方法,处理前面可能出现的错误。
错误处理
- 如果
await
后面的异步操作出错,那么等同于async
函数返回的 Promise 对象被reject
。防止出错的方法,也是将其放在try...catch
代码块之中。如果有多个await
命令,可以统一放在try...catch
结构中。 - 下面的例子使用
try...catch
结构,实现多次重复尝试。
const superagent = require('superagent');
const NUM_RETRIES = 3;
async function test() {
let i;
for (i = 0; i < NUM_RETRIES; ++i) {
try {
await superagent.get('http://google.com/this-throws-an-error');
break;
} catch(err) {}
}
console.log(i); // 3
}
test();
上面代码中,如果await
操作成功,就会使用break
语句退出循环;如果失败,会被catch
语句捕捉,然后进入下一轮循环。
使用注意点
- 第一点,前面已经说过,
await
命令后面的Promise对象,运行结果可能是rejected
,所以最好把await
命令放在try...catch
代码块中。
async function myFunction() {
try {
await somethingThatReturnsAPromise();
} catch (err) {
console.log(err);
}
}
// 另一种写法
async function myFunction() {
await somethingThatReturnsAPromise()
.catch(function (err) {
console.log(err);
});
}
- 第二点,多个
await
命令后面的异步操作,如果不存在继发关系,最好让它们同时触发。
let foo = await getFoo();
let bar = await getBar();
上面代码中,getFoo和getBar是两个独立的异步操作(即互不依赖),被写成继发关系。这样比较耗时,因为只有getFoo完成以后,才会执行getBar,完全可以让它们同时触发。
// 写法一
let [foo, bar] = await Promise.all([getFoo(), getBar()]);
// 写法二
let fooPromise = getFoo();
let barPromise = getBar();
let foo = await fooPromise;
let bar = await barPromise;
上面两种写法,getFoo和getBar都是同时触发,这样就会缩短程序的执行时间。
- 第三点,
await
命令只能用在async
函数之中,如果用在普通函数,就会报错。 - 第四点,
async
函数可以保留运行堆栈。
const a = () => {
b().then(() => c());
};
上面代码中,函数a内部运行了一个异步任务b()。当b()运行的时候,函数a()不会中断,而是继续执行。等到b()运行结束,可能a()早就运行结束了,b()所在的上下文环境已经消失了。如果b()或c()报错,错误堆栈将不包括a()。
现在将这个例子改成async
函数。
const a = async () => {
await b();
c();
};
上面代码中,b()运行的时候,a()是暂停执行,上下文环境都保存着。一旦b()或c()报错,错误堆栈将包括a()。
async 函数的实现原理
- async 函数的实现原理,就是将 Generator 函数和自动执行器,包装在一个函数里。
async function fn(args) {
// ...
}
// 等同于
function fn(args) {
return spawn(function* () {
// ...
});
}
- 所有的
async
函数都可以写成上面的第二种形式,其中的spawn
函数就是自动执行器。 - 下面给出
spawn
函数的实现,基本就是前文自动执行器的翻版。
function spawn(genF) {
return new Promise(function(resolve, reject) {
const gen = genF();
function step(nextF) {
let next;
try {
next = nextF();
} catch(e) {
return reject(e);
}
if(next.done) {
return resolve(next.value);
}
Promise.resolve(next.value).then(function(v) {
step(function() { return gen.next(v); });
}, function(e) {
step(function() { return gen.throw(e); });
});
}
step(function() { return gen.next(undefined); });
});
}
顶层 await
Class 的基本语法
简介
类的由来
- ES6 的类,完全可以看作构造函数的另一种写法。
class Point {
// ...
}
typeof Point // "function"
Point === Point.prototype.constructor // true
上面代码表明,类的数据类型就是函数,类本身就指向构造函数。
- 使用的时候,也是直接对类使用
new
命令,跟构造函数的用法完全一致。 - 构造函数的
prototype
属性,在 ES6 的“类”上面继续存在。事实上,类的所有方法都定义在类的prototype
属性上面。由于类的方法都定义在prototype
对象上面,所以类的新方法可以添加在prototype
对象上面。Object.assign
方法可以很方便地一次向类添加多个方法。
class Point {
constructor(){
// ...
}
}
Object.assign(Point.prototype, {
toString(){},
toValue(){}
});
prototype
对象的constructor
属性,直接指向“类”的本身,这与 ES5 的行为是一致的。
Point.prototype.constructor === Point // true
- 另外,类的内部所有定义的方法,都是不可枚举的(
non-enumerable
)。
class Point {
constructor(x, y) {
// ...
}
toString() {
// ...
}
}
Object.keys(Point.prototype)
// []
Object.getOwnPropertyNames(Point.prototype)
// ["constructor","toString"]
上面代码中,toString
方法是Point类内部定义的方法,它是不可枚举的。这一点与 ES5 的行为不一致。
constructor 方法
constructor
方法是类的默认方法,通过new
命令生成对象实例时,自动调用该方法。一个类必须有constructor
方法,如果没有显式定义,一个空的constructor
方法会被默认添加。constructor
方法默认返回实例对象(即this
),完全可以指定返回另外一个对象。
class Foo {
constructor() {
return Object.create(null);
}
}
new Foo() instanceof Foo
// false
- 类必须使用
new
调用,否则会报错。这是它跟普通构造函数的一个主要区别,后者不用new
也可以执行。
类的实例
- 与 ES5 一样,类的所有实例共享一个原型对象。
var p1 = new Point(2,3);
var p2 = new Point(3,2);
p1.__proto__ === p2.__proto__
//true
__proto__
并不是语言本身的特性,这是各大厂商具体实现时添加的私有属性,虽然目前很多现代浏览器的 JS 引擎中都提供了这个私有属性,但依旧不建议在生产中使用该属性,避免对环境产生依赖。生产环境中,我们可以使用Object.getPrototypeOf
方法来获取实例对象的原型,然后再来为原型添加方法/属性。
var p1 = new Point(2,3);
var p2 = new Point(3,2);
p1.__proto__.printName = function () { return 'Oops' };
p1.printName() // "Oops"
p2.printName() // "Oops"
var p3 = new Point(4,2);
p3.printName() // "Oops"
取值函数(getter)和存值函数(setter)
- 与 ES5 一样,在“类”的内部可以使用
get
和set
关键字,对某个属性设置存值函数和取值函数,拦截该属性的存取行为。
class MyClass {
constructor() {
// ...
}
get prop() {
return 'getter';
}
set prop(value) {
console.log('setter: '+value);
}
}
let inst = new MyClass();
inst.prop = 123;
// setter: 123
inst.prop
// 'getter'
- 存值函数和取值函数是设置在属性的
Descriptor
对象上的。
class CustomHTMLElement {
constructor(element) {
this.element = element;
}
get html() {
return this.element.innerHTML;
}
set html(value) {
this.element.innerHTML = value;
}
}
var descriptor = Object.getOwnPropertyDescriptor(
CustomHTMLElement.prototype, "html"
);
"get" in descriptor // true
"set" in descriptor // true
- 属性表达式
let methodName = 'getArea';
class Square {
constructor(length) {
// ...
}
[methodName]() {
// ...
}
}
Class 表达式
- 与函数一样,类也可以使用表达式的形式定义。
const MyClass = class Me {
getClassName() {
return Me.name;
}
};
上面代码使用表达式定义了一个类。需要注意的是,这个类的名字是Me,但是Me只在 Class 的内部可用,指代当前类。在 Class 外部,这个类只能用MyClass引用。
let inst = new MyClass();
inst.getClassName() // Me
Me.name // ReferenceError: Me is not defined
上面代码表示,Me只在 Class 内部有定义。
- 如果类的内部没用到的话,可以省略Me,也就是可以写成下面的形式。
const MyClass = class { /* ... */ };
let person = new class {
constructor(name) {
this.name = name;
}
sayName() {
console.log(this.name);
}
}('张三');
person.sayName(); // "张三"
注意点
(1)严格模式
类和模块的内部,默认就是严格模式,所以不需要使用use strict
指定运行模式。
(2)不存在提升
类不存在变量提升(hoist
),这一点与 ES5 完全不同。
new Foo(); // ReferenceError
class Foo {}
(3)name
属性
由于本质上,ES6 的类只是 ES5 的构造函数的一层包装,所以函数的许多特性都被Class继承,包括name
属性。
class Point {}
Point.name // "Point"
(4)Generator 方法
如果某个方法之前加上星号(*),就表示该方法是一个 Generator 函数。
class Foo {
constructor(...args) {
this.args = args;
}
* [Symbol.iterator]() {
for (let arg of this.args) {
yield arg;
}
}
}
for (let x of new Foo('hello', 'world')) {
console.log(x);
}
// hello
// world
(5)this
的指向
类的方法内部如果含有this
,它默认指向类的实例。但是,必须非常小心,一旦单独使用该方法,很可能报错。
class Logger {
printName(name = 'there') {
this.print(`Hello ${name}`);
}
print(text) {
console.log(text);
}
}
const logger = new Logger();
const { printName } = logger; //可以取到
printName(); // TypeError: Cannot read property 'print' of undefined
上面代码中,printName
方法中的this
,默认指向Logger类的实例。但是,如果将这个方法提取出来单独使用,this
会指向该方法运行时所在的环境(由于 class 内部是严格模式,所以 this
实际指向的是undefined),从而导致找不到print
方法而报错。
- 一个比较简单的解决方法是,在构造方法中绑定
this
,这样就不会找不到print
方法了。
class Logger {
constructor() {
this.printName = this.printName.bind(this);
}
// ...
}
- 另一种解决方法是使用箭头函数。
class Obj {
constructor() {
this.getThis = () => this;
}
}
const myObj = new Obj();
myObj.getThis() === myObj // true
- 还有一种解决方法是使用Proxy,获取方法的时候,自动绑定
this
。
function selfish (target) {
const cache = new WeakMap();
const handler = {
get (target, key) {
const value = Reflect.get(target, key);
if (typeof value !== 'function') {
return value;
}
if (!cache.has(value)) {
cache.set(value, value.bind(target));
}
return cache.get(value);
}
};
const proxy = new Proxy(target, handler);
return proxy;
}
const logger = selfish(new Logger());
静态方法
- 类相当于实例的原型,所有在类中定义的方法,都会被实例继承。如果在一个方法前,加上
static
关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”。
class Foo {
static classMethod() {
return 'hello';
}
}
Foo.classMethod() // 'hello'
var foo = new Foo();
foo.classMethod()
// TypeError: foo.classMethod is not a function
- 注意,如果静态方法包含
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 {
}
Bar.classMethod() // 'hello'
- 静态方法也是可以从
super
对象上调用的。
class Foo {
static classMethod() {
return 'hello';
}
}
class Bar extends Foo {
static classMethod() {
return super.classMethod() + ', too';
}
}
Bar.classMethod() // "hello, too"
实例属性的新写法
class IncreasingCounter {
_count = 0;
get value() {
console.log('Getting the current value!');
return this._count;
}
increment() {
this._count++;
}
}
- 上面代码中,实例属性
_count
与取值函数value()
和increment()
方法,处于同一个层级。这时,不需要在实例属性前面加上this
。这种新写法的好处是,所有实例对象自身的属性都定义在类的头部,看上去比较整齐,一眼就能看出这个类有哪些实例属性。
静态属性
静态属性指的是 Class 本身的属性,即Class.propName
,而不是定义在实例对象(this
)上的属性。
class Foo {
}
Foo.prop = 1;
Foo.prop // 1
上面的写法为Foo类定义了一个静态属性prop。
- 目前,只有这种写法可行,因为 ES6 明确规定,Class 内部只有静态方法,没有静态属性。现在有一个提案提供了类的静态属性,写法是在实例属性的前面,加上
static
关键字。
class MyClass {
static myStaticProp = 42;
constructor() {
console.log(MyClass.myStaticProp); // 42
}
}
私有方法和私有属性
现有的解决方案
- 一种做法是在命名上加以区别。
class Widget {
// 公有方法
foo (baz) {
this._bar(baz);
}
// 私有方法
_bar(baz) {
return this.snaf = baz;
}
// ...
}
- 另一种方法就是索性将私有方法移出模块,因为模块内部的所有方法都是对外可见的。
class Widget {
foo (baz) {
bar.call(this, baz);
}
// ...
}
function bar(baz) {
return this.snaf = baz;
}
- 还有一种方法是利用Symbol值的唯一性,将私有方法的名字命名为一个Symbol值。
const bar = Symbol('bar');
const snaf = Symbol('snaf');
export default class myClass{
// 公有方法
foo(baz) {
this[bar](baz);
}
// 私有方法
[bar](baz) {
return this[snaf] = baz;
}
// ...
};
私有属性的提案
目前,有一个提案,为class加了私有属性。方法是在属性名之前,使用#表示。
new.target 属性
new
是从构造函数生成实例对象的命令。ES6 为new
命令引入了一个new.target
属性,该属性一般用在构造函数之中,返回new命令作用于的那个构造函数。如果构造函数不是通过new命令或Reflect.construct()
调用的,new.target
会返回undefined,因此这个属性可以用来确定构造函数是怎么调用的。
function Person(name) {
if (new.target !== undefined) {
this.name = name;
} else {
throw new Error('必须使用 new 命令生成实例');
}
}
// 另一种写法
function Person(name) {
if (new.target === Person) {
this.name = name;
} else {
throw new Error('必须使用 new 命令生成实例');
}
}
var person = new Person('张三'); // 正确
var notAPerson = Person.call(person, '张三'); // 报错
上面代码确保构造函数只能通过new
命令调用。
- Class 内部调用
new.target
,返回当前 Class。 - 需要注意的是,子类继承父类时,
new.target
会返回子类。
class Rectangle {
constructor(length, width) {
console.log(new.target === Rectangle);
// ...
}
}
class Square extends Rectangle {
constructor(length, width) {
super(length, width);
}
}
var obj = new Square(3); // 输出 false
- 利用这个特点,可以写出不能独立使用、必须继承后才能使用的类。
class Shape {
constructor() {
if (new.target === Shape) {
throw new Error('本类不能实例化');
}
}
}
class Rectangle extends Shape {
constructor(length, width) {
super();
// ...
}
}
var x = new Shape(); // 报错
var y = new Rectangle(3, 4); // 正确
上面代码中,Shape类不能被实例化,只能用于继承。
- 注意,在函数外部,使用
new.target
会报错。
Class 的继承
简介
- Class 可以通过
extends
关键字实现继承 super
关键字,它在这里表示父类的构造函数,用来新建父类的this
对象。- 子类必须在
constructor
方法中调用super
方法,否则新建实例时会报错。这是因为子类自己的this
对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后再对其进行加工,加上子类自己的实例属性和方法。如果不调用super
方法,子类就得不到this
对象。
class Point { /* ... */ }
class ColorPoint extends Point {
constructor() {
}
}
let cp = new ColorPoint(); // ReferenceError
- ES5 的继承,实质是先创造子类的实例对象
this
,然后再将父类的方法添加到this
上面(Parent.apply(this)
)。ES6 的继承机制完全不同,实质是先将父类实例对象的属性和方法,加到this
上面(所以必须先调用super
方法),然后再用子类的构造函数修改this
。 - 如果子类没有定义
constructor
方法,这个方法会被默认添加,代码如下。也就是说,不管有没有显式定义,任何一个子类都有constructor
方法。
class ColorPoint extends Point {
}
// 等同于
class ColorPoint extends Point {
constructor(...args) {
super(...args);
}
}
- 另一个需要注意的地方是,在子类的构造函数中,只有调用
super
之后,才可以使用this
关键字,否则会报错。这是因为子类实例的构建,基于父类实例,只有super
方法才能调用父类实例。
let cp = new ColorPoint(25, 8, 'green');
cp instanceof ColorPoint // true
cp instanceof Point // true
- 最后,父类的静态方法,也会被子类继承。
class A {
static hello() {
console.log('hello world');
}
}
class B extends A {
}
B.hello() // hello world
Object.getPrototypeOf()
Object.getPrototypeOf
方法可以用来从子类上获取父类。
Object.getPrototypeOf(ColorPoint) === Point
// true
因此,可以使用这个方法判断,一个类是否继承了另一个类。
super 关键字
super
这个关键字,既可以当作函数使用,也可以当作对象使用。- 第一种情况,
super
作为函数调用时,代表父类的构造函数。ES6 要求,子类的构造函数必须执行一次super
函数。
class A {}
class B extends A {
constructor() {
super();
}
}
- 注意,super虽然代表了父类A的构造函数,但是返回的是子类B的实例,即
super
内部的this
指的是B的实例,因此super()
在这里相当于A.prototype.constructor.call(this)
。 - 作为函数时,
super()
只能用在子类的构造函数之中,用在其他地方就会报错。 - 第二种情况,
super
作为对象时,在普通方法中,指向父类的原型对象;在静态方法中,指向父类。 - 第二种情况,
super
作为对象时,在普通方法中,指向父类的原型对象;在静态方法中,指向父类。
class A {
p() {
return 2;
}
}
class B extends A {
constructor() {
super();
console.log(super.p()); // 2
}
}
let b = new B();
这里需要注意,由于super
指向父类的原型对象,所以定义在父类实例上的方法或属性,是无法通过super
调用的。
class A {
constructor() {
this.p = 2;
}
}
class B extends A {
get m() {
return super.p;
}
}
let b = new B();
b.m // undefined
如果属性定义在父类的原型对象上,super
就可以取到。
class A {}
A.prototype.x = 2;
class B extends A {
constructor() {
super();
console.log(super.x) // 2
}
}
let b = new B();
- ES6 规定,在子类普通方法中通过
super
调用父类的方法时,方法内部的this
指向当前的子类实例。 - 由于
this
指向子类实例,所以如果通过super
对某个属性赋值,这时super
就是this
,赋值的属性会变成子类实例的属性。
class A {
constructor() {
this.x = 1;
}
}
class B extends A {
constructor() {
super();
this.x = 2;
super.x = 3;
console.log(super.x); // undefined
console.log(this.x); // 3
}
}
let b = new B();
上面代码中,super.x
赋值为3,这时等同于对this.x
赋值为3。而当读取super.x
的时候,读的是A.prototype.x
,所以返回undefined。
-
如果
super
作为对象,用在静态方法之中,这时super将指向父类,而不是父类的原型对象。
class Parent {
static myMethod(msg) {
console.log(‘static’, msg);
}
myMethod(msg) {
console.log(‘instance’, msg);
}
}
class Child extends Parent {
static myMethod(msg) {
super.myMethod(msg);
}myMethod(msg) {
super.myMethod(msg);
}
}
Child.myMethod(1); // static 1
var child = new Child();
child.myMethod(2); // instance 2
另外,在子类的静态方法中通过super调用父类的方法时,方法内部的this指向当前的子类,而不是子类的实例。
class A {
constructor() {
this.x = 1;
}
static print() {
console.log(this.x);
}
}
class B extends A {
constructor() {
super();
this.x = 2;
}
static m() {
super.print();
}
}
B.x = 3;
B.m() // 3
- 注意,使用
super
的时候,必须显式指定是作为函数、还是作为对象使用,否则会报错。
class A {}
class B extends A {
constructor() {
super();
console.log(super); // 报错
}
}
class A {}
class B extends A {
constructor() {
super();
console.log(super.valueOf() instanceof B); // true
}
}
let b = new B();
上面代码中,super.valueOf()
表明super
是一个对象,因此就不会报错。同时,由于super
使得this
指向B的实例,所以**super.valueOf()**
返回的是一个B的实例。
- 最后,由于对象总是继承其他对象的,所以可以在任意一个对象中,使用
super
关键字。
var obj = {
toString() {
return "MyObject: " + super.toString();
}
};
obj.toString(); // MyObject: [object Object]
类的 prototype 属性和__proto__属性
- Class 作为构造函数的语法糖,同时有
prototype
属性和__proto__
属性,因此同时存在两条继承链。
(1)子类的__proto__
属性,表示构造函数的继承,总是指向父类。
(2)子类prototype
属性的__proto__
属性,表示方法的继承,总是指向父类的prototype
属性。
这样的结果是因为,类的继承是按照下面的模式实现的。
class A {
}
class B {
}
// B 的实例继承 A 的实例
Object.setPrototypeOf(B.prototype, A.prototype);
// B 继承 A 的静态属性
Object.setPrototypeOf(B, A);
const b = new B();
Object.setPrototypeOf
方法的实现。
Object.setPrototypeOf = function (obj, proto) {
obj.__proto__ = proto;
return obj;
}
- 这两条继承链,可以这样理解:作为一个对象,子类(B)的原型(
__proto__
属性)是父类(A);作为一个构造函数,子类(B)的原型对象(prototype
属性)是父类的原型对象(prototype
属性)的实例。 - 下面,讨论两种情况。第一种,子类继承Object类。
class A extends Object {
}
A.__proto__ === Object // true
A.prototype.__proto__ === Object.prototype // true
这种情况下,A其实就是构造函数Object
的复制,A的实例就是Object
的实例。
- 第二种情况,不存在任何继承。
class A {
}
A.__proto__ === Function.prototype // true
A.prototype.__proto__ === Object.prototype // true
这种情况下,A作为一个基类(即不存在任何继承),就是一个普通函数,所以直接继承Function.prototype
。但是,A调用后返回一个空对象(即Object实例),所以A.prototype.__proto__
指向构造函数(Object)的prototype
属性。
实例的 proto 属性
- 子类实例的
__proto__
属性的__proto__
属性,指向父类实例的__proto__
属性。也就是说,子类的原型的原型,是父类的原型。
var p1 = new Point(2, 3);
var p2 = new ColorPoint(2, 3, 'red');
p2.__proto__ === p1.__proto__ // false
p2.__proto__.__proto__ === p1.__proto__ // true
上面代码中,ColorPoint继承了Point,导致前者原型的原型是后者的原型。
- 因此,通过子类实例的
__proto__.__proto__
属性,可以修改父类实例的行为。
p2.__proto__.__proto__.printName = function () {
console.log('Ha');
};
p1.printName() // "Ha"
原生构造函数的继承
- 原生构造函数是指语言内置的构造函数,通常用来生成数据结构。ECMAScript 的原生构造函数大致有下面这些。
Boolean()
Number()
String()
Array()
Date()
Function()
RegExp()
Error()
Object()
- 以前,这些原生构造函数是无法继承的,比如,不能自己定义一个Array的子类。
function MyArray() {
Array.apply(this, arguments);
}
MyArray.prototype = Object.create(Array.prototype, {
constructor: {
value: MyArray,
writable: true,
configurable: true,
enumerable: true
}
});
上面代码定义了一个继承 Array 的MyArray类。但是,这个类的行为与Array完全不一致。
var colors = new MyArray();
colors[0] = "red";
colors.length // 0
colors.length = 0;
colors[0] // "red"
- 之所以会发生这种情况,是因为子类无法获得原生构造函数的内部属性,通过
Array.apply()
或者分配给原型对象都不行。原生构造函数会忽略apply方法传入的this
,也就是说,原生构造函数的this
无法绑定,导致拿不到内部属性。 - ES5 是先新建子类的实例对象
this
,再将父类的属性添加到子类上,由于父类的内部属性无法获取,导致无法继承原生的构造函数。比如,Array构造函数有一个内部属性[[DefineOwnProperty]]
,用来定义新属性时,更新length
属性,这个内部属性无法在子类获取,导致子类的length
属性行为不正常。 - ES6 允许继承原生构造函数定义子类,因为 ES6 是先新建父类的实例对象
this
,然后再用子类的构造函数修饰this
,使得父类的所有行为都可以继承。下面是一个继承Array的例子。
class MyArray extends Array {
constructor(...args) {
super(...args);
}
}
var arr = new MyArray();
arr[0] = 12;
arr.length // 1
arr.length = 0;
arr[0] // undefined
上面代码定义了一个MyArray类,继承了Array构造函数,因此就可以从MyArray生成数组的实例。这意味着,ES6 可以自定义原生数据结构(比如Array、String等)的子类,这是 ES5 无法做到的。
- 注意,继承Object的子类,有一个行为差异。
class NewObj extends Object{
constructor(){
super(...arguments);
}
}
var o = new NewObj({attr: true});
o.attr === true // false
上面代码中,NewObj继承了Object,但是无法通过super
方法向父类Object传参。这是因为 ES6 改变了Object构造函数的行为,一旦发现Object方法不是通过new Object()
这种形式调用,ES6 规定Object构造函数会忽略参数。
Mixin 模式的实现
Mixin 指的是多个对象合成一个新的对象,新对象具有各个组成成员的接口。它的最简单实现如下。
const a = {
a: 'a'
};
const b = {
b: 'b'
};
const c = {...a, ...b}; // {a: 'a', b: 'b'}
Module 的语法
概述
- ES6 模块不是对象,而是通过
export
命令显式指定输出的代码,再通过import
命令输入。
// ES6模块
import { stat, exists, readFile } from 'fs';
上面代码的实质是从fs模块加载 3 个方法,其他方法不加载。这种加载称为“编译时加载”或者静态加载,即 ES6 可以在编译时就完成模块加载,效率要比 CommonJS 模块的加载方式高。当然,这也导致了没法引用 ES6 模块本身,因为它不是对象。
严格模式
- ES6 的模块自动采用严格模式,不管你有没有在模块头部加上
"use strict";
。 - 严格模式主要有以下限制。
变量必须声明后再使用
函数的参数不能有同名属性,否则报错
不能使用with语句
不能对只读属性赋值,否则报错
不能使用前缀 0 表示八进制数,否则报错
不能删除不可删除的属性,否则报错
不能删除变量delete prop,会报错,只能删除属性delete global[prop]
eval不会在它的外层作用域引入变量
eval和arguments不能被重新赋值
arguments不会自动反映函数参数的变化
不能使用arguments.callee
不能使用arguments.caller
禁止this指向全局对象
不能使用fn.caller和fn.arguments获取函数调用的堆栈
增加了保留字(比如protected、static和interface)
export 命令
- 模块功能主要由两个命令构成:
export
和import
。export
命令用于规定模块的对外接口,import
命令用于输入其他模块提供的功能。 - 一个模块就是一个独立的文件。该文件内部的所有变量,外部无法获取。如果你希望外部能够读取模块内部的某个变量,就必须使用
export
关键字输出该变量
// profile.js
export var firstName = 'Michael';
export var lastName = 'Jackson';
export var year = 1958;
上面代码是profile.js文件,保存了用户信息。ES6 将其视为一个模块,里面用export命令对外部输出了三个变量。
- export的写法,除了像上面这样,还有另外一种。
// profile.js
var firstName = 'Michael';
var lastName = 'Jackson';
var year = 1958;
export { firstName, lastName, year };
- 上面代码在
export
命令后面,使用大括号指定所要输出的一组变量。它与前一种写法(直接放置在var
语句前)是等价的,但是应该优先考虑使用这种写法。因为这样就可以在脚本尾部,一眼看清楚输出了哪些变量。 export
命令除了输出变量,还可以输出函数或类(class)。
export function multiply(x, y) {
return x * y;
};
上面代码对外输出一个函数multiply
。
- 通常情况下,
export
输出的变量就是本来的名字,但是可以使用as
关键字重命名。
function v1() { ... }
function v2() { ... }
export {
v1 as streamV1,
v2 as streamV2,
v2 as streamLatestVersion
};
上面代码使用as
关键字,重命名了函数v1和v2的对外接口。重命名后,v2可以用不同的名字输出两次。
- 需要特别注意的是,export命令规定的是对外的接口,必须与模块内部的变量建立一一对应关系。
// 报错
export 1;
// 报错
var m = 1;
export m;
上面两种写法都会报错,因为没有提供对外的接口。第一种写法直接输出 1,第二种写法通过变量m,还是直接输出 1。1只是一个值,不是接口。正确的写法是下面这样。
// 写法一
export var m = 1;
// 写法二
var m = 1;
export {m};
// 写法三
var n = 1;
export {n as m};
- 另外,
export
语句输出的接口,与其对应的值是动态绑定关系,即通过该接口,可以取到模块内部实时的值。
export var foo = 'bar';
setTimeout(() => foo = 'baz', 500);
上面代码输出变量foo
,值为bar
,500 毫秒之后变成baz
。
- 这一点与 CommonJS 规范完全不同。CommonJS 模块输出的是值的缓存,不存在动态更新。
- 最后,
export
命令可以出现在模块的任何位置,只要处于模块顶层就可以。如果处于块级作用域内,就会报错。这是因为处于条件代码块之中,就没法做静态优化了,违背了 ES6 模块的设计初衷。
function foo() {
export default 'bar' // SyntaxError
}
foo()
import 命令
- 使用
export
命令定义了模块的对外接口以后,其他 JS 文件就可以通过import
命令加载这个模块。
// main.js
import { firstName, lastName, year } from './profile.js';
- 如果想为输入的变量重新取一个名字,
import
命令要使用as关键字,将输入的变量重命名。
import { lastName as surname } from './profile.js';
import
命令输入的变量都是只读的,因为它的本质是输入接口。也就是说,不允许在加载模块的脚本里面,改写接口。- 但是,如果a是一个对象,改写a的属性是允许的。
import {a} from './xxx.js'
a.foo = 'hello'; // 合法操作
上面代码中,a的属性可以成功改写,并且其他模块也可以读到改写后的值。
import
后面的from
指定模块文件的位置,可以是相对路径,也可以是绝对路径,.js后缀可以省略。如果只是模块名,不带有路径,那么必须有配置文件,告诉 JavaScript 引擎该模块的位置。- 注意,
import
命令具有提升效果,会提升到整个模块的头部,首先执行。
foo();
import { foo } from 'my_module';
上面的代码不会报错,因为import的执行早于foo的调用。这种行为的本质是,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
语句会执行所加载的模块,因此可以有下面的写法。
import 'lodash';
- 上面代码仅仅执行
lodash
模块,但是不输入任何值。 - 如果多次重复执行同一句
import
语句,那么只会执行一次,而不会执行多次。
模块的整体加载
- 除了指定加载某个输出值,还可以使用整体加载,即用星号(*)指定一个对象,所有输出值都加载在这个对象上面。
- 注意,模块整体加载所在的那个对象(上例是circle),应该是可以静态分析的,所以不允许运行时改变。下面的写法都是不允许的。
import * as circle from './circle';
// 下面两行都是不允许的
circle.foo = 'hello';
circle.area = function () {};
export default 命令
- 这时就不需要知道原模块输出的函数名。需要注意的是,这时
import
命令后面,不使用大括号。 - export default命令用在非匿名函数前,也是可以的。
// export-default.js
export default function foo() {
console.log('foo');
}
// 或者写成
function foo() {
console.log('foo');
}
export default foo;
上面代码中,foo函数的函数名foo,在模块外部是无效的。加载的时候,视同匿名函数加载。
// 第一组
export default function crc32() { // 输出
// ...
}
import crc32 from 'crc32'; // 输入
// 第二组
export function crc32() { // 输出
// ...
};
import {crc32} from 'crc32'; // 输入
- 上面代码的两组写法,第一组是使用
export default
时,对应的import
语句不需要使用大括号;第二组是不使用export default
时,对应的import
语句需要使用大括号。 export default
命令用于指定模块的默认输出。显然,一个模块只能有一个默认输出,因此export default
命令只能使用一次。所以,import
命令后面才不用加大括号,因为只可能唯一对应export 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';
- 正是因为
export default
命令其实只是输出一个叫做default
的变量,所以它后面不能跟变量声明语句。
// 正确
export var a = 1;
// 正确
var a = 1;
export default a;
// 错误
export default var a = 1;
上面代码中,export default a
的含义是将变量a的值赋给变量default
。所以,最后一种写法会报错。
- 同样地,因为
export default
命令的本质是将后面的值,赋给default变量,所以可以直接将一个值写在export default
之后。
// 正确
export default 42;
// 报错
export 42;
- 如果想在一条import语句中,同时输入默认方法和其他接口,可以写成下面这样。
import _, { each, forEach } from 'lodash';
export default function (obj) {
// ···
}
export function each(obj, iterator, context) {
// ···
}
export { each as forEach };
- 上面代码的最后一行的意思是,暴露出
forEach
接口,默认指向each
接口,即forEach
和each
指向同一个方法。
* export default
也可以用来输出类。
// MyClass.js
export default class { ... }
// main.js
import MyClass from 'MyClass';
let o = new MyClass();
export 与 import 的复合写法
- 如果在一个模块之中,先输入后输出同一个模块,
import
语句可以与export
语句写在一起。
export { foo, bar } from 'my_module';
// 可以简单理解为
import { foo, bar } from 'my_module';
export { foo, bar };
- 上面代码中,
export
和import
语句可以结合在一起,写成一行。但需要注意的是,写成一行以后,foo
和bar
实际上并没有被导入当前模块,只是相当于对外转发了这两个接口,导致当前模块不能直接使用foo
和bar
。 - 默认接口的写法如下。
export { default } from 'foo';
- 具名接口改为默认接口的写法如下。
export { es6 as default } from './someModule';
// 等同于
import { es6 } from './someModule';
export default es6;
- 同样地,默认接口也可以改名为具名接口。
export { default as es6 } from './someModule';
- ES2020 之前,有一种
import
语句,没有对应的复合写法。
import * as someIdentifier from "someModule";
- ES2020补上了这个写法。
export * as ns from "mod";
// 等同于
import * as ns from "mod";
export {ns};
模块的继承
跨模块常量
如果想设置跨模块的常量(即跨多个文件),或者说一个值要被多个模块共享,可以采用下面的写法。
// constants.js 模块
export const A = 1;
export const B = 3;
export const C = 4;
import()
简介
- ES2020提案 引入
import()
函数,支持动态加载模块。
import(specifier)
上面代码中,import
函数的参数specifier,指定所要加载的模块的位置。import
命令能够接受什么参数,import()
函数就能接受什么参数,两者区别主要是后者为动态加载。
import()
返回一个 Promise 对象。
适用场合
(1)按需加载。
(2)条件加载
import()
可以放在if代码块,根据不同的情况,加载不同的模块。
(3)动态的模块路径
import()
允许模块路径动态生成。
import(f())
.then(...);
注意点
import()
加载模块成功以后,这个模块会作为一个对象,当作then
方法的参数。因此,可以使用对象解构赋值的语法,获取输出接口。- 如果模块有
default
输出接口,可以用参数直接获得。
import('./myModule.js')
.then(myModule => {
console.log(myModule.default);
});
- 上面的代码也可以使用具名输入的形式。
import('./myModule.js')
.then(({default: theDefault}) => {
console.log(theDefault);
});
如果想同时加载多个模块,可以采用下面的写法。
Promise.all([
import('./module1.js'),
import('./module2.js'),
import('./module3.js'),
])
.then(([module1, module2, module3]) => {
···
});
import()
也可以用在async
函数之中。
async function main() {
const myModule = await import('./myModule.js');
const {export1, export2} = await import('./myModule.js');
const [module1, module2, module3] =
await Promise.all([
import('./module1.js'),
import('./module2.js'),
import('./module3.js'),
]);
}
main();
Module 的加载实现
浏览器加载
传统方法
- HTML 网页中,浏览器通过
<script>
标签加载 JavaScript 脚本。 defer
与async
的区别是:defer
要等到整个页面在内存中正常渲染结束(DOM 结构完全生成,以及其他脚本执行完成),才会执行;async
一旦下载完,渲染引擎就会中断渲染,执行这个脚本以后,再继续渲染。一句话,defer
是“渲染完再执行”,async
是“下载完就执行”。另外,如果有多个defer
脚本,会按照它们在页面出现的顺序加载,而多个async
脚本是不能保证加载顺序的。
加载规则
- 浏览器加载 ES6 模块,也使用
<script>
标签,但是要加入type="module"
属性。 - 浏览器对于带有
type="module"
的<script>
,都是异步加载,不会造成堵塞浏览器,即等到整个页面渲染完,再执行模块脚本,等同于打开了<script>
标签的defer
属性。 - 如果网页有多个
<script type="module">
,它们会按照在页面出现的顺序依次执行。 - 一旦使用了
async
属性,<script type="module">
就不会按照在页面出现的顺序执行,而是只要该模块加载完成,就执行该模块。 - 对于外部的模块脚本,有几点需要注意。
1、代码是在模块作用域之中运行,而不是在全局作用域运行。模块内部的顶层变量,外部不可见。
2、模块脚本自动采用严格模式,不管有没有声明use strict
。
3、模块之中,可以使用import
命令加载其他模块(.js
后缀不可省略,需要提供绝对 URL 或相对 URL),也可以使用export
命令输出对外接口。
4、模块之中,顶层的this
关键字返回undefined,而不是指向window。也就是说,在模块顶层使用this
关键字,是无意义的。
5、同一个模块如果加载多次,将只执行一次。
ES6 模块与 CommonJS 模块的差异
- 它们有三个重大差异。
1、CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
2、CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。
3、CommonJS 模块的require()
是同步加载模块,ES6 模块的import
命令是异步加载,有一个独立的模块依赖的解析阶段。 - 第二个差异是因为 CommonJS 加载的是一个对象(即
module.exports
属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。 - CommonJS 模块输出的是值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。
- ES6 模块的运行机制与 CommonJS 不一样。JS 引擎对脚本静态分析的时候,遇到模块加载命令
import
,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。 export
通过接口,输出的是同一个值。不同的脚本加载这个接口,得到的都是同样的实例。
Node.js 的模块加载方法
概述
package.json 的 main 字段
package.json
文件有两个字段可以指定模块的入口文件:main
和exports
。比较简单的模块,可以只使用main
字段,指定模块加载的入口文件。
package.json 的 exports 字段
exports
字段的优先级高于main
字段。它有多种用法。
(1)子目录别名
package.json
文件的exports
字段可以指定脚本或子目录的别名。
如果没有指定别名,就不能用“模块+脚本名”这种形式加载脚本。
(2)main
的别名
exports
字段的别名如果是.
,就代表模块的主入口,优先级高于main
字段,并且可以直接简写成exports
字段的值。
(3)条件加载
CommonJS 模块加载 ES6 模块
- CommonJS 的
require()
命令不能加载 ES6 模块,会报错,只能使用import()
这个方法加载。
ES6 模块加载 CommonJS 模块
- ES6 模块的
import
命令可以加载 CommonJS 模块,但是只能整体加载,不能只加载单一的输出项。
同时支持两种格式的模块
Node.js 的内置模块
加载路径
- ES6 模块的加载路径必须给出脚本的完整路径,不能省略脚本的后缀名。
import
命令和package.json
文件的main
字段如果省略脚本的后缀名,会报错。
内部变量
- ES6 模块之中,顶层的
this
指向undefined;CommonJS 模块的顶层this
指向当前模块,这是两者的一个重大差异。 - 以下这些顶层变量在 ES6 模块之中都是不存在的。
arguments
require
module
exports
__filename
__dirname
循环加载
- “循环加载”(circular dependency)指的是,a脚本的执行依赖b脚本,而b脚本的执行又依赖a脚本。
CommonJS 模块的加载原理
CommonJS 的一个模块,就是一个脚本文件。require
命令第一次加载该脚本,就会执行整个脚本,然后在内存生成一个对象。
CommonJS 模块的循环加载
ES6 模块的循环加载
编程风格
块级作用域
(1)let 取代 var
(2)全局常量和线程安全
- 在let和const之间,建议优先使用const,尤其是在全局环境,不应该设置变量,只应设置常量。
字符串
- 静态字符串一律使用单引号或反引号,不使用双引号。动态字符串使用反引号。
解构赋值
- 使用数组成员对变量赋值时,优先使用解构赋值。
- 函数的参数如果是对象的成员,优先使用解构赋值。
- 如果函数返回多个值,优先使用对象的解构赋值,而不是数组的解构赋值。这样便于以后添加返回值,以及更改返回值的顺序。
对象
- 单行定义的对象,最后一个成员不以逗号结尾。多行定义的对象,最后一个成员以逗号结尾。
- 对象尽量静态化,一旦定义,就不得随意添加新的属性。如果添加属性不可避免,要使用Object.assign方法。
- 如果对象的属性名是动态的,可以在创造对象的时候,使用属性表达式定义。
- 对象的属性和方法,尽量采用简洁表达法,这样易于描述和书写。
数组
- 使用扩展运算符(…)拷贝数组。
- 使用 Array.from 方法,将类似数组的对象转为数组。
函数
- 立即执行函数可以写成箭头函数的形式。
- 那些使用匿名函数当作参数的场合,尽量用箭头函数代替。因为这样更简洁,而且绑定了 this。
- 箭头函数取代Function.prototype.bind,不应再用 self/_this/that 绑定 this。
- 简单的、单行的、不会复用的函数,建议采用箭头函数。如果函数体较为复杂,行数较多,还是应该采用传统的函数写法。
- 所有配置项都应该集中在一个对象,放在最后一个参数,布尔值不可以直接作为参数。
- 不要在函数体内使用 arguments 变量,使用 rest 运算符(…)代替。因为 rest 运算符显式表明你想要获取参数,而且 arguments 是一个类似数组的对象,而 rest 运算符可以提供一个真正的数组。
- 使用默认值语法设置函数参数的默认值。
Map 结构
- 注意区分 Object 和 Map,只有模拟现实世界的实体对象时,才使用 Object。如果只是需要key: value的数据结构,使用 Map 结构。因为 Map 有内建的遍历机制。
Class
- 总是用 Class,取代需要 prototype 的操作。因为 Class 的写法更简洁,更易于理解。
- 使用extends实现继承,因为这样更简单,不会有破坏instanceof运算的危险。
模块
ESLint 的使用
读懂 ECMAScript 规格
概述
术语
抽象操作
Record 和 field
[[Notation]]
Completion Record
抽象操作的标准流程
- 抽象操作的运行流程,一般是下面这样。
1、Let result be AbstractOp().
2、If result is an abrupt completion, return result.
3、Set result to result.[[Value]].
4、return result.
上面的第一步调用了抽象操作AbstractOp()
,得到result,这是一个Completion Record
。第二步,如果result
属于abrupt completion
,就直接返回。如果此处没有返回,表示result属于normal completion
。第三步,将result
的值设置为resultCompletionRecord.[[Value]]
。第四步,返回result
。
相等运算符
1、如果x不是正常值(比如抛出一个错误),中断执行。
2、如果y不是正常值,中断执行。
3、如果Type(x)
与Type(y)
相同,执行严格相等运算x === y
。
4、如果x是null,y是undefined,返回true
。
5、如果x是undefined,y是null,返回true
。
6、如果Type(x)
是数值,Type(y)
是字符串,返回x == ToNumber(y)
的结果。
7、如果Type(x)
是字符串,Type(y)
是数值,返回ToNumber(x) == y
的结果。
8、如果Type(x)
是布尔值,返回ToNumber(x) == y
的结果。
9、如果Type(y)
是布尔值,返回x == ToNumber(y)
的结果。
10、如果Type(x)
是字符串或数值或Symbol
值,Type(y)
是对象,返回x == ToPrimitive(y)
的结果。
11、如果Type(x)
是对象,Type(y)
是字符串或数值或Symbol值,返回ToPrimitive(x) == y
的结果。
12、返回false
。
0 == null // false
数组的空位
const a1 = [undefined, undefined, undefined];
const a2 = [, , ,];
a1.length // 3
a2.length // 3
a1[0] // undefined
a2[0] // undefined
a1[0] === a2[0] // true
- 但是,它们实际上存在重大差异。
0 in a1 // true
0 in a2 // false
a1.hasOwnProperty(0) // true
a2.hasOwnProperty(0) // false
Object.keys(a1) // ["0", "1", "2"]
Object.keys(a2) // []
a1.map(n => 1) // [1, 1, 1]
a2.map(n => 1) // [, , ,]
- "数组成员可以省略。只要逗号前面没有任何表达式,数组的length属性就会加 1,并且相应增加其后成员的位置索引。被省略的成员不会被定义。如果被省略的成员是数组最后一个成员,则不会导致数组length属性增加。”
数组的空位会反映在length属性,也就是说空位有自己的位置,但是这个位置的值是未定义,即这个值是不存在的。如果一定要读取,结果就是undefined - 这就解释了为什么
in
运算符、数组的hasOwnProperty
方法、Object.keys
方法,都取不到空位的属性名。因为这个属性名根本就不存在,规格里面没说要为空位分配属性名(位置索引),只说要为下一个元素的位置索引加 1。
数组的 map 方法
- 当处理一个数组时,
map
会跳过空位
异步遍历器
同步遍历器的问题
- Iterator 协议里面
next()
方法只能包含同步操作。 - 目前的解决方法是,将异步操作包装成
Thunk
函数或者Promise
对象,即next()
方法返回值的value
属性是一个Thunk
函数或者Promise
对象,等待以后返回真正的值,而done
属性则还是同步产生的。
function idMaker() {
let index = 0;
return {
next: function() {
return {
value: new Promise(resolve => setTimeout(() => resolve(index++), 1000)),
done: false
};
}
};
}
const it = idMaker();
it.next().value.then(o => console.log(o)) // 1
it.next().value.then(o => console.log(o)) // 2
异步遍历的接口
- 异步遍历器的最大的语法特点,就是调用遍历器的
next
方法,返回的是一个 Promise 对象。
asyncIterator
.next()
.then(
({ value, done }) => /* ... */
);
- 一个对象的同步遍历器的接口,部署在
Symbol.iterator
属性上面。同样地,对象的异步遍历器接口,部署在Symbol.asyncIterator
属性上面。不管是什么样的对象,只要它的Symbol.asyncIterator
属性有值,就表示应该对它进行异步遍历。 - 注意,异步遍历器的
next
方法是可以连续调用的,不必等到上一步产生的 Promise 对象resolve
以后再调用。这种情况下,next
方法会累积起来,自动按照每一步的顺序运行下去。
## for await...of * `for...of`循环用于遍历同步的 Iterator 接口。新引入的`for await...of`循环,则是用于遍历异步的 Iterator 接口。 ``` async function f() { for await (const x of createAsyncIterable(['a', 'b'])) { console.log(x); } } // a // b ``` 上面代码中,`createAsyncIterable()`返回一个拥有异步遍历器接口的对象,`for...of`循环自动调用这个对象的异步遍历器的`next`方法,会得到一个 Promise 对象。`await`用来处理这个 Promise 对象,一旦`resolve`,就把得到的值(x)传入`for...of`的循环体。 如果`next`方法返回的 Promise 对象被`reject`,`for await...of`就会报错,要用`try...catch`捕捉。 * 注意,`for await...of`循环也可以用于同步遍历器。
异步 Generator 函数
- 在语法上,异步 Generator 函数就是
async
函数与 Generator 函数的结合。
async function* gen() {
yield 'hello';
}
const genObj = gen();
genObj.next().then(x => console.log(x));
// { value: 'hello', done: false }
上面代码中,gen
是一个异步 Generator 函数,执行后返回一个异步 Iterator 对象。对该对象调用next
方法,返回一个 Promise 对象。
// 同步 Generator 函数
function* map(iterable, func) {
const iter = iterable[Symbol.iterator]();
while (true) {
const {value, done} = iter.next();
if (done) break;
yield func(value);
}
}
// 异步 Generator 函数
async function* map(iterable, func) {
const iter = iterable[Symbol.asyncIterator]();
while (true) {
const {value, done} = await iter.next();
if (done) break;
yield func(value);
}
}
yield* 语句
ArrayBuffer
- 简单说,ArrayBuffer对象代表原始的二进制数据,TypedArray视图用来读写简单类型的二进制数据,DataView视图用来读写复杂类型的二进制数据。
- 注意,二进制数组并不是真正的数组,而是类似数组的对象。
- 很多浏览器操作的 API,用到了二进制数组操作二进制数据,下面是其中的几个。
Canvas
Fetch API
File API
WebSockets
XMLHttpRequest
ArrayBuffer 对象
概述
ArrayBuffer
对象代表储存二进制数据的一段内存,它不能直接读写,只能通过视图(TypedArray
视图和DataView
视图)来读写,视图的作用是以指定格式解读二进制数据。ArrayBuffer
也是一个构造函数,可以分配一段可以存放数据的连续内存区域。
const buf = new ArrayBuffer(32);
上面代码生成了一段 32 字节的内存区域,每个字节的值默认都是 0。可以看到,ArrayBuffer
构造函数的参数是所需要的内存大小(单位字节)。
- 为了读写这段内容,需要为它指定视图。
DataView
视图的创建,需要提供ArrayBuffer
对象实例作为参数。
const buf = new ArrayBuffer(32);
const dataView = new DataView(buf);
dataView.getUint8(0) // 0
上面代码对一段 32 字节的内存,建立DataView
视图,然后以不带符号的 8 位整数格式,从头读取 8 位二进制数据,结果得到 0,因为原始内存的ArrayBuffer
对象,默认所有位都是 0。
- 另一种
TypedArray
视图,与DataView
视图的一个区别是,它不是一个构造函数,而是一组构造函数,代表不同的数据格式。
const buffer = new ArrayBuffer(12);
const x1 = new Int32Array(buffer);
x1[0] = 1;
const x2 = new Uint8Array(buffer);
x2[0] = 2;
x1[0] // 2
上面代码对同一段内存,分别建立两种视图:32 位带符号整数(Int32Array
构造函数)和 8 位不带符号整数(Uint8Array
构造函数)。由于两个视图对应的是同一段内存,一个视图修改底层内存,会影响到另一个视图。
TypedArray
视图的构造函数,除了接受ArrayBuffer
实例作为参数,还可以接受普通数组作为参数,直接分配内存生成底层的ArrayBuffer
实例,并同时完成对这段内存的赋值。
const typedArray = new Uint8Array([0,1,2]);
typedArray.length // 3
typedArray[0] = 5;
typedArray // [5, 1, 2]
ArrayBuffer.prototype.byteLength
ArrayBuffer
实例的byteLength
属性,返回所分配的内存区域的字节长度。
const buffer = new ArrayBuffer(32);
buffer.byteLength
// 32
- 如果要分配的内存区域很大,有可能分配失败(因为没有那么多的连续空余内存),所以有必要检查是否分配成功。
if (buffer.byteLength === n) {
// 成功
} else {
// 失败
}
ArrayBuffer.prototype.slice()
ArrayBuffer
实例有一个slice
方法,允许将内存区域的一部分,拷贝生成一个新的ArrayBuffer
对象。
const buffer = new ArrayBuffer(8);
const newBuffer = buffer.slice(0, 3);
- 上面代码拷贝
buffer
对象的前 3 个字节(从 0 开始,到第 3 个字节前面结束),生成一个新的ArrayBuffer
对象。slice
方法其实包含两步,第一步是先分配一段新内存,第二步是将原来那个ArrayBuffer
对象拷贝过去。 - 除了
slice
方法,ArrayBuffer
对象不提供任何直接读写内存的方法,只允许在其上方建立视图,然后通过视图读写。
ArrayBuffer.isView()
ArrayBuffer
有一个静态方法isView
,返回一个布尔值,表示参数是否为ArrayBuffer
的视图实例。这个方法大致相当于判断参数,是否为TypedArray
实例或DataView
实例。
const buffer = new ArrayBuffer(8);
ArrayBuffer.isView(buffer) // false
const v = new Int32Array(buffer);
ArrayBuffer.isView(v) // true
TypedArray 视图
概述
- 目前,
TypedArray
视图一共包括 9 种类型,每一种视图都是一种构造函数。 - 普通数组与
TypedArray
数组的差异主要在以下方面。
1、TypedArray
数组的所有成员,都是同一种类型。
2、TypedArray
数组的成员是连续的,不会有空位。
3、TypedArray
数组成员的默认值为 0。比如,new Array(10)
返回一个普通数组,里面没有任何成员,只是 10 个空位;new Uint8Array(10)
返回一个TypedArray
数组,里面 10 个成员都是 0。
4、TypedArray
数组只是一层视图,本身不储存数据,它的数据都储存在底层的ArrayBuffer
对象之中,要获取底层对象必须使用buffer
属性。
构造函数
(1)TypedArray(buffer, byteOffset=0, length?)
- 同一个
ArrayBuffer
对象之上,可以根据不同的数据类型,建立多个视图。
// 创建一个8字节的ArrayBuffer
const b = new ArrayBuffer(8);
// 创建一个指向b的Int32视图,开始于字节0,直到缓冲区的末尾
const v1 = new Int32Array(b);
// 创建一个指向b的Uint8视图,开始于字节2,直到缓冲区的末尾
const v2 = new Uint8Array(b, 2);
// 创建一个指向b的Int16视图,开始于字节2,长度为2
const v3 = new Int16Array(b, 2, 2);
- 视图的构造函数可以接受三个参数:
第一个参数(必需):视图对应的底层ArrayBuffer
对象。
第二个参数(可选):视图开始的字节序号,默认从 0 开始。
第三个参数(可选):视图包含的数据个数,默认直到本段内存区域结束。 - 注意,
byteOffset
必须与所要建立的数据类型一致,否则会报错。
const buffer = new ArrayBuffer(8);
const i16 = new Int16Array(buffer, 1);
// Uncaught RangeError: start offset of Int16Array should be a multiple of 2
上面代码中,新生成一个 8 个字节的ArrayBuffer
对象,然后在这个对象的第一个字节,建立带符号的 16 位整数视图,结果报错。因为,带符号的 16 位整数需要两个字节,所以byteOffset
参数必须能够被 2 整除。
(2)TypedArray(length)
- 视图还可以不通过
ArrayBuffer
对象,直接分配内存而生成。
const f64a = new Float64Array(8);
f64a[0] = 10;
f64a[1] = 20;
f64a[2] = f64a[0] + f64a[1];
上面代码生成一个 8 个成员的Float64Array
数组(共 64 字节),然后依次对每个成员赋值。这时,视图构造函数的参数就是成员的个数。
(3)TypedArray(typedArray)
TypedArray
数组的构造函数,可以接受另一个TypedArray
实例作为参数。
const typedArray = new Int8Array(new Uint8Array(4));
上面代码中,Int8Array
构造函数接受一个Uint8Array
实例作为参数。
- 注意,此时生成的新数组,只是复制了参数数组的值,对应的底层内存是不一样的。新数组会开辟一段新的内存储存数据,不会在原数组的内存之上建立视图。
如果想基于同一段内存,构造不同的视图,可以采用下面的写法。
const x = new Int8Array([1, 1]);
const y = new Int8Array(x.buffer);
x[0] // 1
y[0] // 1
x[0] = 2;
y[0] // 2
(4)TypedArray(arrayLikeObject)
构造函数的参数也可以是一个普通数组,然后直接生成TypedArray
实例。
const typedArray = new Uint8Array([1, 2, 3, 4]);
- 注意,这时
TypedArray
视图会重新开辟内存,不会在原数组的内存上建立视图。上面代码从一个普通的数组,生成一个 8 位无符号整数的TypedArray
实例。 TypedArray
数组也可以转换回普通数组。
const normalArray = [...typedArray];
// or
const normalArray = Array.from(typedArray);
// or
const normalArray = Array.prototype.slice.call(typedArray);
数组方法
- 注意,
TypedArray
数组没有concat
方法。 - 另外,
TypedArray
数组与普通数组一样,部署了 Iterator 接口,所以可以被遍历。
字节序
- 字节序指的是数值在内存中的表示方式。
const buffer = new ArrayBuffer(16);
const int32View = new Int32Array(buffer);
for (let i = 0; i < int32View.length; i++) {
int32View[i] = i * 2;
}
上面代码生成一个 16 字节的ArrayBuffer
对象,然后在它的基础上,建立了一个 32 位整数的视图。由于每个 32 位整数占据 4 个字节,所以一共可以写入 4 个整数,依次为 0,2,4,6。
如果在这段数据上接着建立一个 16 位整数的视图,则可以读出完全不一样的结果。
const int16View = new Int16Array(buffer);
for (let i = 0; i < int16View.length; i++) {
console.log("Entry " + i + ": " + int16View[i]);
}
// Entry 0: 0
// Entry 1: 0
// Entry 2: 2
// Entry 3: 0
// Entry 4: 4
// Entry 5: 0
// Entry 6: 6
// Entry 7: 0
- 由于每个 16 位整数占据 2 个字节,所以整个
ArrayBuffer
对象现在分成 8 段。然后,由于 x86 体系的计算机都采用小端字节序(little endian),相对重要的字节排在后面的内存地址,相对不重要字节排在前面的内存地址,所以就得到了上面的结果。
BYTES_PER_ELEMENT 属性
- 每一种视图的构造函数,都有一个
BYTES_PER_ELEMENT
属性,表示这种数据类型占据的字节数。 - 这个属性在
TypedArray
实例上也能获取,即TypedArray.prototype.BYTES_PER_ELEMENT
。
ArrayBuffer 与字符串的互相转换
溢出
TypedArray.prototype.buffer
TypedArray
实例的buffer
属性,返回整段内存区域对应的ArrayBuffer
对象。该属性为只读属性。
TypedArray.prototype.byteLength,TypedArray.prototype.byteOffset
byteLength
属性返回TypedArray
数组占据的内存长度,单位为字节。byteOffset
属性返回TypedArray
数组从底层ArrayBuffer
对象的哪个字节开始。这两个属性都是只读属性。
TypedArray.prototype.length
length
属性表示TypedArray
数组含有多少个成员。注意将length
属性和byteLength
属性区分,前者是成员长度,后者是字节长度。
TypedArray.prototype.set()
TypedArray
数组的set
方法用于复制数组(普通数组或TypedArray
数组),也就是将一段内容完全复制到另一段内存。
const a = new Uint8Array(8);
const b = new Uint8Array(8);
b.set(a);
上面代码复制a数组的内容到b数组,它是整段内存的复制,比一个个拷贝成员的那种复制快得多。
set
方法还可以接受第二个参数,表示从b对象的哪一个成员开始复制a对象。
const a = new Uint16Array(8);
const b = new Uint16Array(10);
b.set(a, 2)
上面代码的b数组比a数组多两个成员,所以从b[2]开始复制。
TypedArray.prototype.subarray()
subarray
方法是对于TypedArray
数组的一部分,再建立一个新的视图。
const a = new Uint16Array(8);
const b = a.subarray(2,3);
a.byteLength // 16
b.byteLength // 2
subarray
方法的第一个参数是起始的成员序号,第二个参数是结束的成员序号(不含该成员),如果省略则包含剩余的全部成员。所以,上面代码的a.subarray(2,3)
,意味着 b 只包含a[2]一个成员,字节长度为 2。
TypedArray.prototype.slice()
TypeArray
实例的slice
方法,可以返回一个指定位置的新的TypedArray
实例。
let ui8 = Uint8Array.of(0, 1, 2);
ui8.slice(-1)
// Uint8Array [ 2 ]
上面代码中,ui8是 8 位无符号整数数组视图的一个实例。它的slice
方法可以从当前视图之中,返回一个新的视图实例。
slice
方法的参数,表示原数组的具体位置,开始生成新数组。负值表示逆向的位置,即-1 为倒数第一个位置,-2 表示倒数第二个位置,以此类推。
TypedArray.of()
TypedArray
数组的所有构造函数,都有一个静态方法of
,用于将参数转为一个TypedArray
实例。
Float32Array.of(0.151, -8, 3.7)
// Float32Array [ 0.151, -8, 3.7 ]
- 下面三种方法都会生成同样一个
TypedArray
数组。
// 方法一
let tarr = new Uint8Array([1,2,3]);
// 方法二
let tarr = Uint8Array.of(1,2,3);
// 方法三
let tarr = new Uint8Array(3);
tarr[0] = 1;
tarr[1] = 2;
tarr[2] = 3;
TypedArray.from()
- 静态方法
from
接受一个可遍历的数据结构(比如数组)作为参数,返回一个基于这个结构的TypedArray
实例。 from
方法还可以接受一个函数,作为第二个参数,用来对每个元素进行遍历,功能类似map
方法。
复合视图
由于视图的构造函数可以指定起始位置和长度,所以在同一段内存之中,可以依次存放不同类型的数据,这叫做“复合视图”。
const buffer = new ArrayBuffer(24);
const idView = new Uint32Array(buffer, 0, 1);
const usernameView = new Uint8Array(buffer, 4, 16);
const amountDueView = new Float32Array(buffer, 20, 1);
上面代码将一个 24 字节长度的ArrayBuffer
对象,分成三个部分:
1、字节 0 到字节 3:1 个 32 位无符号整数
2、字节 4 到字节 19:16 个 8 位整数
3、字节 20 到字节 23:1 个 32 位浮点数
DataView 视图
二进制数组的应用
AJAX
- 传统上,服务器通过 AJAX 操作只能返回文本数据,即
responseType
属性默认为text
。* *XMLHttpRequest
第二版XHR2允许服务器返回二进制数据,这时分成两种情况。如果明确知道返回的二进制数据类型,可以把返回类型(responseType
)设为arraybuffer
;如果不知道,就设为blob
。
Canvas
- Canvas
网页Canvas元素输出的二进制像素数据,就是TypedArray
数组。
const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const uint8ClampedArray = imageData.data;
WebSocket
- WebSocket可以通过
ArrayBuffer
,发送或接收二进制数据。
let socket = new WebSocket('ws://127.0.0.1:8081');
socket.binaryType = 'arraybuffer';
// Wait until socket is open
socket.addEventListener('open', function (event) {
// Send binary data
const typedArray = new Uint8Array(4);
socket.send(typedArray.buffer);
});
// Receive binary data
socket.addEventListener('message', function (event) {
const arrayBuffer = event.data;
// ···
});
Fetch API
- Fetch API 取回的数据,就是
ArrayBuffer
对象。
File API
如果知道一个文件的二进制数据类型,也可以将这个文件读取为ArrayBuffer
对象。
SharedArrayBuffer
- ES2017 引入
SharedArrayBuffer
,允许 Worker 线程与主线程共享同一块内存。SharedArrayBuffer
的 API 与ArrayBuffer
一模一样,唯一的区别是后者无法共享数据。
// 主线程
// 新建 1KB 共享内存
const sharedBuffer = new SharedArrayBuffer(1024);
// 主线程将共享内存的地址发送出去
w.postMessage(sharedBuffer);
// 在共享内存上建立视图,供写入数据
const sharedArray = new Int32Array(sharedBuffer);
Atomics 对象
(1)Atomics.store(),Atomics.load()
store()
方法用来向共享内存写入数据,load()
方法用来从共享内存读出数据。比起直接的读写操作,它们的好处是保证了读写操作的原子性。
(2)Atomics.exchange()
- Worker 线程如果要写入数据,可以使用上面的
Atomics.store()
方法,也可以使用Atomics.exchange()
方法。它们的区别是,Atomics.store()
返回写入的值,而Atomics.exchange()
返回被替换的值。
(3)Atomics.wait(),Atomics.wake()
- 使用
while
循环等待主线程的通知,不是很高效,如果用在主线程,就会造成卡顿,Atomics对象提供了wait()
和wake()
两个方法用于等待通知。这两个方法相当于锁内存,即在一个线程进行操作时,让其他线程休眠(建立锁),等到操作结束,再唤醒那些休眠的线程(解除锁)。
(4)运算方法
(5)其他方法
最新提案
do 表达式
- 本质上,块级作用域是一个语句,将多个操作封装在一起,没有返回值。
- 现在有一个提案,使得块级作用域可以变为表达式,也就是说可以返回值,办法就是在块级作用域之前加上
do
,使它变为do
表达式,然后就会返回内部最后执行的表达式的值。
let x = do {
let t = f();
t * t + 1;
};
- 上面代码中,变量x会得到整个块级作用域的返回值
(t * t + 1)
。 do
表达式的逻辑非常简单:封装的是什么,就会返回什么。
// 等同于 <表达式>
do { <表达式>; }
// 等同于 <语句>
do { <语句> }
do
表达式的好处是可以封装多个语句,让程序更加模块化,就像乐高积木那样一块块拼装起来。do
块级作用域提供了单独的作用域,内部操作可以与全局作用域隔绝。
throw 表达式
- JavaScript 语法规定
throw
是一个命令,用来抛出错误,不能用于表达式之中。 - 现在有一个提案,允许
throw
用于表达式。
函数的部分执行
语法
- 多参数的函数有时需要绑定其中的一个或多个参数,然后返回一个新函数。
function add(x, y) { return x + y; }
function add7(x) { return x + 7; }
- 上面代码中,add7函数其实是add函数的一个特殊版本,通过将一个参数绑定为7,就可以从add得到add7。
// bind 方法
const add7 = add.bind(null, 7);
// 箭头函数
const add7 = x => add(x, 7);
- 现在有一个提案,使得绑定参数并返回一个新函数更加容易。这叫做函数的部分执行(partial application)。
const add = (x, y) => x + y;
const addOne = add(1, ?);
const maxGreaterThanZero = Math.max(0, ...);
- 根据新提案,?是单个参数的占位符,…是多个参数的占位符。?和…只能出现在函数的调用之中,并且会返回一个新函数。
注意点
(1)函数的部分执行是基于原函数的。如果原函数发生变化,部分执行生成的新函数也会立即反映这种变化。
let f = (x, y) => x + y;
const g = f(?, 3);
g(1); // 4
// 替换函数 f
f = (x, y) => x * y;
g(1); // 3
(2)如果预先提供的那个值是一个表达式,那么这个表达式并不会在定义时求值,而是在每次调用时求值。
let a = 3;
const f = (x, y) => x + y;
const g = f(?, a);
g(1); // 4
// 改变 a 的值
a = 10;
g(1); // 11
(3)如果新函数的参数多于占位符的数量,那么多余的参数将被忽略。
(4)…只会被采集一次,如果函数的部分执行使用了多个…,那么每个…的值都将相同。
管道运算符
- JavaScript 的管道是一个运算符,写作
|>
。它的左边是一个表达式,右边是一个函数。管道运算符把左边表达式的值,传入右边的函数进行求值。
x |> f
// 等同于
f(x)
- 管道运算符最大的好处,就是可以把嵌套的函数,写成从左到右的链式表达式。
function doubleSay (str) {
return str + ", " + str;
}
function capitalize (str) {
return str[0].toUpperCase() + str.substring(1);
}
function exclaim (str) {
return str + '!';
}
- 上面是三个简单的函数。如果要嵌套执行,传统的写法和管道的写法分别如下。
// 传统的写法
exclaim(capitalize(doubleSay('hello')))
// "Hello, hello!"
// 管道的写法
'hello'
|> doubleSay
|> capitalize
|> exclaim
// "Hello, hello!"
- 管道运算符只能传递一个值,这意味着它右边的函数必须是一个单参数函数。如果是多参数函数,就必须进行柯里化,改成单参数的版本。
- 管道运算符对于
await
函数也适用。
x |> await f
// 等同于
await f(x)
数值分隔符
Math.signbit()
Math.sign()
用来判断一个值的正负,但是如果参数是-0,它会返回-0。这导致对于判断符号位的正负,Math.sign()
不是很有用。- 目前,有一个提案,引入了
Math.signbit()
方法判断一个数的符号位是否设置了。
该方法的算法如下。
如果参数是NaN,返回false
如果参数是-0,返回true
如果参数是负值,返回true
其他情况返回false
双冒号运算符
- 函数绑定运算符是并排的两个冒号(::),双冒号左边是一个对象,右边是一个函数。该运算符会自动将左边的对象,作为上下文环境(即
this
对象),绑定到右边的函数上面。
foo::bar;
// 等同于
bar.bind(foo);
foo::bar(...arguments);
// 等同于
bar.apply(foo, arguments);
- 如果双冒号左边为空,右边是一个对象的方法,则等于将该方法绑定在该对象上面。
var method = obj::obj.foo;
// 等同于
var method = ::obj.foo;
let log = ::console.log;
// 等同于
var log = console.log.bind(console);
- 如果双冒号运算符的运算结果,还是一个对象,就可以采用链式写法。
Realm API
- Realm API 提供沙箱功能(sandbox),允许隔离代码,防止那些被隔离的代码拿到全局对象。
#!命令
import.meta
装饰器
最后
以上就是感性毛豆为你收集整理的ES6详解(持续更)前言let和const命令变量的解构赋值字符串的拓展字符串的新增方法正则的扩展数值的拓展函数的拓展数组的扩展对象的拓展对象的新增方法SymbolSet 和 Map 数据结构Proxy(建议先看Reflect)ReflectPromiseIterator 和 for…of 循环Generator 函数的语法Generator 函数的异步应用async 函数Class 的基本语法Class 的继承Module 的语法Module 的加载实现编程风格读懂 ECMAScript 规格的全部内容,希望文章能够帮你解决ES6详解(持续更)前言let和const命令变量的解构赋值字符串的拓展字符串的新增方法正则的扩展数值的拓展函数的拓展数组的扩展对象的拓展对象的新增方法SymbolSet 和 Map 数据结构Proxy(建议先看Reflect)ReflectPromiseIterator 和 for…of 循环Generator 函数的语法Generator 函数的异步应用async 函数Class 的基本语法Class 的继承Module 的语法Module 的加载实现编程风格读懂 ECMAScript 规格所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复