一丶数组的扩展

1.扩展运算符

扩展运算符(spread)是三个点(...)。它好比 rest 参数的逆运算,将一个数组转为用逗号分隔的参数序列。

  1. console.log(...[1, 2, 3])
  2. // 1 2 3
  3. console.log(1, ...[2, 3, 4], 5)
  4. // 1 2 3 4 5
  5. [...document.querySelectorAll('div')]
  6. // [<div>, <div>, <div>]

该运算符主要用于函数调用。

  1. function push(array, ...items) {
  2. array.push(...items);
  3. }
  4. function add(x, y) {
  5. return x + y;
  6. }
  7. const numbers = [4, 38];
  8. add(...numbers) // 42

上面代码中,array.push(...items)add(...numbers)这两行,都是函数的调用,它们的都使用了扩展运算符。该运算符将一个数组,变为参数序列。
扩展运算符与正常的函数参数可以结合使用,非常灵活。

  1. function f(v, w, x, y, z) { }
  2. const args = [0, 1];
  3. f(-1, ...args, 2, ...[3]);

扩展运算符后面还可以放置表达式。

  1. const arr = [
  2. ...(x > 0 ? ['a'] : []),
  3. 'b',
  4. ];

如果扩展运算符后面是一个空数组,则不产生任何效果。

  1. [...[], 1]
  2. // [1]

注意,只有函数调用时,扩展运算符才可以放在圆括号中,否则会报错。

  1. (...[1, 2])
  2. // Uncaught SyntaxError: Unexpected number
  3. console.log((...[1, 2]))
  4. // Uncaught SyntaxError: Unexpected number
  5. console.log(...[1, 2])
  6. // 1 2

上面三种情况,扩展运算符都放在圆括号里面,但是前两种情况会报错,因为扩展运算符所在的括号不是函数调用。

2.Array.from()

Array.from方法用于将两类对象转为真正的数组:类似数组的对象(array-like object)和可遍历(iterable)的对象(包括 ES6 新增的数据结构 Set 和 Map)。
扩展运算符背后调用的是遍历器接口(Symbol.iterator),如果一个对象没有部署这个接口,就无法转换。Array.from方法还支持类似数组的对象。所谓类似数组的对象,本质特征只有一点,即必须有length属性。因此,任何有length属性的对象,都可以通过Array.from方法转为数组,而此时扩展运算符就无法转换。

  1. Array.from({ length: 3 });
  2. // [ undefined, undefined, undefined ]

上面代码中,Array.from返回了一个具有三个成员的数组,每个位置的值都是undefined。扩展运算符转换不了这个对象。
对于还没有部署该方法的浏览器,可以用Array.prototype.slice方法替代。

  1. const toArray = (() =>
  2. Array.from ? Array.from : obj => [].slice.call(obj)
  3. )();

Array.from还可以接受第二个参数,作用类似于数组的map方法,用来对每个元素进行处理,将处理后的值放入返回的数组。

  1. Array.from(arrayLike, x => x * x);
  2. // 等同于
  3. Array.from(arrayLike).map(x => x * x);
  4. Array.from([1, 2, 3], (x) => x * x)
  5. // [1, 4, 9]

3.Array.of()

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

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

这个方法的主要目的,是弥补数组构造函数Array()的不足。因为参数个数的不同,会导致Array()的行为有差异。

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

上面代码中,Array方法没有参数、一个参数、三个参数时,返回结果都不一样。只有当参数个数不少于 2 个时,Array()才会返回由参数组成的新数组。参数个数只有一个时,实际上是指定数组的长度。
Array.of基本上可以用来替代Array()new Array(),并且不存在由于参数不同而导致的重载。它的行为非常统一。

  1. Array.of() // []
  2. Array.of(undefined) // [undefined]
  3. Array.of(1) // [1]
  4. Array.of(1, 2) // [1, 2]

Array.of总是返回参数值组成的数组。如果没有参数,就返回一个空数组。
Array.of方法可以用下面的代码模拟实现。

  1. function ArrayOf(){
  2. return [].slice.call(arguments);
  3. }

4.数组实例的 copyWithin()

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

  1. Array.prototype.copyWithin(target, start = 0, end = this.length)

它接受三个参数。

  • target(必需):从该位置开始替换数据。如果为负值,表示倒数。
  • start(可选):从该位置开始读取数据,默认为 0。如果为负值,表示从末尾开始计算。
  • end(可选):到该位置前停止读取数据,默认等于数组长度。如果为负值,表示从末尾开始计算。

这三个参数都应该是数值,如果不是,会自动转为数值。

  1. [1, 2, 3, 4, 5].copyWithin(0, 3)
  2. // [4, 5, 3, 4, 5]

上面代码表示将从 3 号位直到数组结束的成员(4 和 5),复制到从 0 号位开始的位置,结果覆盖了原来的 1 和 2。
下面是更多例子。

  1. // 将3号位复制到0号位
  2. [1, 2, 3, 4, 5].copyWithin(0, 3, 4)
  3. // [4, 2, 3, 4, 5]
  4. // -2相当于3号位,-1相当于4号位
  5. [1, 2, 3, 4, 5].copyWithin(0, -2, -1)
  6. // [4, 2, 3, 4, 5]
  7. // 将3号位复制到0号位
  8. [].copyWithin.call({length: 5, 3: 1}, 0, 3)
  9. // {0: 1, 3: 1, length: 5}
  10. // 将2号位到数组结束,复制到0号位
  11. let i32a = new Int32Array([1, 2, 3, 4, 5]);
  12. i32a.copyWithin(0, 2);
  13. // Int32Array [3, 4, 5, 4, 5]
  14. // 对于没有部署 TypedArray 的 copyWithin 方法的平台
  15. // 需要采用下面的写法
  16. [].copyWithin.call(new Int32Array([1, 2, 3, 4, 5]), 0, 3, 4);
  17. // Int32Array [4, 2, 3, 4, 5]

