- 一丶数组的扩展
- 二丶数值的扩展
- 三丶对象的扩展
- 2.super 关键字
- 四丶对象的新增方法
- 五丶Symbol
一丶数组的扩展
1.扩展运算符
扩展运算符(spread)是三个点(...)。它好比 rest 参数的逆运算,将一个数组转为用逗号分隔的参数序列。
console.log(...[1, 2, 3])// 1 2 3console.log(1, ...[2, 3, 4], 5)// 1 2 3 4 5[...document.querySelectorAll('div')]// [<div>, <div>, <div>]
该运算符主要用于函数调用。
function push(array, ...items) {array.push(...items);}function add(x, y) {return x + y;}const numbers = [4, 38];add(...numbers) // 42
上面代码中,array.push(...items)和add(...numbers)这两行,都是函数的调用,它们的都使用了扩展运算符。该运算符将一个数组,变为参数序列。
扩展运算符与正常的函数参数可以结合使用,非常灵活。
function f(v, w, x, y, z) { }const args = [0, 1];f(-1, ...args, 2, ...[3]);
扩展运算符后面还可以放置表达式。
const arr = [...(x > 0 ? ['a'] : []),'b',];
如果扩展运算符后面是一个空数组,则不产生任何效果。
[...[], 1]// [1]
注意,只有函数调用时,扩展运算符才可以放在圆括号中,否则会报错。
(...[1, 2])// Uncaught SyntaxError: Unexpected numberconsole.log((...[1, 2]))// Uncaught SyntaxError: Unexpected numberconsole.log(...[1, 2])// 1 2
上面三种情况,扩展运算符都放在圆括号里面,但是前两种情况会报错,因为扩展运算符所在的括号不是函数调用。
2.Array.from()
Array.from方法用于将两类对象转为真正的数组:类似数组的对象(array-like object)和可遍历(iterable)的对象(包括 ES6 新增的数据结构 Set 和 Map)。
扩展运算符背后调用的是遍历器接口(Symbol.iterator),如果一个对象没有部署这个接口,就无法转换。Array.from方法还支持类似数组的对象。所谓类似数组的对象,本质特征只有一点,即必须有length属性。因此,任何有length属性的对象,都可以通过Array.from方法转为数组,而此时扩展运算符就无法转换。
Array.from({ length: 3 });// [ undefined, undefined, undefined ]
上面代码中,Array.from返回了一个具有三个成员的数组,每个位置的值都是undefined。扩展运算符转换不了这个对象。
对于还没有部署该方法的浏览器,可以用Array.prototype.slice方法替代。
const toArray = (() =>Array.from ? Array.from : obj => [].slice.call(obj))();
Array.from还可以接受第二个参数,作用类似于数组的map方法,用来对每个元素进行处理,将处理后的值放入返回的数组。
Array.from(arrayLike, x => x * x);// 等同于Array.from(arrayLike).map(x => x * x);Array.from([1, 2, 3], (x) => x * x)// [1, 4, 9]
3.Array.of()
Array.of方法用于将一组值,转换为数组。
Array.of(3, 11, 8) // [3,11,8]Array.of(3) // [3]Array.of(3).length // 1
这个方法的主要目的,是弥补数组构造函数Array()的不足。因为参数个数的不同,会导致Array()的行为有差异。
Array() // []Array(3) // [, , ,]Array(3, 11, 8) // [3, 11, 8]
上面代码中,Array方法没有参数、一个参数、三个参数时,返回结果都不一样。只有当参数个数不少于 2 个时,Array()才会返回由参数组成的新数组。参数个数只有一个时,实际上是指定数组的长度。Array.of基本上可以用来替代Array()或new Array(),并且不存在由于参数不同而导致的重载。它的行为非常统一。
Array.of() // []Array.of(undefined) // [undefined]Array.of(1) // [1]Array.of(1, 2) // [1, 2]
Array.of总是返回参数值组成的数组。如果没有参数,就返回一个空数组。Array.of方法可以用下面的代码模拟实现。
function ArrayOf(){return [].slice.call(arguments);}
4.数组实例的 copyWithin()
数组实例的copyWithin()方法,在当前数组内部,将指定位置的成员复制到其他位置(会覆盖原有成员),然后返回当前数组。也就是说,使用这个方法,会修改当前数组。
Array.prototype.copyWithin(target, start = 0, end = this.length)
它接受三个参数。
- target(必需):从该位置开始替换数据。如果为负值,表示倒数。
- start(可选):从该位置开始读取数据,默认为 0。如果为负值,表示从末尾开始计算。
- end(可选):到该位置前停止读取数据,默认等于数组长度。如果为负值,表示从末尾开始计算。
这三个参数都应该是数值,如果不是,会自动转为数值。
[1, 2, 3, 4, 5].copyWithin(0, 3)// [4, 5, 3, 4, 5]
上面代码表示将从 3 号位直到数组结束的成员(4 和 5),复制到从 0 号位开始的位置,结果覆盖了原来的 1 和 2。
下面是更多例子。
// 将3号位复制到0号位[1, 2, 3, 4, 5].copyWithin(0, 3, 4)// [4, 2, 3, 4, 5]// -2相当于3号位,-1相当于4号位[1, 2, 3, 4, 5].copyWithin(0, -2, -1)// [4, 2, 3, 4, 5]// 将3号位复制到0号位[].copyWithin.call({length: 5, 3: 1}, 0, 3)// {0: 1, 3: 1, length: 5}// 将2号位到数组结束,复制到0号位let i32a = new Int32Array([1, 2, 3, 4, 5]);i32a.copyWithin(0, 2);// Int32Array [3, 4, 5, 4, 5]// 对于没有部署 TypedArray 的 copyWithin 方法的平台// 需要采用下面的写法[].copyWithin.call(new Int32Array([1, 2, 3, 4, 5]), 0, 3, 4);// Int32Array [4, 2, 3, 4, 5]
5.数组实例的 find() 和 findIndex()
数组实例的find方法,用于找出第一个符合条件的数组成员。它的参数是一个回调函数,所有数组成员依次执行该回调函数,直到找出第一个返回值为true的成员,然后返回该成员。如果没有符合条件的成员,则返回undefined。
[1, 4, -5, 10].find((n) => n < 0)// -5
上面代码找出数组中第一个小于 0 的成员。
[1, 5, 10, 15].find(function(value, index, arr) {return value > 9;}) // 10
上面代码中,find方法的回调函数可以接受三个参数,依次为当前的值、当前的位置和原数组。
数组实例的findIndex方法的用法与find方法非常类似,返回第一个符合条件的数组成员的位置,如果所有成员都不符合条件,则返回-1。
[1, 5, 10, 15].findIndex(function(value, index, arr) {return value > 9;}) // 2
这两个方法都可以接受第二个参数,用来绑定回调函数的this对象。
function f(v){return v > this.age;}let person = {name: 'John', age: 20};[10, 12, 26, 15].find(f, person); // 26
上面的代码中,find函数接收了第二个参数person对象,回调函数中的this对象指向person对象。
另外,这两个方法都可以发现NaN,弥补了数组的indexOf方法的不足。
[NaN].indexOf(NaN)// -1[NaN].findIndex(y => Object.is(NaN, y))// 0
上面代码中,indexOf方法无法识别数组的NaN成员,但是findIndex方法可以借助Object.is方法做到。
6.数组实例的 fill()
fill方法使用给定值,填充一个数组。
['a', 'b', 'c'].fill(7)// [7, 7, 7]new Array(3).fill(7)// [7, 7, 7]
上面代码表明,fill方法用于空数组的初始化非常方便。数组中已有的元素,会被全部抹去。fill方法还可以接受第二个和第三个参数,用于指定填充的起始位置和结束位置。
['a', 'b', 'c'].fill(7, 1, 2)// ['a', 7, 'c']
上面代码表示,fill方法从 1 号位开始,向原数组填充 7,到 2 号位之前结束。
注意,如果填充的类型为对象,那么被赋值的是同一个内存地址的对象,而不是深拷贝对象。
let arr = new Array(3).fill({name: "Mike"});arr[0].name = "Ben";arr// [{name: "Ben"}, {name: "Ben"}, {name: "Ben"}]let arr = new Array(3).fill([]);arr[0].push(5);arr// [[5], [5], [5]]
7.数组实例的 entries(),keys() 和 values()
ES6 提供三个新的方法——entries(),keys()和values()——用于遍历数组。它们都返回一个遍历器对象(详见《Iterator》一章),可以用for...of循环进行遍历,唯一的区别是keys()是对键名的遍历、values()是对键值的遍历,entries()是对键值对的遍历。
for (let index of ['a', 'b'].keys()) {console.log(index);}// 0// 1for (let elem of ['a', 'b'].values()) {console.log(elem);}// 'a'// 'b'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']
8.数组实例的 includes()
Array.prototype.includes方法返回一个布尔值,表示某个数组是否包含给定的值,与字符串的includes方法类似。ES2016 引入了该方法。
[1, 2, 3].includes(2) // true[1, 2, 3].includes(4) // false[1, 2, NaN].includes(NaN) // true
该方法的第二个参数表示搜索的起始位置,默认为0。如果第二个参数为负数,则表示倒数的位置,如果这时它大于数组长度(比如第二个参数为-4,但数组长度为3),则会重置为从0开始。
[1, 2, 3].includes(3, 3); // false[1, 2, 3].includes(3, -1); // true
没有该方法之前,我们通常使用数组的indexOf方法,检查是否包含某个值。
if (arr.indexOf(el) !== -1) {// ...}
indexOf方法有两个缺点,一是不够语义化,它的含义是找到参数值的第一个出现位置,所以要去比较是否不等于-1,表达起来不够直观。二是,它内部使用严格相等运算符(===)进行判断,这会导致对NaN的误判。
[NaN].indexOf(NaN)// -1
includes使用的是不一样的判断算法,就没有这个问题。
[NaN].includes(NaN)// true
下面代码用来检查当前环境是否支持该方法,如果不支持,部署一个简易的替代版本。
const contains = (() =>Array.prototype.includes? (arr, value) => arr.includes(value): (arr, value) => arr.some(el => el === value))();contains(['foo', 'bar'], 'baz'); // => false
另外,Map 和 Set 数据结构有一个has方法,需要注意与includes区分。
- Map 结构的
has方法,是用来查找键名的,比如Map.prototype.has(key)、WeakMap.prototype.has(key)、Reflect.has(target, propertyKey)。 Set 结构的
has方法,是用来查找值的,比如Set.prototype.has(value)、WeakSet.prototype.has(value)。9.数组实例的 flat(),flatMap()
数组的成员有时还是数组,
Array.prototype.flat()用于将嵌套的数组“拉平”,变成一维的数组。该方法返回一个新数组,对原数据没有影响。[1, 2, [3, 4]].flat()// [1, 2, 3, 4]
上面代码中,原数组的成员里面有一个数组,
flat()方法将子数组的成员取出来,添加在原来的位置。flat()默认只会“拉平”一层,如果想要“拉平”多层的嵌套数组,可以将flat()方法的参数写成一个整数,表示想要拉平的层数,默认为1。[1, 2, [3, [4, 5]]].flat()// [1, 2, 3, [4, 5]][1, 2, [3, [4, 5]]].flat(2)// [1, 2, 3, 4, 5]
上面代码中,
flat()的参数为2,表示要“拉平”两层的嵌套数组。
如果不管有多少层嵌套,都要转成一维数组,可以用Infinity关键字作为参数。[1, [2, [3]]].flat(Infinity)// [1, 2, 3]
如果原数组有空位,
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()返回的还是一个嵌套数组。flatMap()方法的参数是一个遍历函数,该函数可以接受三个参数,分别是当前数组成员、当前数组成员的位置(从零开始)、原数组。arr.flatMap(function callback(currentValue[, index[, array]]) {// ...}[, thisArg])
flatMap()方法还可以有第二个参数,用来绑定遍历函数里面的this。10.数组的空位
数组的空位指,数组的某一个位置没有任何值。比如,
Array构造函数返回的数组都是空位。Array(3) // [, , ,]
上面代码中,
Array(3)返回一个具有 3 个空位的数组。
注意,空位不是undefined,一个位置的值等于undefined,依然是有值的。空位是没有任何值,in运算符可以说明这一点。0 in [undefined, undefined, undefined] // true0 in [, , ,] // false
上面代码说明,第一个数组的 0 号位置是有值的,第二个数组的 0 号位置没有值。
Array.from方法会将数组的空位,转为undefined,也就是说,这个方法不会忽略空位。Array.from(['a',,'b'])// [ "a", undefined, "b" ]
扩展运算符(
...)也会将空位转为undefined。[...['a',,'b']]// [ "a", undefined, "b" ]
copyWithin()会连空位一起拷贝。[,'a','b',,].copyWithin(2,0) // [,"a",,"a"]
fill()会将空位视为正常的数组位置。new Array(3).fill('a') // ["a","a","a"]
for...of循环也会遍历空位。let arr = [, ,];for (let i of arr) {console.log(1);}// 1// 1
上面代码中,数组
arr有两个空位,for...of并没有忽略它们。如果改成map方法遍历,空位是会跳过的。entries()、keys()、values()、find()和findIndex()会将空位处理成undefined。// entries()[...[,'a'].entries()] // [[0,undefined], [1,"a"]]// keys()[...[,'a'].keys()] // [0,1]// values()[...[,'a'].values()] // [undefined,"a"]// find()[,'a'].find(x => true) // undefined// findIndex()[,'a'].findIndex(x => true) // 0
二丶数值的扩展
1.Number.isFinite(), Number.isNaN()
ES6 在
Number对象上,新提供了Number.isFinite()和Number.isNaN()两个方法。Number.isFinite()用来检查一个数值是否为有限的(finite),即不是Infinity。Number.isFinite(15); // trueNumber.isFinite(0.8); // trueNumber.isFinite(NaN); // falseNumber.isFinite(Infinity); // falseNumber.isFinite(-Infinity); // falseNumber.isFinite('foo'); // falseNumber.isFinite('15'); // falseNumber.isFinite(true); // false
注意,如果参数类型不是数值,
Number.isFinite一律返回false。Number.isNaN()用来检查一个值是否为NaN。Number.isNaN(NaN) // trueNumber.isNaN(15) // falseNumber.isNaN('15') // falseNumber.isNaN(true) // falseNumber.isNaN(9/NaN) // trueNumber.isNaN('true' / 0) // trueNumber.isNaN('true' / 'true') // true
如果参数类型不是
NaN,Number.isNaN一律返回false。
它们与传统的全局方法isFinite()和isNaN()的区别在于,传统方法先调用Number()将非数值的值转为数值,再进行判断,而这两个新方法只对数值有效,Number.isFinite()对于非数值一律返回false,Number.isNaN()只有对于NaN才返回true,非NaN一律返回false。isFinite(25) // trueisFinite("25") // trueNumber.isFinite(25) // trueNumber.isFinite("25") // falseisNaN(NaN) // trueisNaN("NaN") // trueNumber.isNaN(NaN) // trueNumber.isNaN("NaN") // falseNumber.isNaN(1) // false
2.Number.parseInt(), Number.parseFloat()
ES6 将全局方法
parseInt()和parseFloat(),移植到Number对象上面,行为完全保持不变。// ES5的写法parseInt('12.34') // 12parseFloat('123.45#') // 123.45// ES6的写法Number.parseInt('12.34') // 12Number.parseFloat('123.45#') // 123.45
这样做的目的,是逐步减少全局性方法,使得语言逐步模块化。
Number.parseInt === parseInt // trueNumber.parseFloat === parseFloat // true
3.Number.isInteger()
Number.isInteger()用来判断一个数值是否为整数。Number.isInteger(25) // trueNumber.isInteger(25.1) // false
JavaScript 内部,整数和浮点数采用的是同样的储存方法,所以 25 和 25.0 被视为同一个值。
Number.isInteger(25) // trueNumber.isInteger(25.0) // true
如果参数不是数值,
Number.isInteger返回false。Number.isInteger() // falseNumber.isInteger(null) // falseNumber.isInteger('15') // falseNumber.isInteger(true) // false
注意,由于 JavaScript 采用 IEEE 754 标准,数值存储为64位双精度格式,数值精度最多可以达到 53 个二进制位(1 个隐藏位与 52 个有效位)。如果数值的精度超过这个限度,第54位及后面的位就会被丢弃,这种情况下,
Number.isInteger可能会误判。Number.isInteger(3.0000000000000002) // true
上面代码中,
Number.isInteger的参数明明不是整数,但是会返回true。原因就是这个小数的精度达到了小数点后16个十进制位,转成二进制位超过了53个二进制位,导致最后的那个2被丢弃了。
类似的情况还有,如果一个数值的绝对值小于Number.MIN_VALUE(5E-324),即小于 JavaScript 能够分辨的最小值,会被自动转为 0。这时,Number.isInteger也会误判。Number.isInteger(5E-324) // falseNumber.isInteger(5E-325) // true
上面代码中,
5E-325由于值太小,会被自动转为0,因此返回true。
总之,如果对数据精度的要求较高,不建议使用Number.isInteger()判断一个数值是否为整数。4.Number.EPSILON
ES6 在
Number对象上面,新增一个极小的常量Number.EPSILON。根据规格,它表示 1 与大于 1 的最小浮点数之间的差。
对于 64 位浮点数来说,大于 1 的最小浮点数相当于二进制的1.00..001,小数点后面有连续 51 个零。这个值减去 1 之后,就等于 2 的 -52 次方。Number.EPSILON === Math.pow(2, -52)// trueNumber.EPSILON// 2.220446049250313e-16Number.EPSILON.toFixed(20)// "0.00000000000000022204"
Number.EPSILON实际上是 JavaScript 能够表示的最小精度。误差如果小于这个值,就可以认为已经没有意义了,即不存在误差了。
引入一个这么小的量的目的,在于为浮点数计算,设置一个误差范围。我们知道浮点数计算是不精确的。0.1 + 0.2// 0.300000000000000040.1 + 0.2 - 0.3// 5.551115123125783e-175.551115123125783e-17.toFixed(20)// '0.00000000000000005551'
上面代码解释了,为什么比较
0.1 + 0.2与0.3得到的结果是false。0.1 + 0.2 === 0.3 // false
Number.EPSILON可以用来设置“能够接受的误差范围”。比如,误差范围设为 2 的-50 次方(即Number.EPSILON * Math.pow(2, 2)),即如果两个浮点数的差小于这个值,我们就认为这两个浮点数相等。5.551115123125783e-17 < Number.EPSILON * Math.pow(2, 2)// true
因此,
Number.EPSILON的实质是一个可以接受的最小误差范围。function withinErrorMargin (left, right) {return Math.abs(left - right) < Number.EPSILON * Math.pow(2, 2);}0.1 + 0.2 === 0.3 // falsewithinErrorMargin(0.1 + 0.2, 0.3) // true1.1 + 1.3 === 2.4 // falsewithinErrorMargin(1.1 + 1.3, 2.4) // true
5.安全整数和 Number.isSafeInteger()
JavaScript 能够准确表示的整数范围在
-2^53到2^53之间(不含两个端点),超过这个范围,无法精确表示这个值。Math.pow(2, 53) // 90071992547409929007199254740992 // 90071992547409929007199254740993 // 9007199254740992Math.pow(2, 53) === Math.pow(2, 53) + 1// true
上面代码中,超出 2 的 53 次方之后,一个数就不精确了。
ES6 引入了Number.MAX_SAFE_INTEGER和Number.MIN_SAFE_INTEGER这两个常量,用来表示这个范围的上下限。Number.MAX_SAFE_INTEGER === Math.pow(2, 53) - 1// trueNumber.MAX_SAFE_INTEGER === 9007199254740991// trueNumber.MIN_SAFE_INTEGER === -Number.MAX_SAFE_INTEGER// trueNumber.MIN_SAFE_INTEGER === -9007199254740991// true
上面代码中,可以看到 JavaScript 能够精确表示的极限。
Number.isSafeInteger()则是用来判断一个整数是否落在这个范围之内。Number.isSafeInteger('a') // falseNumber.isSafeInteger(null) // falseNumber.isSafeInteger(NaN) // falseNumber.isSafeInteger(Infinity) // falseNumber.isSafeInteger(-Infinity) // falseNumber.isSafeInteger(3) // trueNumber.isSafeInteger(1.2) // falseNumber.isSafeInteger(9007199254740990) // trueNumber.isSafeInteger(9007199254740992) // falseNumber.isSafeInteger(Number.MIN_SAFE_INTEGER - 1) // falseNumber.isSafeInteger(Number.MIN_SAFE_INTEGER) // trueNumber.isSafeInteger(Number.MAX_SAFE_INTEGER) // trueNumber.isSafeInteger(Number.MAX_SAFE_INTEGER + 1) // false
这个函数的实现很简单,就是跟安全整数的两个边界值比较一下。
Number.isSafeInteger = function (n) {return (typeof n === 'number' &&Math.round(n) === n &&Number.MIN_SAFE_INTEGER <= n &&n <= Number.MAX_SAFE_INTEGER);}
实际使用这个函数时,需要注意。验证运算结果是否落在安全整数的范围内,不要只验证运算结果,而要同时验证参与运算的每个值。
Number.isSafeInteger(9007199254740993)// falseNumber.isSafeInteger(990)// trueNumber.isSafeInteger(9007199254740993 - 990)// true9007199254740993 - 990// 返回结果 9007199254740002// 正确答案应该是 9007199254740003
上面代码中,
9007199254740993不是一个安全整数,但是Number.isSafeInteger会返回结果,显示计算结果是安全的。这是因为,这个数超出了精度范围,导致在计算机内部,以9007199254740992的形式储存。9007199254740993 === 9007199254740992// true
所以,如果只验证运算结果是否为安全整数,很可能得到错误结果。下面的函数可以同时验证两个运算数和运算结果。
function trusty (left, right, result) {if (Number.isSafeInteger(left) &&Number.isSafeInteger(right) &&Number.isSafeInteger(result)) {return result;}throw new RangeError('Operation cannot be trusted!');}trusty(9007199254740993, 990, 9007199254740993 - 990)// RangeError: Operation cannot be trusted!trusty(1, 2, 3)// 3
6.Math 对象的扩展
ES6 在 Math 对象上新增了 17 个与数学相关的方法。所有这些方法都是静态方法,只能在 Math 对象上调用。
1.Math.trunc()
Math.trunc方法用于去除一个数的小数部分,返回整数部分。Math.trunc(4.1) // 4Math.trunc(4.9) // 4Math.trunc(-4.1) // -4Math.trunc(-4.9) // -4Math.trunc(-0.1234) // -0
对于非数值,
Math.trunc内部使用Number方法将其先转为数值。Math.trunc('123.456') // 123Math.trunc(true) //1Math.trunc(false) // 0Math.trunc(null) // 0
对于空值和无法截取整数的值,返回
NaN。Math.trunc(NaN); // NaNMath.trunc('foo'); // NaNMath.trunc(); // NaNMath.trunc(undefined) // NaN
对于没有部署这个方法的环境,可以用下面的代码模拟。
Math.trunc = Math.trunc || function(x) {return x < 0 ? Math.ceil(x) : Math.floor(x);};
2.Math.sign()
Math.sign方法用来判断一个数到底是正数、负数、还是零。对于非数值,会先将其转换为数值。
它会返回五种值。参数为正数,返回
+1;- 参数为负数,返回
-1; - 参数为 0,返回
0; - 参数为-0,返回
-0; 其他值,返回
NaN。Math.sign(-5) // -1Math.sign(5) // +1Math.sign(0) // +0Math.sign(-0) // -0Math.sign(NaN) // NaN
如果参数是非数值,会自动转为数值。对于那些无法转为数值的值,会返回
NaN。Math.sign('') // 0Math.sign(true) // +1Math.sign(false) // 0Math.sign(null) // 0Math.sign('9') // +1Math.sign('foo') // NaNMath.sign() // NaNMath.sign(undefined) // NaN
对于没有部署这个方法的环境,可以用下面的代码模拟。
Math.sign = Math.sign || function(x) {x = +x; // convert to a numberif (x === 0 || isNaN(x)) {return x;}return x > 0 ? 1 : -1;};
3.Math.cbrt()
Math.cbrt方法用于计算一个数的立方根。Math.cbrt(-1) // -1Math.cbrt(0) // 0Math.cbrt(1) // 1Math.cbrt(2) // 1.2599210498948734
对于非数值,
Math.cbrt方法内部也是先使用Number方法将其转为数值。Math.cbrt('8') // 2Math.cbrt('hello') // NaN
对于没有部署这个方法的环境,可以用下面的代码模拟。
Math.cbrt = Math.cbrt || function(x) {var y = Math.pow(Math.abs(x), 1/3);return x < 0 ? -y : y;};
4.Math.clz32()
Math.clz32()方法将参数转为 32 位无符号整数的形式,然后返回这个 32 位值里面有多少个前导 0。Math.clz32(0) // 32Math.clz32(1) // 31Math.clz32(1000) // 22Math.clz32(0b01000000000000000000000000000000) // 1Math.clz32(0b00100000000000000000000000000000) // 2
上面代码中,0 的二进制形式全为 0,所以有 32 个前导 0;1 的二进制形式是
0b1,只占 1 位,所以 32 位之中有 31 个前导 0;1000 的二进制形式是0b1111101000,一共有 10 位,所以 32 位之中有 22 个前导 0。clz32这个函数名就来自”count leading zero bits in 32-bit binary representation of a number“(计算一个数的 32 位二进制形式的前导 0 的个数)的缩写。
左移运算符(<<)与Math.clz32方法直接相关。Math.clz32(0) // 32Math.clz32(1) // 31Math.clz32(1 << 1) // 30Math.clz32(1 << 2) // 29Math.clz32(1 << 29) // 2
对于小数,
Math.clz32方法只考虑整数部分。Math.clz32(3.2) // 30Math.clz32(3.9) // 30
对于空值或其他类型的值,
Math.clz32方法会将它们先转为数值,然后再计算。Math.clz32() // 32Math.clz32(NaN) // 32Math.clz32(Infinity) // 32Math.clz32(null) // 32Math.clz32('foo') // 32Math.clz32([]) // 32Math.clz32({}) // 32Math.clz32(true) // 31
5.Math.imul()
Math.imul方法返回两个数以 32 位带符号整数形式相乘的结果,返回的也是一个 32 位的带符号整数。Math.imul(2, 4) // 8Math.imul(-1, 8) // -8Math.imul(-2, -2) // 4
如果只考虑最后 32 位,大多数情况下,
Math.imul(a, b)与a * b的结果是相同的,即该方法等同于(a * b)|0的效果(超过 32 位的部分溢出)。之所以需要部署这个方法,是因为 JavaScript 有精度限制,超过 2 的 53 次方的值无法精确表示。这就是说,对于那些很大的数的乘法,低位数值往往都是不精确的,Math.imul方法可以返回正确的低位数值。(0x7fffffff * 0x7fffffff)|0 // 0
上面这个乘法算式,返回结果为 0。但是由于这两个二进制数的最低位都是 1,所以这个结果肯定是不正确的,因为根据二进制乘法,计算结果的二进制最低位应该也是 1。这个错误就是因为它们的乘积超过了 2 的 53 次方,JavaScript 无法保存额外的精度,就把低位的值都变成了 0。
Math.imul方法可以返回正确的值 1。Math.imul(0x7fffffff, 0x7fffffff) // 1
6.Math.fround()
Math.fround方法返回一个数的32位单精度浮点数形式。
对于32位单精度格式来说,数值精度是24个二进制位(1 位隐藏位与 23 位有效位),所以对于 -2 至 2 之间的整数(不含两个端点),返回结果与参数本身一致。Math.fround(0) // 0Math.fround(1) // 1Math.fround(2 ** 24 - 1) // 16777215
如果参数的绝对值大于 2,返回的结果便开始丢失精度。
Math.fround(2 ** 24) // 16777216Math.fround(2 ** 24 + 1) // 16777216
Math.fround方法的主要作用,是将64位双精度浮点数转为32位单精度浮点数。如果小数的精度超过24个二进制位,返回值就会不同于原值,否则返回值不变(即与64位双精度值一致)。// 未丢失有效精度Math.fround(1.125) // 1.125Math.fround(7.25) // 7.25// 丢失精度Math.fround(0.3) // 0.30000001192092896Math.fround(0.7) // 0.699999988079071Math.fround(1.0000000123) // 1
对于
NaN和Infinity,此方法返回原值。对于其它类型的非数值,Math.fround方法会先将其转为数值,再返回单精度浮点数。Math.fround(NaN) // NaNMath.fround(Infinity) // InfinityMath.fround('5') // 5Math.fround(true) // 1Math.fround(null) // 0Math.fround([]) // 0Math.fround({}) // NaN
对于没有部署这个方法的环境,可以用下面的代码模拟。
Math.fround = Math.fround || function (x) {return new Float32Array([x])[0];};
7.Math.hypot()
Math.hypot方法返回所有参数的平方和的平方根。Math.hypot(3, 4); // 5Math.hypot(3, 4, 5); // 7.0710678118654755Math.hypot(); // 0Math.hypot(NaN); // NaNMath.hypot(3, 4, 'foo'); // NaNMath.hypot(3, 4, '5'); // 7.0710678118654755Math.hypot(-3); // 3
上面代码中,3 的平方加上 4 的平方,等于 5 的平方。
如果参数不是数值,Math.hypot方法会将其转为数值。只要有一个参数无法转为数值,就会返回 NaN。8.对数方法
(1) Math.expm1()
Math.expm1(x)返回 e - 1,即Math.exp(x) - 1。Math.expm1(-1) // -0.6321205588285577Math.expm1(0) // 0Math.expm1(1) // 1.718281828459045
对于没有部署这个方法的环境,可以用下面的代码模拟。
Math.expm1 = Math.expm1 || function(x) {return Math.exp(x) - 1;};
(2)Math.log1p()
Math.log1p(x)方法返回1 + x的自然对数,即Math.log(1 + x)。如果x小于-1,返回NaN。Math.log1p(1) // 0.6931471805599453Math.log1p(0) // 0Math.log1p(-1) // -InfinityMath.log1p(-2) // NaN
对于没有部署这个方法的环境,可以用下面的代码模拟。
Math.log1p = Math.log1p || function(x) {return Math.log(1 + x);};
(3)Math.log10()
Math.log10(x)返回以 10 为底的x的对数。如果x小于 0,则返回 NaN。Math.log10(2) // 0.3010299956639812Math.log10(1) // 0Math.log10(0) // -InfinityMath.log10(-2) // NaNMath.log10(100000) // 5
对于没有部署这个方法的环境,可以用下面的代码模拟。
Math.log10 = Math.log10 || function(x) {return Math.log(x) / Math.LN10;};
(4)Math.log2()
Math.log2(x)返回以 2 为底的x的对数。如果x小于 0,则返回 NaN。Math.log2(3) // 1.584962500721156Math.log2(2) // 1Math.log2(1) // 0Math.log2(0) // -InfinityMath.log2(-2) // NaNMath.log2(1024) // 10Math.log2(1 << 29) // 29
对于没有部署这个方法的环境,可以用下面的代码模拟。
Math.log2 = Math.log2 || function(x) {return Math.log(x) / Math.LN2;};
9.双曲函数方法
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)7.指数运算符
ES2016 新增了一个指数运算符(
**)。2 ** 2 // 42 ** 3 // 8
这个运算符的一个特点是右结合,而不是常见的左结合。多个指数运算符连用时,是从最右边开始计算的。
// 相当于 2 ** (3 ** 2)2 ** 3 ** 2// 512
上面代码中,首先计算的是第二个指数运算符,而不是第一个。
指数运算符可以与等号结合,形成一个新的赋值运算符(**=)。let a = 1.5;a **= 2;// 等同于 a = a * a;let b = 4;b **= 3;// 等同于 b = b * b * b;
注意,V8 引擎的指数运算符与
Math.pow的实现不相同,对于特别大的运算结果,两者会有细微的差异。Math.pow(99, 99)// 3.697296376497263e+19799 ** 99// 3.697296376497268e+197
三丶对象的扩展
1.属性的遍历
(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()]
Reflect.ownKeys方法返回一个数组,包含了参数对象的所有属性。这个数组的属性次序是这样的,首先是数值属性2和10,其次是字符串属性b和a,最后是 Symbol 属性。2.super 关键字
我们知道,this关键字总是指向函数所在的当前对象,ES6 又新增了另一个类似的关键字super,指向当前对象的原型对象。
上面代码中,对象const proto = {foo: 'hello'};const obj = {foo: 'world',find() {return super.foo;}};Object.setPrototypeOf(obj, proto);obj.find() // "hello"
obj.find()方法之中,通过super.foo引用了原型对象proto的foo属性。
注意,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)(方法)。
上面代码中,const proto = {x: 'hello',foo() {console.log(this.x);},};const obj = {x: 'world',foo() {super.foo();}}Object.setPrototypeOf(obj, proto);obj.foo() // "world"
super.foo指向原型对象proto的foo方法,但是绑定的this却还是当前对象obj,因此输出的就是world。四丶对象的新增方法
1.Object.is()
Object.is就是部署这个算法的新方法。它用来比较两个值是否严格相等,与严格比较运算符(===)的行为基本一致。不同之处只有两个:一是+0不等于-0,二是NaN等于自身。2.Object.assign()
Object.assign方法用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target)。const target = { a: 1 };const source1 = { b: 2 };const source2 = { c: 3 };Object.assign(target, source1, source2);target // {a:1, b:2, c:3}
Object.assign方法的第一个参数是目标对象,后面的参数都是源对象。
注意,如果目标对象与源对象有同名属性,或多个源对象有同名属性,则后面的属性会覆盖前面的属性。Object.assign拷贝的属性是有限制的,只拷贝源对象的自身属性(不拷贝继承属性),也不拷贝不可枚举的属性(enumerable: false)。
上面代码中,Object.assign({b: 'c'},Object.defineProperty({}, 'invisible', {enumerable: false,value: 'hello'}))// { b: 'c' }
Object.assign要拷贝的对象只有一个不可枚举属性invisible,这个属性并没有被拷贝进去。
属性名为 Symbol 值的属性,也会被Object.assign拷贝。Object.assign({ a: 'b' }, { [Symbol('c')]: 'd' })// { a: 'b', Symbol(c): 'd' }
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']
注意点
(1)浅拷贝
Object.assign方法实行的是浅拷贝,而不是深拷贝。也就是说,如果源对象某个属性的值是对象,那么目标对象拷贝得到的是这个对象的引用。(2)同名属性的替换
对于这种嵌套的对象,一旦遇到同名属性,Object.assign的处理方法是替换,而不是添加。const target = { a: { b: 'c', d: 'e' } }const source = { a: { b: 'hello' } }Object.assign(target, source)// { a: { b: 'hello' } }
(3)数组的处理
Object.assign可以用来处理数组,但是会把数组视为对象。
上面代码中,Object.assign([1, 2, 3], [4, 5])// [4, 5, 3]
Object.assign把数组视为属性名为 0、1、2 的对象,因此源数组的 0 号属性4覆盖了目标数组的 0 号属性1。(4)取值函数的处理
Object.assign只能进行值的复制,如果要复制的值是一个取值函数,那么将求值后再复制。3.proto属性,Object.setPrototypeOf(),Object.getPrototypeOf()
JavaScript 语言的对象继承是通过原型链实现的。ES6 提供了更多原型对象的操作方法。proto属性
__proto__属性(前后各两个下划线),用来读取或设置当前对象的prototype对象。目前,所有浏览器(包括 IE11)都部署了这个属性。
该属性没有写入 ES6 的正文,而是写入了附录,原因是// es5 的写法const obj = {method: function() { ... }};obj.__proto__ = someOtherObj;// es6 的写法var obj = Object.create(someOtherObj);obj.method = function() { ... };
__proto__前后的双下划线,说明它本质上是一个内部属性,而不是一个正式的对外的 API,只是由于浏览器广泛支持,才被加入了 ES6。标准明确规定,只有浏览器必须部署这个属性,其他运行环境不一定需要部署,而且新的代码最好认为这个属性是不存在的。因此,无论从语义的角度,还是从兼容性的角度,都不要使用这个属性,而是使用下面的Object.setPrototypeOf()(写操作)、Object.getPrototypeOf()(读操作)、Object.create()(生成操作)代替。Object.setPrototypeOf()
Object.setPrototypeOf方法的作用与__proto__相同,用来设置一个对象的prototype对象,返回参数对象本身。它是 ES6 正式推荐的设置原型对象的方法。
该方法等同于下面的函数。// 格式Object.setPrototypeOf(object, prototype)// 用法const o = Object.setPrototypeOf({}, null);
function setPrototypeOf(obj, proto) {obj.__proto__ = proto;return obj;}
Object.getPrototypeOf()
该方法与Object.setPrototypeOf方法配套,用于读取一个对象的原型对象。五丶Symbol
1.概述
ES6 引入了一种新的原始数据类型Symbol,表示独一无二的值。它是 JavaScript 语言的第七种数据类型,前六种是:undefined、null、布尔值(Boolean)、字符串(String)、数值(Number)、对象(Object)。
Symbol 值通过Symbol函数生成。这就是说,对象的属性名现在可以有两种类型,一种是原来就有的字符串,另一种就是新增的 Symbol 类型。凡是属性名属于 Symbol 类型,就都是独一无二的,可以保证不会与其他属性名产生冲突。
上面代码中,变量let s = Symbol();typeof s// "symbol"
s就是一个独一无二的值。typeof运算符的结果,表明变量s是 Symbol 数据类型,而不是字符串之类的其他类型。
注意,Symbol函数前不能使用new命令,否则会报错。这是因为生成的 Symbol 是一个原始类型的值,不是对象。也就是说,由于 Symbol 值不是对象,所以不能添加属性。基本上,它是一种类似于字符串的数据类型。Symbol函数可以接受一个字符串作为参数,表示对 Symbol 实例的描述,主要是为了在控制台显示,或者转为字符串时,比较容易区分。2.Symbol.prototype.description
const sym = Symbol('foo');sym.description // "foo"
3.作为属性名的 Symbol
由于每一个 Symbol 值都是不相等的,这意味着 Symbol 值可以作为标识符,用于对象的属性名,就能保证不会出现同名的属性。这对于一个对象由多个模块构成的情况非常有用,能防止某一个键被不小心改写或覆盖。 ```javascript let mySymbol = Symbol(); // 第一种写法 let a = {}; amySymbol = ‘Hello!’; // 第二种写法 let a = {
}; // 第三种写法 let a = {}; Object.defineProperty(a, mySymbol, { value: ‘Hello!’ }); // 以上写法都得到同样结果 amySymbol // “Hello!”
上面代码通过方括号结构和`Object.defineProperty`,将对象的属性名指定为一个 Symbol 值。<br />注意,Symbol 值作为对象属性名时,不能用点运算符。```javascriptconst mySymbol = Symbol();const a = {};a.mySymbol = 'Hello!';a[mySymbol] // undefineda['mySymbol'] // "Hello!"
4.属性名的遍历
Symbol 作为属性名,该属性不会出现在for...in、for...of循环中,也不会被Object.keys()、Object.getOwnPropertyNames()、JSON.stringify()返回。但是,它也不是私有属性,有一个Object.getOwnPropertySymbols方法,可以获取指定对象的所有 Symbol 属性名。Object.getOwnPropertySymbols方法返回一个数组,成员是当前对象的所有用作属性名的 Symbol 值。
const obj = {};let a = Symbol('a');let b = Symbol('b');obj[a] = 'Hello';obj[b] = 'World';const objectSymbols = Object.getOwnPropertySymbols(obj);objectSymbols// [Symbol(a), Symbol(b)]
Reflect.ownKeys方法可以返回所有类型的键名,包括常规键名和 Symbol 键名。
let obj = {[Symbol('my_key')]: 1,enum: 2,nonEnum: 3};Reflect.ownKeys(obj)// ["enum", "nonEnum", Symbol(my_key)]
由于以 Symbol 值作为名称的属性,不会被常规方法遍历得到。我们可以利用这个特性,为对象定义一些非私有的、但又希望只用于内部的方法。
let size = Symbol('size');class Collection {constructor() {this[size] = 0;}add(item) {this[this[size]] = item;this[size]++;}static sizeOf(instance) {return instance[size];}}let x = new Collection();Collection.sizeOf(x) // 0x.add('foo');Collection.sizeOf(x) // 1Object.keys(x) // ['0']Object.getOwnPropertyNames(x) // ['0']Object.getOwnPropertySymbols(x) // [Symbol(size)]
上面代码中,对象x的size属性是一个 Symbol 值,所以Object.keys(x)、Object.getOwnPropertyNames(x)都无法获取它。这就造成了一种非私有的内部方法的效果。
5.Symbol.for(),Symbol.keyFor()
有时,我们希望重新使用同一个 Symbol 值,Symbol.for方法可以做到这一点。它接受一个字符串作为参数,然后搜索有没有以该参数作为名称的 Symbol 值。如果有,就返回这个 Symbol 值,否则就新建并返回一个以该字符串为名称的 Symbol 值。
let s1 = Symbol.for('foo');let s2 = Symbol.for('foo');s1 === s2 // true
上面代码中,s1和s2都是 Symbol 值,但是它们都是同样参数的Symbol.for方法生成的,所以实际上是同一个值。Symbol.for()与Symbol()这两种写法,都会生成新的 Symbol。它们的区别是,前者会被登记在全局环境中供搜索,后者不会。Symbol.for()不会每次调用就返回一个新的 Symbol 类型的值,而是会先检查给定的key是否已经存在,如果不存在才会新建一个值。比如,如果你调用Symbol.for("cat")30 次,每次都会返回同一个 Symbol 值,但是调用Symbol("cat")30 次,会返回 30 个不同的 Symbol 值。
Symbol.for("bar") === Symbol.for("bar")// trueSymbol("bar") === Symbol("bar")// false
上面代码中,由于Symbol()写法没有登记机制,所以每次调用都会返回一个不同的值。Symbol.keyFor方法返回一个已登记的 Symbol 类型值的key。
let s1 = Symbol.for("foo");Symbol.keyFor(s1) // "foo"let s2 = Symbol("foo");Symbol.keyFor(s2) // undefined
上面代码中,变量s2属于未登记的 Symbol 值,所以返回undefined。
6.内置的 Symbol 值
除了定义自己使用的 Symbol 值以外,ES6 还提供了 11 个内置的 Symbol 值,指向语言内部使用的方法
1.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
上面代码中,MyClass是一个类,new MyClass()会返回一个实例。该实例的Symbol.hasInstance方法,会在进行instanceof运算时自动调用,判断左侧的运算子是否为Array的实例。
2.Symbol.isConcatSpreadable
对象的Symbol.isConcatSpreadable属性等于一个布尔值,表示该对象用于Array.prototype.concat()时,是否可以展开。
3.Symbol.isConcatSpreadable
对象的Symbol.isConcatSpreadable属性等于一个布尔值,表示该对象用于Array.prototype.concat()时,是否可以展开。
4.Symbol.match
对象的Symbol.match属性,指向一个函数。当执行str.match(myObject)时,如果该属性存在,会调用它,返回该方法的返回值。
5.Symbol.replace
对象的Symbol.replace属性,指向一个方法,当该对象被String.prototype.replace方法调用时,会返回该方法的返回值。
6.Symbol.search
对象的Symbol.search属性,指向一个方法,当该对象被String.prototype.search方法调用时,会返回该方法的返回值。
7.Symbol.split
对象的Symbol.split属性,指向一个方法,当该对象被String.prototype.split方法调用时,会返回该方法的返回值。
8.Symbol.iterator
对象的Symbol.iterator属性,指向该对象的默认遍历器方法。
9.Symbol.toPrimitive
对象的Symbol.toPrimitive属性,指向一个方法。该对象被转为原始类型的值时,会调用这个方法,返回该对象对应的原始类型值。Symbol.toPrimitive被调用时,会接受一个字符串参数,表示当前运算的模式,一共有三种模式。
- Number:该场合需要转成数值
- String:该场合需要转成字符串
-
10.Symbol.toStringTag
对象的
Symbol.toStringTag属性,指向一个方法。在该对象上面调用Object.prototype.toString方法时,如果这个属性存在,它的返回值会出现在toString方法返回的字符串之中,表示对象的类型。也就是说,这个属性可以用来定制[object Object]或[object Array]中object后面的那个字符串。
ES6 新增内置对象的Symbol.toStringTag属性值如下。 JSON[Symbol.toStringTag]:’JSON’Math[Symbol.toStringTag]:’Math’- Module 对象
M[Symbol.toStringTag]:’Module’ ArrayBuffer.prototype[Symbol.toStringTag]:’ArrayBuffer’DataView.prototype[Symbol.toStringTag]:’DataView’Map.prototype[Symbol.toStringTag]:’Map’Promise.prototype[Symbol.toStringTag]:’Promise’Set.prototype[Symbol.toStringTag]:’Set’%TypedArray%.prototype[Symbol.toStringTag]:’Uint8Array’等WeakMap.prototype[Symbol.toStringTag]:’WeakMap’WeakSet.prototype[Symbol.toStringTag]:’WeakSet’%MapIteratorPrototype%[Symbol.toStringTag]:’Map Iterator’%SetIteratorPrototype%[Symbol.toStringTag]:’Set Iterator’%StringIteratorPrototype%[Symbol.toStringTag]:’String Iterator’Symbol.prototype[Symbol.toStringTag]:’Symbol’Generator.prototype[Symbol.toStringTag]:’Generator’GeneratorFunction.prototype[Symbol.toStringTag]:’GeneratorFunction’11.Symbol.unscopables
对象的Symbol.unscopables属性,指向一个对象。该对象指定了使用with关键字时,哪些属性会被with环境排除。