5.数组实例的 find() 和 findIndex()

数组实例的find方法,用于找出第一个符合条件的数组成员。它的参数是一个回调函数,所有数组成员依次执行该回调函数,直到找出第一个返回值为true的成员,然后返回该成员。如果没有符合条件的成员,则返回undefined

  1. [1, 4, -5, 10].find((n) => n < 0)
  2. // -5

上面代码找出数组中第一个小于 0 的成员。

  1. [1, 5, 10, 15].find(function(value, index, arr) {
  2. return value > 9;
  3. }) // 10

上面代码中,find方法的回调函数可以接受三个参数,依次为当前的值、当前的位置和原数组。
数组实例的findIndex方法的用法与find方法非常类似,返回第一个符合条件的数组成员的位置,如果所有成员都不符合条件,则返回-1

  1. [1, 5, 10, 15].findIndex(function(value, index, arr) {
  2. return value > 9;
  3. }) // 2

这两个方法都可以接受第二个参数,用来绑定回调函数的this对象。

  1. function f(v){
  2. return v > this.age;
  3. }
  4. let person = {name: 'John', age: 20};
  5. [10, 12, 26, 15].find(f, person); // 26

上面的代码中,find函数接收了第二个参数person对象,回调函数中的this对象指向person对象。
另外,这两个方法都可以发现NaN,弥补了数组的indexOf方法的不足。

  1. [NaN].indexOf(NaN)
  2. // -1
  3. [NaN].findIndex(y => Object.is(NaN, y))
  4. // 0

上面代码中,indexOf方法无法识别数组的NaN成员,但是findIndex方法可以借助Object.is方法做到。

6.数组实例的 fill()

fill方法使用给定值,填充一个数组。

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

上面代码表明,fill方法用于空数组的初始化非常方便。数组中已有的元素,会被全部抹去。
fill方法还可以接受第二个和第三个参数,用于指定填充的起始位置和结束位置。

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

上面代码表示,fill方法从 1 号位开始,向原数组填充 7,到 2 号位之前结束。
注意,如果填充的类型为对象,那么被赋值的是同一个内存地址的对象,而不是深拷贝对象。

  1. let arr = new Array(3).fill({name: "Mike"});
  2. arr[0].name = "Ben";
  3. arr
  4. // [{name: "Ben"}, {name: "Ben"}, {name: "Ben"}]
  5. let arr = new Array(3).fill([]);
  6. arr[0].push(5);
  7. arr
  8. // [[5], [5], [5]]

7.数组实例的 entries(),keys() 和 values()

ES6 提供三个新的方法——entries()keys()values()——用于遍历数组。它们都返回一个遍历器对象(详见《Iterator》一章),可以用for...of循环进行遍历,唯一的区别是keys()是对键名的遍历、values()是对键值的遍历,entries()是对键值对的遍历。

  1. for (let index of ['a', 'b'].keys()) {
  2. console.log(index);
  3. }
  4. // 0
  5. // 1
  6. for (let elem of ['a', 'b'].values()) {
  7. console.log(elem);
  8. }
  9. // 'a'
  10. // 'b'
  11. for (let [index, elem] of ['a', 'b'].entries()) {
  12. console.log(index, elem);
  13. }
  14. // 0 "a"
  15. // 1 "b"

如果不使用for...of循环,可以手动调用遍历器对象的next方法,进行遍历。

  1. let letter = ['a', 'b', 'c'];
  2. let entries = letter.entries();
  3. console.log(entries.next().value); // [0, 'a']
  4. console.log(entries.next().value); // [1, 'b']
  5. console.log(entries.next().value); // [2, 'c']

8.数组实例的 includes()

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

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

该方法的第二个参数表示搜索的起始位置,默认为0。如果第二个参数为负数,则表示倒数的位置,如果这时它大于数组长度(比如第二个参数为-4,但数组长度为3),则会重置为从0开始。

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

没有该方法之前,我们通常使用数组的indexOf方法,检查是否包含某个值。

  1. if (arr.indexOf(el) !== -1) {
  2. // ...
  3. }

indexOf方法有两个缺点,一是不够语义化,它的含义是找到参数值的第一个出现位置,所以要去比较是否不等于-1,表达起来不够直观。二是,它内部使用严格相等运算符(===)进行判断,这会导致对NaN的误判。

  1. [NaN].indexOf(NaN)
  2. // -1

includes使用的是不一样的判断算法,就没有这个问题。

  1. [NaN].includes(NaN)
  2. // true

下面代码用来检查当前环境是否支持该方法,如果不支持,部署一个简易的替代版本。

  1. const contains = (() =>
  2. Array.prototype.includes
  3. ? (arr, value) => arr.includes(value)
  4. : (arr, value) => arr.some(el => el === value)
  5. )();
  6. 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. [1, 2, [3, 4]].flat()
    2. // [1, 2, 3, 4]

    上面代码中,原数组的成员里面有一个数组,flat()方法将子数组的成员取出来,添加在原来的位置。
    flat()默认只会“拉平”一层,如果想要“拉平”多层的嵌套数组,可以将flat()方法的参数写成一个整数,表示想要拉平的层数,默认为1。

    1. [1, 2, [3, [4, 5]]].flat()
    2. // [1, 2, 3, [4, 5]]
    3. [1, 2, [3, [4, 5]]].flat(2)
    4. // [1, 2, 3, 4, 5]

    上面代码中,flat()的参数为2,表示要“拉平”两层的嵌套数组。
    如果不管有多少层嵌套,都要转成一维数组,可以用Infinity关键字作为参数。

    1. [1, [2, [3]]].flat(Infinity)
    2. // [1, 2, 3]

    如果原数组有空位,flat()方法会跳过空位。

    1. [1, 2, , 4, 5].flat()
    2. // [1, 2, 4, 5]

    flatMap()方法对原数组的每个成员执行一个函数(相当于执行Array.prototype.map()),然后对返回值组成的数组执行flat()方法。该方法返回一个新数组,不改变原数组。

    1. // 相当于 [[2, 4], [3, 6], [4, 8]].flat()
    2. [2, 3, 4].flatMap((x) => [x, x * 2])
    3. // [2, 4, 3, 6, 4, 8]

    flatMap()只能展开一层数组。

    1. // 相当于 [[[2]], [[4]], [[6]], [[8]]].flat()
    2. [1, 2, 3, 4].flatMap(x => [[x * 2]])
    3. // [[2], [4], [6], [8]]

    上面代码中,遍历函数返回的是一个双层的数组,但是默认只能展开一层,因此flatMap()返回的还是一个嵌套数组。
    flatMap()方法的参数是一个遍历函数,该函数可以接受三个参数,分别是当前数组成员、当前数组成员的位置(从零开始)、原数组。

    1. arr.flatMap(function callback(currentValue[, index[, array]]) {
    2. // ...
    3. }[, thisArg])

    flatMap()方法还可以有第二个参数,用来绑定遍历函数里面的this

    10.数组的空位

    数组的空位指,数组的某一个位置没有任何值。比如,Array构造函数返回的数组都是空位。

    1. Array(3) // [, , ,]

    上面代码中,Array(3)返回一个具有 3 个空位的数组。
    注意,空位不是undefined,一个位置的值等于undefined,依然是有值的。空位是没有任何值,in运算符可以说明这一点。

    1. 0 in [undefined, undefined, undefined] // true
    2. 0 in [, , ,] // false

    上面代码说明,第一个数组的 0 号位置是有值的,第二个数组的 0 号位置没有值。
    Array.from方法会将数组的空位,转为undefined,也就是说,这个方法不会忽略空位。

    1. Array.from(['a',,'b'])
    2. // [ "a", undefined, "b" ]

    扩展运算符(...)也会将空位转为undefined

    1. [...['a',,'b']]
    2. // [ "a", undefined, "b" ]

    copyWithin()会连空位一起拷贝。

    1. [,'a','b',,].copyWithin(2,0) // [,"a",,"a"]

    fill()会将空位视为正常的数组位置。

    1. new Array(3).fill('a') // ["a","a","a"]

    for...of循环也会遍历空位。

    1. let arr = [, ,];
    2. for (let i of arr) {
    3. console.log(1);
    4. }
    5. // 1
    6. // 1

    上面代码中,数组arr有两个空位,for...of并没有忽略它们。如果改成map方法遍历,空位是会跳过的。
    entries()keys()values()find()findIndex()会将空位处理成undefined

    1. // entries()
    2. [...[,'a'].entries()] // [[0,undefined], [1,"a"]]
    3. // keys()
    4. [...[,'a'].keys()] // [0,1]
    5. // values()
    6. [...[,'a'].values()] // [undefined,"a"]
    7. // find()
    8. [,'a'].find(x => true) // undefined
    9. // findIndex()
    10. [,'a'].findIndex(x => true) // 0

    由于空位的处理规则非常不统一,所以建议避免出现空位。

    二丶数值的扩展

    1.Number.isFinite(), Number.isNaN()

    ES6 在Number对象上,新提供了Number.isFinite()Number.isNaN()两个方法。
    Number.isFinite()用来检查一个数值是否为有限的(finite),即不是Infinity

    1. Number.isFinite(15); // true
    2. Number.isFinite(0.8); // true
    3. Number.isFinite(NaN); // false
    4. Number.isFinite(Infinity); // false
    5. Number.isFinite(-Infinity); // false
    6. Number.isFinite('foo'); // false
    7. Number.isFinite('15'); // false
    8. Number.isFinite(true); // false

    注意,如果参数类型不是数值,Number.isFinite一律返回false
    Number.isNaN()用来检查一个值是否为NaN

    1. Number.isNaN(NaN) // true
    2. Number.isNaN(15) // false
    3. Number.isNaN('15') // false
    4. Number.isNaN(true) // false
    5. Number.isNaN(9/NaN) // true
    6. Number.isNaN('true' / 0) // true
    7. Number.isNaN('true' / 'true') // true

    如果参数类型不是NaNNumber.isNaN一律返回false
    它们与传统的全局方法isFinite()isNaN()的区别在于,传统方法先调用Number()将非数值的值转为数值,再进行判断,而这两个新方法只对数值有效,Number.isFinite()对于非数值一律返回false, Number.isNaN()只有对于NaN才返回true,非NaN一律返回false

    1. isFinite(25) // true
    2. isFinite("25") // true
    3. Number.isFinite(25) // true
    4. Number.isFinite("25") // false
    5. isNaN(NaN) // true
    6. isNaN("NaN") // true
    7. Number.isNaN(NaN) // true
    8. Number.isNaN("NaN") // false
    9. Number.isNaN(1) // false

    2.Number.parseInt(), Number.parseFloat()

    ES6 将全局方法parseInt()parseFloat(),移植到Number对象上面,行为完全保持不变。

    1. // ES5的写法
    2. parseInt('12.34') // 12
    3. parseFloat('123.45#') // 123.45
    4. // ES6的写法
    5. Number.parseInt('12.34') // 12
    6. Number.parseFloat('123.45#') // 123.45

    这样做的目的,是逐步减少全局性方法,使得语言逐步模块化。

    1. Number.parseInt === parseInt // true
    2. Number.parseFloat === parseFloat // true

    3.Number.isInteger()

    Number.isInteger()用来判断一个数值是否为整数。

    1. Number.isInteger(25) // true
    2. Number.isInteger(25.1) // false

    JavaScript 内部,整数和浮点数采用的是同样的储存方法,所以 25 和 25.0 被视为同一个值。

    1. Number.isInteger(25) // true
    2. Number.isInteger(25.0) // true

    如果参数不是数值,Number.isInteger返回false

    1. Number.isInteger() // false
    2. Number.isInteger(null) // false
    3. Number.isInteger('15') // false
    4. Number.isInteger(true) // false

    注意,由于 JavaScript 采用 IEEE 754 标准,数值存储为64位双精度格式,数值精度最多可以达到 53 个二进制位(1 个隐藏位与 52 个有效位)。如果数值的精度超过这个限度,第54位及后面的位就会被丢弃,这种情况下,Number.isInteger可能会误判。

    1. Number.isInteger(3.0000000000000002) // true

    上面代码中,Number.isInteger的参数明明不是整数,但是会返回true。原因就是这个小数的精度达到了小数点后16个十进制位,转成二进制位超过了53个二进制位,导致最后的那个2被丢弃了。
    类似的情况还有,如果一个数值的绝对值小于Number.MIN_VALUE(5E-324),即小于 JavaScript 能够分辨的最小值,会被自动转为 0。这时,Number.isInteger也会误判。

    1. Number.isInteger(5E-324) // false
    2. Number.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 次方。

    1. Number.EPSILON === Math.pow(2, -52)
    2. // true
    3. Number.EPSILON
    4. // 2.220446049250313e-16
    5. Number.EPSILON.toFixed(20)
    6. // "0.00000000000000022204"

    Number.EPSILON实际上是 JavaScript 能够表示的最小精度。误差如果小于这个值,就可以认为已经没有意义了,即不存在误差了。
    引入一个这么小的量的目的,在于为浮点数计算,设置一个误差范围。我们知道浮点数计算是不精确的。

    1. 0.1 + 0.2
    2. // 0.30000000000000004
    3. 0.1 + 0.2 - 0.3
    4. // 5.551115123125783e-17
    5. 5.551115123125783e-17.toFixed(20)
    6. // '0.00000000000000005551'

    上面代码解释了,为什么比较0.1 + 0.20.3得到的结果是false

    1. 0.1 + 0.2 === 0.3 // false

    Number.EPSILON可以用来设置“能够接受的误差范围”。比如,误差范围设为 2 的-50 次方(即Number.EPSILON * Math.pow(2, 2)),即如果两个浮点数的差小于这个值,我们就认为这两个浮点数相等。

    1. 5.551115123125783e-17 < Number.EPSILON * Math.pow(2, 2)
    2. // true

    因此,Number.EPSILON的实质是一个可以接受的最小误差范围。

    1. function withinErrorMargin (left, right) {
    2. return Math.abs(left - right) < Number.EPSILON * Math.pow(2, 2);
    3. }
    4. 0.1 + 0.2 === 0.3 // false
    5. withinErrorMargin(0.1 + 0.2, 0.3) // true
    6. 1.1 + 1.3 === 2.4 // false
    7. withinErrorMargin(1.1 + 1.3, 2.4) // true

    上面的代码为浮点数运算,部署了一个误差检查函数。

    5.安全整数和 Number.isSafeInteger()

    JavaScript 能够准确表示的整数范围在-2^532^53之间(不含两个端点),超过这个范围,无法精确表示这个值。

    1. Math.pow(2, 53) // 9007199254740992
    2. 9007199254740992 // 9007199254740992
    3. 9007199254740993 // 9007199254740992
    4. Math.pow(2, 53) === Math.pow(2, 53) + 1
    5. // true

    上面代码中,超出 2 的 53 次方之后,一个数就不精确了。
    ES6 引入了Number.MAX_SAFE_INTEGERNumber.MIN_SAFE_INTEGER这两个常量,用来表示这个范围的上下限。

    1. Number.MAX_SAFE_INTEGER === Math.pow(2, 53) - 1
    2. // true
    3. Number.MAX_SAFE_INTEGER === 9007199254740991
    4. // true
    5. Number.MIN_SAFE_INTEGER === -Number.MAX_SAFE_INTEGER
    6. // true
    7. Number.MIN_SAFE_INTEGER === -9007199254740991
    8. // true

    上面代码中,可以看到 JavaScript 能够精确表示的极限。
    Number.isSafeInteger()则是用来判断一个整数是否落在这个范围之内。

    1. Number.isSafeInteger('a') // false
    2. Number.isSafeInteger(null) // false
    3. Number.isSafeInteger(NaN) // false
    4. Number.isSafeInteger(Infinity) // false
    5. Number.isSafeInteger(-Infinity) // false
    6. Number.isSafeInteger(3) // true
    7. Number.isSafeInteger(1.2) // false
    8. Number.isSafeInteger(9007199254740990) // true
    9. Number.isSafeInteger(9007199254740992) // false
    10. Number.isSafeInteger(Number.MIN_SAFE_INTEGER - 1) // false
    11. Number.isSafeInteger(Number.MIN_SAFE_INTEGER) // true
    12. Number.isSafeInteger(Number.MAX_SAFE_INTEGER) // true
    13. Number.isSafeInteger(Number.MAX_SAFE_INTEGER + 1) // false

    这个函数的实现很简单,就是跟安全整数的两个边界值比较一下。

    1. Number.isSafeInteger = function (n) {
    2. return (typeof n === 'number' &&
    3. Math.round(n) === n &&
    4. Number.MIN_SAFE_INTEGER <= n &&
    5. n <= Number.MAX_SAFE_INTEGER);
    6. }

    实际使用这个函数时,需要注意。验证运算结果是否落在安全整数的范围内,不要只验证运算结果,而要同时验证参与运算的每个值。

    1. Number.isSafeInteger(9007199254740993)
    2. // false
    3. Number.isSafeInteger(990)
    4. // true
    5. Number.isSafeInteger(9007199254740993 - 990)
    6. // true
    7. 9007199254740993 - 990
    8. // 返回结果 9007199254740002
    9. // 正确答案应该是 9007199254740003

    上面代码中,9007199254740993不是一个安全整数,但是Number.isSafeInteger会返回结果,显示计算结果是安全的。这是因为,这个数超出了精度范围,导致在计算机内部,以9007199254740992的形式储存。

    1. 9007199254740993 === 9007199254740992
    2. // true

    所以,如果只验证运算结果是否为安全整数,很可能得到错误结果。下面的函数可以同时验证两个运算数和运算结果。

    1. function trusty (left, right, result) {
    2. if (
    3. Number.isSafeInteger(left) &&
    4. Number.isSafeInteger(right) &&
    5. Number.isSafeInteger(result)
    6. ) {
    7. return result;
    8. }
    9. throw new RangeError('Operation cannot be trusted!');
    10. }
    11. trusty(9007199254740993, 990, 9007199254740993 - 990)
    12. // RangeError: Operation cannot be trusted!
    13. trusty(1, 2, 3)
    14. // 3

    6.Math 对象的扩展

    ES6 在 Math 对象上新增了 17 个与数学相关的方法。所有这些方法都是静态方法,只能在 Math 对象上调用。

    1.Math.trunc()

    Math.trunc方法用于去除一个数的小数部分,返回整数部分。

    1. Math.trunc(4.1) // 4
    2. Math.trunc(4.9) // 4
    3. Math.trunc(-4.1) // -4
    4. Math.trunc(-4.9) // -4
    5. Math.trunc(-0.1234) // -0

    对于非数值,Math.trunc内部使用Number方法将其先转为数值。

    1. Math.trunc('123.456') // 123
    2. Math.trunc(true) //1
    3. Math.trunc(false) // 0
    4. Math.trunc(null) // 0

    对于空值和无法截取整数的值,返回NaN

    1. Math.trunc(NaN); // NaN
    2. Math.trunc('foo'); // NaN
    3. Math.trunc(); // NaN
    4. Math.trunc(undefined) // NaN

    对于没有部署这个方法的环境,可以用下面的代码模拟。

    1. Math.trunc = Math.trunc || function(x) {
    2. return x < 0 ? Math.ceil(x) : Math.floor(x);
    3. };

    2.Math.sign()

    Math.sign方法用来判断一个数到底是正数、负数、还是零。对于非数值,会先将其转换为数值。
    它会返回五种值。

  • 参数为正数,返回+1

  • 参数为负数,返回-1
  • 参数为 0,返回0
  • 参数为-0,返回-0;
  • 其他值,返回NaN

    1. Math.sign(-5) // -1
    2. Math.sign(5) // +1
    3. Math.sign(0) // +0
    4. Math.sign(-0) // -0
    5. Math.sign(NaN) // NaN

    如果参数是非数值,会自动转为数值。对于那些无法转为数值的值,会返回NaN

    1. Math.sign('') // 0
    2. Math.sign(true) // +1
    3. Math.sign(false) // 0
    4. Math.sign(null) // 0
    5. Math.sign('9') // +1
    6. Math.sign('foo') // NaN
    7. Math.sign() // NaN
    8. Math.sign(undefined) // NaN

    对于没有部署这个方法的环境,可以用下面的代码模拟。

    1. Math.sign = Math.sign || function(x) {
    2. x = +x; // convert to a number
    3. if (x === 0 || isNaN(x)) {
    4. return x;
    5. }
    6. return x > 0 ? 1 : -1;
    7. };

    3.Math.cbrt()

    Math.cbrt方法用于计算一个数的立方根。

    1. Math.cbrt(-1) // -1
    2. Math.cbrt(0) // 0
    3. Math.cbrt(1) // 1
    4. Math.cbrt(2) // 1.2599210498948734

    对于非数值,Math.cbrt方法内部也是先使用Number方法将其转为数值。

    1. Math.cbrt('8') // 2
    2. Math.cbrt('hello') // NaN

    对于没有部署这个方法的环境,可以用下面的代码模拟。

    1. Math.cbrt = Math.cbrt || function(x) {
    2. var y = Math.pow(Math.abs(x), 1/3);
    3. return x < 0 ? -y : y;
    4. };

    4.Math.clz32()

    Math.clz32()方法将参数转为 32 位无符号整数的形式,然后返回这个 32 位值里面有多少个前导 0。

    1. Math.clz32(0) // 32
    2. Math.clz32(1) // 31
    3. Math.clz32(1000) // 22
    4. Math.clz32(0b01000000000000000000000000000000) // 1
    5. Math.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方法直接相关。

    1. Math.clz32(0) // 32
    2. Math.clz32(1) // 31
    3. Math.clz32(1 << 1) // 30
    4. Math.clz32(1 << 2) // 29
    5. Math.clz32(1 << 29) // 2

    对于小数,Math.clz32方法只考虑整数部分。

    1. Math.clz32(3.2) // 30
    2. Math.clz32(3.9) // 30

    对于空值或其他类型的值,Math.clz32方法会将它们先转为数值,然后再计算。

    1. Math.clz32() // 32
    2. Math.clz32(NaN) // 32
    3. Math.clz32(Infinity) // 32
    4. Math.clz32(null) // 32
    5. Math.clz32('foo') // 32
    6. Math.clz32([]) // 32
    7. Math.clz32({}) // 32
    8. Math.clz32(true) // 31

    5.Math.imul()

    Math.imul方法返回两个数以 32 位带符号整数形式相乘的结果,返回的也是一个 32 位的带符号整数。

    1. Math.imul(2, 4) // 8
    2. Math.imul(-1, 8) // -8
    3. Math.imul(-2, -2) // 4

    如果只考虑最后 32 位,大多数情况下,Math.imul(a, b)a * b的结果是相同的,即该方法等同于(a * b)|0的效果(超过 32 位的部分溢出)。之所以需要部署这个方法,是因为 JavaScript 有精度限制,超过 2 的 53 次方的值无法精确表示。这就是说,对于那些很大的数的乘法,低位数值往往都是不精确的,Math.imul方法可以返回正确的低位数值。

    1. (0x7fffffff * 0x7fffffff)|0 // 0

    上面这个乘法算式,返回结果为 0。但是由于这两个二进制数的最低位都是 1,所以这个结果肯定是不正确的,因为根据二进制乘法,计算结果的二进制最低位应该也是 1。这个错误就是因为它们的乘积超过了 2 的 53 次方,JavaScript 无法保存额外的精度,就把低位的值都变成了 0。Math.imul方法可以返回正确的值 1。

    1. Math.imul(0x7fffffff, 0x7fffffff) // 1

    6.Math.fround()

    Math.fround方法返回一个数的32位单精度浮点数形式。
    对于32位单精度格式来说,数值精度是24个二进制位(1 位隐藏位与 23 位有效位),所以对于 -2 至 2 之间的整数(不含两个端点),返回结果与参数本身一致。

    1. Math.fround(0) // 0
    2. Math.fround(1) // 1
    3. Math.fround(2 ** 24 - 1) // 16777215

    如果参数的绝对值大于 2,返回的结果便开始丢失精度。

    1. Math.fround(2 ** 24) // 16777216
    2. Math.fround(2 ** 24 + 1) // 16777216

    Math.fround方法的主要作用,是将64位双精度浮点数转为32位单精度浮点数。如果小数的精度超过24个二进制位,返回值就会不同于原值,否则返回值不变(即与64位双精度值一致)。

    1. // 未丢失有效精度
    2. Math.fround(1.125) // 1.125
    3. Math.fround(7.25) // 7.25
    4. // 丢失精度
    5. Math.fround(0.3) // 0.30000001192092896
    6. Math.fround(0.7) // 0.699999988079071
    7. Math.fround(1.0000000123) // 1

    对于 NaNInfinity,此方法返回原值。对于其它类型的非数值,Math.fround 方法会先将其转为数值,再返回单精度浮点数。

    1. Math.fround(NaN) // NaN
    2. Math.fround(Infinity) // Infinity
    3. Math.fround('5') // 5
    4. Math.fround(true) // 1
    5. Math.fround(null) // 0
    6. Math.fround([]) // 0
    7. Math.fround({}) // NaN

    对于没有部署这个方法的环境,可以用下面的代码模拟。

    1. Math.fround = Math.fround || function (x) {
    2. return new Float32Array([x])[0];
    3. };

    7.Math.hypot()

    Math.hypot方法返回所有参数的平方和的平方根。

    1. Math.hypot(3, 4); // 5
    2. Math.hypot(3, 4, 5); // 7.0710678118654755
    3. Math.hypot(); // 0
    4. Math.hypot(NaN); // NaN
    5. Math.hypot(3, 4, 'foo'); // NaN
    6. Math.hypot(3, 4, '5'); // 7.0710678118654755
    7. Math.hypot(-3); // 3

    上面代码中,3 的平方加上 4 的平方,等于 5 的平方。
    如果参数不是数值,Math.hypot方法会将其转为数值。只要有一个参数无法转为数值,就会返回 NaN。

    8.对数方法

    ES6 新增了 4 个对数相关方法。

    (1) Math.expm1()

    Math.expm1(x)返回 e - 1,即Math.exp(x) - 1

    1. Math.expm1(-1) // -0.6321205588285577
    2. Math.expm1(0) // 0
    3. Math.expm1(1) // 1.718281828459045

    对于没有部署这个方法的环境,可以用下面的代码模拟。

    1. Math.expm1 = Math.expm1 || function(x) {
    2. return Math.exp(x) - 1;
    3. };

    (2)Math.log1p()

    Math.log1p(x)方法返回1 + x的自然对数,即Math.log(1 + x)。如果x小于-1,返回NaN

    1. Math.log1p(1) // 0.6931471805599453
    2. Math.log1p(0) // 0
    3. Math.log1p(-1) // -Infinity
    4. Math.log1p(-2) // NaN

    对于没有部署这个方法的环境,可以用下面的代码模拟。

    1. Math.log1p = Math.log1p || function(x) {
    2. return Math.log(1 + x);
    3. };

    (3)Math.log10()

    Math.log10(x)返回以 10 为底的x的对数。如果x小于 0,则返回 NaN。

    1. Math.log10(2) // 0.3010299956639812
    2. Math.log10(1) // 0
    3. Math.log10(0) // -Infinity
    4. Math.log10(-2) // NaN
    5. Math.log10(100000) // 5

    对于没有部署这个方法的环境,可以用下面的代码模拟。

    1. Math.log10 = Math.log10 || function(x) {
    2. return Math.log(x) / Math.LN10;
    3. };

    (4)Math.log2()

    Math.log2(x)返回以 2 为底的x的对数。如果x小于 0,则返回 NaN。

    1. Math.log2(3) // 1.584962500721156
    2. Math.log2(2) // 1
    3. Math.log2(1) // 0
    4. Math.log2(0) // -Infinity
    5. Math.log2(-2) // NaN
    6. Math.log2(1024) // 10
    7. Math.log2(1 << 29) // 29

    对于没有部署这个方法的环境,可以用下面的代码模拟。

    1. Math.log2 = Math.log2 || function(x) {
    2. return Math.log(x) / Math.LN2;
    3. };

    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 新增了一个指数运算符(**)。

    1. 2 ** 2 // 4
    2. 2 ** 3 // 8

    这个运算符的一个特点是右结合,而不是常见的左结合。多个指数运算符连用时,是从最右边开始计算的。

    1. // 相当于 2 ** (3 ** 2)
    2. 2 ** 3 ** 2
    3. // 512

    上面代码中,首先计算的是第二个指数运算符,而不是第一个。
    指数运算符可以与等号结合,形成一个新的赋值运算符(**=)。

    1. let a = 1.5;
    2. a **= 2;
    3. // 等同于 a = a * a;
    4. let b = 4;
    5. b **= 3;
    6. // 等同于 b = b * b * b;

    注意,V8 引擎的指数运算符与Math.pow的实现不相同,对于特别大的运算结果,两者会有细微的差异。

    1. Math.pow(99, 99)
    2. // 3.697296376497263e+197
    3. 99 ** 99
    4. // 3.697296376497268e+197

    上面代码中,两个运算结果的最后一位有效数字是有差异的。

    三丶对象的扩展

    1.属性的遍历

    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 键,按照加入时间升序排列。
    1. Reflect.ownKeys({ [Symbol()]:0, b:0, 10:0, 2:0, a:0 })
    2. // ['2', '10', 'b', 'a', Symbol()]
    上面代码中,Reflect.ownKeys方法返回一个数组,包含了参数对象的所有属性。这个数组的属性次序是这样的,首先是数值属性210,其次是字符串属性ba,最后是 Symbol 属性。

    2.super 关键字

    我们知道,this关键字总是指向函数所在的当前对象,ES6 又新增了另一个类似的关键字super,指向当前对象的原型对象。
    1. const proto = {
    2. foo: 'hello'
    3. };
    4. const obj = {
    5. foo: 'world',
    6. find() {
    7. return super.foo;
    8. }
    9. };
    10. Object.setPrototypeOf(obj, proto);
    11. obj.find() // "hello"
    上面代码中,对象obj.find()方法之中,通过super.foo引用了原型对象protofoo属性。
    注意,super关键字表示原型对象时,只能用在对象的方法之中,用在其他地方都会报错。
    1. // 报错
    2. const obj = {
    3. foo: super.foo
    4. }
    5. // 报错
    6. const obj = {
    7. foo: () => super.foo
    8. }
    9. // 报错
    10. const obj = {
    11. foo: function () {
    12. return super.foo
    13. }
    14. }
    上面三种super的用法都会报错,因为对于 JavaScript 引擎来说,这里的super都没有用在对象的方法之中。第一种写法是super用在属性里面,第二种和第三种写法是super用在一个函数里面,然后赋值给foo属性。目前,只有对象方法的简写法可以让 JavaScript 引擎确认,定义的是对象的方法。
    JavaScript 引擎内部,super.foo等同于Object.getPrototypeOf(this).foo(属性)或Object.getPrototypeOf(this).foo.call(this)(方法)。
    1. const proto = {
    2. x: 'hello',
    3. foo() {
    4. console.log(this.x);
    5. },
    6. };
    7. const obj = {
    8. x: 'world',
    9. foo() {
    10. super.foo();
    11. }
    12. }
    13. Object.setPrototypeOf(obj, proto);
    14. obj.foo() // "world"
    上面代码中,super.foo指向原型对象protofoo方法,但是绑定的this却还是当前对象obj,因此输出的就是world

    四丶对象的新增方法

    1.Object.is()

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

    2.Object.assign()

    Object.assign方法用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target)。
    1. const target = { a: 1 };
    2. const source1 = { b: 2 };
    3. const source2 = { c: 3 };
    4. Object.assign(target, source1, source2);
    5. target // {a:1, b:2, c:3}
    Object.assign方法的第一个参数是目标对象,后面的参数都是源对象。
    注意,如果目标对象与源对象有同名属性,或多个源对象有同名属性,则后面的属性会覆盖前面的属性。
    Object.assign拷贝的属性是有限制的,只拷贝源对象的自身属性(不拷贝继承属性),也不拷贝不可枚举的属性(enumerable: false)。
    1. Object.assign({b: 'c'},
    2. Object.defineProperty({}, 'invisible', {
    3. enumerable: false,
    4. value: 'hello'
    5. })
    6. )
    7. // { b: 'c' }
    上面代码中,Object.assign要拷贝的对象只有一个不可枚举属性invisible,这个属性并没有被拷贝进去。
    属性名为 Symbol 值的属性,也会被Object.assign拷贝。
    1. Object.assign({ a: 'b' }, { [Symbol('c')]: 'd' })
    2. // { a: 'b', Symbol(c): 'd' }
    1. let letter = ['a', 'b', 'c'];
    2. let entries = letter.entries();
    3. console.log(entries.next().value); // [0, 'a']
    4. console.log(entries.next().value); // [1, 'b']
    5. console.log(entries.next().value); // [2, 'c']

    注意点

    (1)浅拷贝

    Object.assign方法实行的是浅拷贝,而不是深拷贝。也就是说,如果源对象某个属性的值是对象,那么目标对象拷贝得到的是这个对象的引用。

    (2)同名属性的替换

    对于这种嵌套的对象,一旦遇到同名属性,Object.assign的处理方法是替换,而不是添加。
    1. const target = { a: { b: 'c', d: 'e' } }
    2. const source = { a: { b: 'hello' } }
    3. Object.assign(target, source)
    4. // { a: { b: 'hello' } }

    (3)数组的处理

    Object.assign可以用来处理数组,但是会把数组视为对象。
    1. Object.assign([1, 2, 3], [4, 5])
    2. // [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)都部署了这个属性。
    1. // es5 的写法
    2. const obj = {
    3. method: function() { ... }
    4. };
    5. obj.__proto__ = someOtherObj;
    6. // es6 的写法
    7. var obj = Object.create(someOtherObj);
    8. obj.method = function() { ... };
    该属性没有写入 ES6 的正文,而是写入了附录,原因是__proto__前后的双下划线,说明它本质上是一个内部属性,而不是一个正式的对外的 API,只是由于浏览器广泛支持,才被加入了 ES6。标准明确规定,只有浏览器必须部署这个属性,其他运行环境不一定需要部署,而且新的代码最好认为这个属性是不存在的。因此,无论从语义的角度,还是从兼容性的角度,都不要使用这个属性,而是使用下面的Object.setPrototypeOf()(写操作)、Object.getPrototypeOf()(读操作)、Object.create()(生成操作)代替。

    Object.setPrototypeOf()

    Object.setPrototypeOf方法的作用与__proto__相同,用来设置一个对象的prototype对象,返回参数对象本身。它是 ES6 正式推荐的设置原型对象的方法。
    1. // 格式
    2. Object.setPrototypeOf(object, prototype)
    3. // 用法
    4. const o = Object.setPrototypeOf({}, null);
    该方法等同于下面的函数。
    1. function setPrototypeOf(obj, proto) {
    2. obj.__proto__ = proto;
    3. return obj;
    4. }

    Object.getPrototypeOf()

    该方法与Object.setPrototypeOf方法配套,用于读取一个对象的原型对象。

    五丶Symbol

    1.概述

    ES6 引入了一种新的原始数据类型Symbol,表示独一无二的值。它是 JavaScript 语言的第七种数据类型,前六种是:undefinednull、布尔值(Boolean)、字符串(String)、数值(Number)、对象(Object)。
    Symbol 值通过Symbol函数生成。这就是说,对象的属性名现在可以有两种类型,一种是原来就有的字符串,另一种就是新增的 Symbol 类型。凡是属性名属于 Symbol 类型,就都是独一无二的,可以保证不会与其他属性名产生冲突。
    1. let s = Symbol();
    2. typeof s
    3. // "symbol"
    上面代码中,变量s就是一个独一无二的值。typeof运算符的结果,表明变量s是 Symbol 数据类型,而不是字符串之类的其他类型。
    注意,Symbol函数前不能使用new命令,否则会报错。这是因为生成的 Symbol 是一个原始类型的值,不是对象。也就是说,由于 Symbol 值不是对象,所以不能添加属性。基本上,它是一种类似于字符串的数据类型。
    Symbol函数可以接受一个字符串作为参数,表示对 Symbol 实例的描述,主要是为了在控制台显示,或者转为字符串时,比较容易区分。

    2.Symbol.prototype.description

    1. const sym = Symbol('foo');
    2. 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!”

  1. 上面代码通过方括号结构和`Object.defineProperty`,将对象的属性名指定为一个 Symbol 值。<br />注意,Symbol 值作为对象属性名时,不能用点运算符。
  2. ```javascript
  3. const mySymbol = Symbol();
  4. const a = {};
  5. a.mySymbol = 'Hello!';
  6. a[mySymbol] // undefined
  7. a['mySymbol'] // "Hello!"

4.属性名的遍历

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

  1. const obj = {};
  2. let a = Symbol('a');
  3. let b = Symbol('b');
  4. obj[a] = 'Hello';
  5. obj[b] = 'World';
  6. const objectSymbols = Object.getOwnPropertySymbols(obj);
  7. objectSymbols
  8. // [Symbol(a), Symbol(b)]

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

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

由于以 Symbol 值作为名称的属性,不会被常规方法遍历得到。我们可以利用这个特性,为对象定义一些非私有的、但又希望只用于内部的方法。

  1. let size = Symbol('size');
  2. class Collection {
  3. constructor() {
  4. this[size] = 0;
  5. }
  6. add(item) {
  7. this[this[size]] = item;
  8. this[size]++;
  9. }
  10. static sizeOf(instance) {
  11. return instance[size];
  12. }
  13. }
  14. let x = new Collection();
  15. Collection.sizeOf(x) // 0
  16. x.add('foo');
  17. Collection.sizeOf(x) // 1
  18. Object.keys(x) // ['0']
  19. Object.getOwnPropertyNames(x) // ['0']
  20. Object.getOwnPropertySymbols(x) // [Symbol(size)]

上面代码中,对象xsize属性是一个 Symbol 值,所以Object.keys(x)Object.getOwnPropertyNames(x)都无法获取它。这就造成了一种非私有的内部方法的效果。

5.Symbol.for(),Symbol.keyFor()

有时,我们希望重新使用同一个 Symbol 值,Symbol.for方法可以做到这一点。它接受一个字符串作为参数,然后搜索有没有以该参数作为名称的 Symbol 值。如果有,就返回这个 Symbol 值,否则就新建并返回一个以该字符串为名称的 Symbol 值。

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

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

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

上面代码中,由于Symbol()写法没有登记机制,所以每次调用都会返回一个不同的值。
Symbol.keyFor方法返回一个已登记的 Symbol 类型值的key

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

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

6.内置的 Symbol 值

除了定义自己使用的 Symbol 值以外,ES6 还提供了 11 个内置的 Symbol 值,指向语言内部使用的方法

1.Symbol.hasInstance

对象的Symbol.hasInstance属性,指向一个内部方法。当其他对象使用instanceof运算符,判断是否为该对象的实例时,会调用这个方法。比如,foo instanceof Foo在语言内部,实际调用的是Foo[Symbol.hasInstance](foo)

  1. class MyClass {
  2. [Symbol.hasInstance](foo) {
  3. return foo instanceof Array;
  4. }
  5. }
  6. [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:该场合需要转成字符串
  • Default:该场合可以转成数值,也可以转成字符串

    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环境排除。