,
]
function push(array, ...items) {
array.push(...items);
}
function add(x, y) {
return x + y;
}
const numbers = [4, 38];
add(...numbers) // 42
```
扩展运算符后面还可以放置表达式
```js
const arr = [
...(x > 0 ? ['a'] : []),
'b',
];
[...[], 1]
// [1]
//只有函数调用时,扩展运算符才可以放在圆括号中,否则会报错
(...[1, 2])
// Uncaught SyntaxError: Unexpected number
console.log((...[1, 2]))
// Uncaught SyntaxError: Unexpected number
console.log(...[1, 2])
// 1 2
```
#### 9.1.1 代替函数的apply方法
由于扩展运算符可以展开数组,所以不再需要`apply`方法,将数组转为函数的参数了
```js
// ES5 的写法
function f(x, y, z) {
// ...
}
var args = [0, 1, 2];
f.apply(null, args);
// ES6的写法
function f(x, y, z) {
// ...
}
let args = [0, 1, 2];
f(...args);
```
应用`Math.max` 方法,简化求出数组的最大元素的写法
```js
// ES5 的写法
Math.max.apply(null, [14, 3, 77])
// ES6 的写法
Math.max(...[14, 3, 77])
// 等同于
Math.max(14, 3, 77);
```
`push`函数,将一个数组添加到另一个数组的尾部
```js
// ES5的 写法
var arr1 = [0, 1, 2];
var arr2 = [3, 4, 5];
Array.prototype.push.apply(arr1, arr2);
// ES6 的写法
let arr1 = [0, 1, 2];
let arr2 = [3, 4, 5];
arr1.push(...arr2);
new (Date.bind.apply(Date,[null,2015,1,1]))
//Sun Feb 01 2015 00:00:00 GMT+0800 (中国标准时间)
new Date(...[2015,1,1])
//Sun Feb 01 2015 00:00:00 GMT+0800 (中国标准时间)
```
#### 9.1.2 扩展运算符的应用
**(1)赋值数组**
数组是复合的数据类型,直接复制的话,只是复制了指向底层数据结构的指针,而不是克隆一个全新的数组。
```js
const a1 = [1, 2];
const a2 = a1;
a2[0] = 2;
a1 // [2, 2]
```
上面代码中,`a2`并不是`a1`的克隆,而是指向同一份数据的另一个指针。修改`a2`,会直接导致`a1`的变化。
ES5 只能用变通方法来复制数组。
```js
const a1 = [1, 2];
const a2 = a1.concat();
a2[0] = 2;
a1 // [1, 2]
```
扩展运算符提供了复制数组的简便写法
```js
const a1 = [1, 2];
// 写法一
const a2 = [...a1];
// 写法二
const [...a2] = a1;
```
**(2)合并数组**
扩展运算符提供了数组合并的新写法
```js
const arr1 = ['a', 'b'];
const arr2 = ['c'];
const arr3 = ['d', 'e'];
// ES5 的合并数组
arr1.concat(arr2, arr3);
// [ 'a', 'b', 'c', 'd', 'e' ]
// ES6 的合并数组
[...arr1, ...arr2, ...arr3]
// [ 'a', 'b', 'c', 'd', 'e' ]
```
不过,这两种方法都是浅拷贝
```js
const a1 = [{ foo: 1 }];
const a2 = [{ bar: 2 }];
const a3 = a1.concat(a2);
const a4 = [...a1, ...a2];
a3[0] === a1[0] // true
a4[0] === a1[0] // true
```
**(3)与结构赋值结合**
扩展运算符可以与解构赋值结合起来,用于生成数组
```js
// ES5
a = list[0], rest = list.slice(1)
// ES6
[a, ...rest] = list
const [first, ...rest] = [1, 2, 3, 4, 5];
first // 1
rest // [2, 3, 4, 5]
const [first, ...rest] = [];
first // undefined
rest // []
const [first, ...rest] = ["foo"];
first // "foo"
rest // []
```
**(4)**字符串
扩展运算符还可以将字符串转为真正的数组
```js
[...'hello']
// [ "h", "e", "l", "l", "o" ]
//采用扩展运算符返回字符串长度的函数
function length(str) {
return [...str].length;
}
length('x\uD83D\uDE80y') // 3
let str = 'x\uD83D\uDE80y';
str.split('').reverse().join('')
// 'y\uDE80\uD83Dx'
[...str].reverse().join('')
// 'y\uD83D\uDE80x'
```
**(5)**实现Iterator接口的对象
任何定义了遍历器(Iterator)接口的对象(参阅 Iterator 一章),都可以用扩展运算符转为真正的数组
```js
let nodeList = document.querySelectorAll('div');
let array = [...nodeList];
Number.prototype[Symbol.iterator] = function*() {
let i = 0;
let num = this.valueOf();
while (i < num) {
yield i++;
}
}
console.log([...5]) // [0, 1, 2, 3, 4]
```
**(6)**Map 和 Set 结构,Generator 函数
扩展运算符内部调用的是数据结构的 Iterator 接口,因此只要具有 Iterator 接口的对象,都可以使用扩展运算符,比如 Map 结构。
```js
let map = new Map([
[1, 'one'],
[2, 'two'],
[3, 'three'],
]);
let arr = [...map.keys()]; // [1, 2, 3]
const go = function*(){
yield 1;
yield 2;
yield 3;
};
[...go()] // [1, 2, 3]
```
### 9.2 数组的空位
数组的空位指,数组的某一个位置没有任何值。`Array`构造函数返回的数组都是空位
```js
Array(3) //[,,,]
```
空位不是`undefined`,一个位置的值等于`undefined`,依然是有值的。空位是没有任何值,`in`运算符可以说明这一点。
```js
0 in [undefined, undefined, undefined] // true
0 in [, , ,] // false
```
ES5 对空位的处理,已经很不一致了,大多数情况下会忽略空位。
- `forEach()`, `filter()`, `reduce()`, `every()` 和`some()`都会跳过空位。
- `map()`会跳过空位,但会保留这个值
- `join()`和`toString()`会将空位视为`undefined`,而`undefined`和`null`会被处理成空字符串。
```js
// forEach方法
[,'a'].forEach((x,i) => console.log(i)); // 1
// filter方法
['a',,'b'].filter(x => true) // ['a','b']
// every方法
[,'a'].every(x => x==='a') // true
// reduce方法
[1,,2].reduce((x,y) => x+y) // 3
// some方法
[,'a'].some(x => x !== 'a') // false
// map方法
[,'a'].map(x => 1) // [,1]
// join方法
[,'a',undefined,null].join('#') // "#a##"
// toString方法
[,'a',undefined,null].toString() // ",a,,"
```
ES6 则是明确将空位转为`undefined`。
`Array.from`方法会将数组的空位,转为`undefined`,也就是说,这个方法不会忽略空位。
```js
Array.from(['a',,'b'])
// [ "a", undefined, "b" ]
//扩展运算符(...)也会将空位转为undefined
[...['a',,'b']]
// [ "a", undefined, "b" ]
//copyWithin()会连空位一起拷贝
[,'a','b',,].copyWithin(2,0)
//fill()会将空位视为正常的数组位置
new Array(3).fill('a') // ["a","a","a"]
//for...of循环也会遍历空位
let arr = [, ,];
for (let i of arr) {
console.log(1);
}
//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
```
### 9.3 Array.prototype.sort() 的排序稳定性
排序稳定性(stable sorting)是排序算法的重要属性,指的是排序关键字相同的项目,排序前后的顺序不变
```js
const arr = [
'peach',
'straw',
'apple',
'spork'
];
const stableSorting=(s1,s2)=>{
if(s1[0]{
if(s1[0]<=s2[0]) return -1;
return 1;
}
arr.sort(unstableSorting);
//['apple', 'peach', 'spork', 'straw']
```
上面代码中,排序结果是`spork`在`straw`前面,跟原始顺序相反,所以排序算法`unstableSorting`是不稳定的。
### 9.4 Array.from()
`Array.from`方法用于将两类对象转为真正的数组:类似数组的对象(array-like object)和可遍历(iterable)的对象(包括 ES6 新增的数据结构 Set 和 Map)
类似数组的对象,`Array.from`将它转化为真正的数组
```js
let arrayLike = {
'0': 'a',
'1': 'b',
'2': 'c',
length: 3
};
let arr1=[].slice.call(arrayLike); //['a', 'b', 'c']
let arr2=Array.from(arrayLike); //['a', 'b', 'c']
```
类似数组的对象是 DOM 操作返回的 NodeList 集合,以及函数内部的`arguments`对象。`Array.from`都可以将它们转为真正的数组
```js
// NodeList对象
let ps = document.querySelectorAll('p');
Array.from(ps).filter(p => {
return p.textContent.length > 100;
});
// arguments对象
function foo() {
var args = Array.from(arguments);
// ...
}
```
继承了`Iterator`接口的数据结构,`Array.from`都可以将其转化为数组
```js
Array.from('hello')
// ['h', 'e', 'l', 'l', 'o']
let namesSet = new Set(['a', 'b'])
Array.from(namesSet) // ['a', 'b']
```
字符串和 Set 结构都具有 Iterator 接口,因此可以被`Array.from`转为真正的数组。
扩展运算符背后调用的是遍历器接口(`Symbol.iterator`),如果一个对象没有部署这个接口,就无法转化。
对于还没有部署该方法的浏览器,可以用`Array.prototype.slice`方法替代
```js
const toArray = (() =>
Array.from ? Array.from : obj => [].slice.call(obj)
)();
```
`Array.from`还可以接受第二个参数,作用类似于数组的`map`方法,用来对每个元素进行处理,将处理后的值放入返回的数组
```js
Array.from(arrayLike, x => x * x);
// 等同于
Array.from(arrayLike).map(x => x * x);
Array.from([1, 2, 3], (x) => x * x)
// [1, 4, 9]
```
返回各种数据的类型
```js
function typesOf(...args){
return Array.from(args,value=>typeof value)
}
function typesOf () {
return Array.from(arguments, value => typeof value)
}
typesOf(null,[],NaN)
// ['object', 'object', 'number']
```
### 9.5 Array.of()
`Array.of`方法用于将一组值,转换为数组
```js
Array.of(3, 11, 8) // [3,11,8]
Array.of(3) // [3]
Array.of(3).length // 1
```
这个方法的主要目的,是弥补数组构造函数`Array()`的不足。因为参数个数的不同,会导致`Array()`的行为有差异
```js
Array() // []
Array(3) // [, , ,]
Array(3, 11, 8) // [3, 11, 8]
```
`Array`方法没有参数、一个参数、三个参数时,返回结果都不一样。只有当参数个数不少于 2 个时,`Array()`才会返回由参数组成的新数组。参数个数只有一个时,实际上是指定数组的长度。
`Array.of`基本上可以用来替代`Array()`或`new Array()`,并且不存在由于参数不同而导致的重载。它的行为非常统一。
```js
Array.of() // []
Array.of(undefined) // [undefined]
Array.of(1) // [1]
Array.of(1, 2) // [1, 2]
```
`Array.of`总是返回参数值组成的数组。如果没有参数,就返回一个空数组。
`Array.of`方法可以用下面的代码模拟实现。
```js
function ArrayOf(){
return [].slice.call(arguments);
}
```
### 9.6 数组实例的 copyWithin()
数组实例的`copyWithin()`方法,在当前数组内部,将指定位置的成员复制到其他位置(会覆盖原有成员),然后返回当前数组。也就是说,使用这个方法,会修改当前数组。
```js
Array.prototype.copyWithin(target, start = 0, end = this.length)
```
- target(必需):从该位置开始替换数据。如果为负值,表示倒数。
- start(可选):从该位置开始读取数据,默认为 0。如果为负值,表示从末尾开始计算。
- end(可选):到该位置前停止读取数据,默认等于数组长度。如果为负值,表示从末尾开始计算。
```js
[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]
```
### 9.7 数组实例的find()和findIndex()
数组实例的`find`方法,用于找出第一个符合条件的数组成员。它的参数是一个回调函数,所有数组成员依次执行该回调函数,直到找出第一个返回值为`true`的成员,然后返回该成员。如果没有符合条件的成员,则返回`undefined`
```js
[1, 4, -5, 10].find((n) => n < 0)
// -5
[1, 5, 10, 15].find(function(value, index, arr) {
return value > 9;
}) // 10
```
数组实例的`findIndex`方法的用法与`find`方法非常类似,返回第一个符合条件的数组成员的位置,如果所有成员都不符合条件,则返回`-1`。
```js
[1, 5, 10, 15].findIndex(function(value, index, arr) {
return value > 9;
}) // 2
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`方法的不足。
```js
[NaN].indexOf(NaN)
// -1
[NaN].findIndex(y => Object.is(NaN, y))
// 0
```
`indexOf`方法无法识别数组的`NaN`成员,但是`findIndex`方法可以借助`Object.is`方法做到
### 9.8 数组实例的fill()
**用法**
`fill` 方法使用给定值,填充一个数组。
```js
['a','b','c'].fill('石岩')
//['石岩', '石岩', '石岩']
new Array(3).fill('夏心研')
//['夏心研', '夏心研', '夏心研']
```
上面代码表明,`fill`方法用于空数组的初始化非常方便。数组中已有的元素,会被全部抹去。
`fill`方法还可以接受第二个和第三个参数,用于指定填充的起始位置和结束位置。
```js
['a', 'b', 'c'].fill(7, 1, 2)
// ['a', 7, 'c']
```
如果填充的类型为对象,那么被赋值的是同一个内存地址的对象,而不是深拷贝对象。
```js
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]]
```
### 9.9 数组实例的entries()、keys()、values()
ES6 提供三个新的方法——`entries()`,`keys()`和`values()`——用于遍历数组。它们都返回一个遍历器对象,可以用`for...of`循环进行遍历,唯一的区别是`keys()`是对键名的遍历、`values()`是对键值的遍历,`entries()`是对键值对的遍历。
```js
for (let index of ['a', 'b'].keys()) {
console.log(index);
}
// 0
// 1
for (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`方法,进行遍历
```js
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']
```
### 9.10 数组实例的includes()
`Array.prototype.includes`方法返回一个布尔值,表示某个数组是否包含给定的值,与字符串的`includes`方法类似。
```js
[1,2,3].includes(3)
//true
[1,2,3].includes(4)
//false
[1,2,NaN].includes(NaN)
//true
```
该方法的第二个参数表示搜索的起始位置,默认为`0`。如果第二个参数为负数,则表示倒数的位置,如果这时它大于数组长度(比如第二个参数为`-4`,但数组长度为`3`),则会重置为从`0`开始
```js
[1, 2, 3].includes(3, 3); // false
[1, 2, 3].includes(3, -1); // true
```
`indexOf`方法有两个缺点,一是不够语义化,它的含义是找到参数值的第一个出现位置,所以要去比较是否不等于`-1`,表达起来不够直观。二是,它内部使用严格相等运算符(`===`)进行判断,这会导致对`NaN`的误判。
```js
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.11 数组实例的flat()、flatMap()
数组的成员有时还是数组,`Array.prototype.flat()`用于将嵌套的数组“拉平”,变成一维的数组。该方法返回一个新数组,对原数据没有影响。
```js
[1,2,[3,4]].flat()
// [1, 2, 3, 4]
```
`flat()`默认只会“拉平”一层,如果想要“拉平”多层的嵌套数组,可以将`flat()`方法的参数写成一个整数,表示想要拉平的层数,默认为1。
```js
[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`关键字作为参数
```js
[1, [2, [3]]].flat(Infinity)
// [1, 2, 3]
```
如果原数组有空位,`flat()`方法会跳过空位
```js
[1, 2, , 4, 5].flat()
// [1, 2, 4, 5]
```
`flatMap()`方法对原数组的每个成员执行一个函数(相当于执行`Array.prototype.map()`),然后对返回值组成的数组执行`flat()`方法。该方法返回一个新数组,不改变原数组
```js
// 相当于 [[2, 4], [3, 6], [4, 8]].flat()
[2, 3, 4].flatMap((x) => [x, x * 2])
// [2, 4, 3, 6, 4, 8]
```
`flatMap()`只能展开一层数组
```js
// 相当于 [[[2]], [[4]], [[6]], [[8]]].flat()
[1, 2, 3, 4].flatMap(x => [[x * 2]])
// [[2], [4], [6], [8]]
```
上面代码中,遍历函数返回的是一个双层的数组,但是默认只能展开一层,因此`flatMap()`返回的还是一个嵌套数组。
`flatMap()`方法的参数是一个遍历函数,该函数可以接受三个参数,分别是当前数组成员、当前数组成员的位置(从零开始)、原数组。
```js
arr.flatMap(function callback(currentValue[, index[, array]]) {
// ...
}[, thisArg])
```
flatMap()`方法还可以有第二个参数,用来绑定遍历函数里面的`this
## 10. 对象的扩展
### 10.1 属性的简洁表示法
ES6 允许在大括号里面,直接写入变量和函数,作为对象的属性和方法。
```javascript
const foo = 'bar';
const baz = {foo};
baz // {foo: "bar"}
// 等同于
const baz = {foo: foo};
function f(x, y) {
return {x, y};
}
// 等同于
function f(x, y) {
return {x: x, y: y};
}
f(1, 2) // Object {x: 1, y: 2}
```
**方法简写**
```js
const o = {
method() {
return "Hello!";
}
};
// 等同于
const o = {
method: function() {
return "Hello!";
}
};
let birth = '2000/01/01';
const Person = {
name: '张三',
//等同于birth: birth
birth,
// 等同于hello: function ()...
hello() { console.log('我的名字是', this.name); }
};
function getPoint() {
const x = 1;
const y = 10;
return {x, y};
}
getPoint()
// {x:1, y:10}
```
CommonJS 模块输出一组变量
```js
let ms = {};
function getItem (key) {
return key in ms ? ms[key] : null;
}
function setItem (key, value) {
ms[key] = value;
}
function clear () {
ms = {};
}
module.exports = { getItem, setItem, clear };
// 等同于
module.exports = {
getItem: getItem,
setItem: setItem,
clear: clear
};
```
属性的赋值器(setter)和取值器(getter)
```js
const cart = {
_wheels: 4,
get wheels () {
return this._wheels;
},
set wheels (value) {
if (value < this._wheels) {
throw new Error('数值太小了!');
}
this._wheels = value;
}
}
let user = {
name: 'test'
};
let foo = {
bar: 'baz'
};
console.log(user, foo)
// {name: "test"} {bar: "baz"}
console.log({user, foo})
// {user: {name: "test"}, foo: {bar: "baz"}}
```
> 注意,简写的对象方法不能用作构造函数,会报错
>
> ```js
> const obj = {
> f() {
> this.foo = 'bar';
> }
> };
> new obj.f() // 报错
> ```
### 10.2 属性表达式
`JavaScript`定义对象的属性,有两种方法
```js
//方法一
obj.foo=true;
//方法二
obj['a'+'bc']=123;
```
上面代码的方法一是直接用标识符作为属性名,方法二是用表达式作为属性名,这时要将表达式放在方括号之内
如果使用字面量方式定义对象(使用大括号),在 ES5 中只能使用方法一(标识符)定义属性
```js
let obj={
foo:true,
abc:123
}
//ES6 允许字面量定义对象时,用方法二(表达式)作为对象的属性名,即把表达式放在方括号内
let propKey = 'foo';
let obj = {
[propKey]: true,
['a' + 'bc']: 123
};
let lastWord = 'last word';
const a = {
'first word': 'hello',
[lastWord]: 'world'
};
a['first word'] // "hello"
a[lastWord] // "world"
a['last word'] // "world"
```
表达式还可以用于定义方法名
```js
let obj = {
['h' + 'ello']() {
return 'hi';
}
};
obj.hello() // hi
```
> 注意,属性名表达式与简洁表示法,不能同时使用,会报错
>
> ```js
> // 报错
> const foo = 'bar';
> const bar = 'abc';
> const baz = { [foo] };
> // 正确
> const foo = 'bar';
> const baz = { [foo]: 'abc'};
> ```
>
> 注意,属性名表达式如果是一个对象,默认情况下会自动将对象转为字符串`[object Object]`
>
> ```js
> const keyA = {a: 1};
> const keyB = {b: 2};
> const myObject = {
> [keyA]: 'valueA',
> [keyB]: 'valueB'
> };
> myObject // Object {[object Object]: "valueB"}
> ```
### 10.3 方法的name属性
函数的`name`属性,返回函数名。对象方法也是函数,因此也有`name`属性
```js
const person = {
sayName() {
console.log('hello!');
},
};
person.sayName.name // "sayName"
//方法的name属性返回函数名(即方法名)
```
如果对象的方法使用了取值函数(`getter`)和存值函数(`setter`),则`name`属性不是在该方法上面,而是该方法的属性的描述对象的`get`和`set`属性上面,返回值是方法名前加上`get`和`set`
```js
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`。
```js
(new Function()).name // "anonymous"
var doSomething = function() {
// ...
};
doSomething.bind().name // "bound doSomething"
```
如果对象的方法是一个 Symbol 值,那么`name`属性返回的是这个 Symbol 值的描述
```js
const key1 = Symbol('description');
const key2 = Symbol();
let obj = {
[key1]() {},
[key2]() {},
};
obj[key1].name // "[description]"
obj[key2].name // ""
```
### 10.4 属性的可枚举性和遍历
#### 10.4.1 可枚举性
对象的每个属性都有一个描述对象(Descriptor),用来控制该属性的行为。`Object.getOwnPropertyDescriptor`方法可以获取该属性的描述对象。
```js
let obj = { foo: 123 };
Object.getOwnPropertyDescriptor(obj, 'foo')
/*
{
value: 123,
writable: true,
enumerable: true,
configurable: true
}
*/
```
描述对象的`enumerable`属性,称为“可枚举性”,如果该属性为`false`,就表示某些操作会忽略当前属性
目前,有四个操作会忽略`enumerable`为`false`的属性
- `for...in`循环:只遍历对象自身的和继承的可枚举的属性
- `Object.keys()`:返回对象自身的可枚举的属性的键名
- `JSON.stringify()`:只串行化对象自身的可枚举的属性
- `Object.assign()`:忽略`enumerable`为`false`的属性,只拷贝对象自身的可枚举的属性
引入“可枚举”(`enumerable`)这个概念的最初目的,就是让某些属性可以规避掉`for...in`操作,不然所有内部属性和方法都会被遍历到。比如,对象原型的`toString`方法,以及数组的`length`属性,就通过“可枚举性”,从而避免被`for...in`遍历到。
```js
Object.getOwnPropertyDescriptor(Object.prototype, 'toString').enumerable
// false
Object.getOwnPropertyDescriptor([], 'length').enumerable
// false
```
上面代码中,`toString`和`length`属性的`enumerable`都是`false`,因此`for...in`不会遍历到这两个继承自原型的属性
> ES6 规定,所有 Class 的原型的方法都是不可枚举的
>
> ```js
> Object.getOwnPropertyDescriptor(class {foo() {}}.prototype, 'foo').enumerable
> // false
> ```
#### 10.4.2 遍历
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 键,按照加入时间升序排列。
```js
Reflect.ownKeys({ [Symbol()]:0, b:0, 10:0, 2:0, a:0 })
// ['2', '10', 'b', 'a', Symbol()]
```
### 10.5 super 关键字
`this`关键字总是指向函数所在的当前对象,ES6 又新增了另一个类似的关键字`super`,指向当前对象的原型对象。
```js
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`关键字表示原型对象时,只能用在对象的方法之中,用在其他地方都会报错
```js
// 报错
const obj = {
foo: super.foo
}
// 报错
const obj = {
foo: () => super.foo
}
// 报错
const obj = {
foo: function () {
return super.foo
}
}
```
JavaScript 引擎内部,`super.foo`等同于`Object.getPrototypeOf(this).foo`(属性)或`Object.getPrototypeOf(this).foo.call(this)`(方法)
```js
const proto = {
x: 'hello',
foo() {
console.log(this.x);
},
};
const obj = {
x: 'world',
foo() {
super.foo();
}
}
Object.setPrototypeOf(obj, proto);
obj.foo() // "world"
```
### 10.6 对象的扩展运算符
#### 10.6.1 解构赋值
对象的解构赋值用于从一个对象取值,相当于将目标对象自身的可遍历的(enumerable、但尚未被读取的属性,分配到指定的对象上面。所有的键和它们的值,都会被拷贝到新对象上面。
```js
let { x, y, ...z } = { x: 1, y: 2, a: 3, b: 4 };
x // 1
y // 2
z // { a: 3, b: 4 }
```
由于解构赋值要求等号右边是一个对象,所以如果等号右边是`undefined`或`null`,就会报错,因为它们无法转为对象。
```js
let { ...z } = null; // 运行时错误
let { ...z } = undefined; // 运行时错误
```
解构赋值必须是最后一个参数,否则会报错
```js
let { ...x, y, z } = someObject; // 句法错误
let { x, ...y, ...z } = someObject; // 句法错误
//上面代码中,解构赋值不是最后一个参数,所以会报错
```
> 注意,解构赋值的拷贝是浅拷贝,即如果一个键的值是复合类型的值(数组、对象、函数)、那么解构赋值拷贝的是这个值的引用,而不是这个值的副本
>
> ```js
> let obj = { a: { b: 1 } };
> let { ...x } = obj;
> obj.a.b = 2;
> x.a.b // 2
> ```
>
> 扩展运算符的解构赋值,不能复制继承自原型对象的属性
>
> ```js
> let o1 = { a: 1 };
> let o2 = { b: 2 };
> o2.__proto__ = o1;
> let { ...o3 } = o2;
> o3 // { b: 2 }
> o3.a // undefined
>
> 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
> ```
**解构赋值的一个用处,是扩展某个函数的参数,引入其他操作**
```js
function baseFunction({ a, b }) {
// ...
}
function wrapperFunction({ x, y, ...restConfig }) {
// 使用 x 和 y 参数进行操作
// 其余参数传给原始函数
return baseFunction(restConfig);
}
//原始函数baseFunction接受a和b作为参数,函数wrapperFunction在baseFunction的基础上进行了扩展,能够接受多余的参数,并且保留原始函数的行为
```
#### 10.6.2 扩展运算符
对象的扩展运算符(`...`)用于取出参数对象的所有可遍历属性,拷贝到当前对象之中
```js
let z = { a: 3, b: 4 };
let n = { ...z };
n // { a: 3, b: 4 }
```
由于数组是特殊的对象,所以对象的扩展运算符也可以用于数组
```js
let foo = { ...['a', 'b', 'c'] };
foo
// {0: "a", 1: "b", 2: "c"}
```
```js
////如果扩展运算符后面是一个空对象,则没有任何效果
{...{}, a: 1}
// { a: 1 }
//如果扩展运算符后面不是对象,则会自动将其转为对象
// 等同于 {...Object(1)}
{...1} // {}
// 等同于 {...Object(true)}
{...true} // {}
// 等同于 {...Object(undefined)}
{...undefined} // {}
// 等同于 {...Object(null)}
{...null} // {}
//如果扩展运算符后面是字符串,它会自动转成一个类似数组的对象,因此返回的不是空对象
{...'hello'}
// {0: "h", 1: "e", 2: "l", 3: "l", 4: "o"}
```
对象的扩展运算符等同于使用`Object.assign()`方法
```js
let aClone = { ...a };
// 等同于
let aClone = Object.assign({}, a);
```
完整克隆一个对象,还拷贝对象原型的属性的方法
```js
// 写法一
const clone1 = {
__proto__: Object.getPrototypeOf(obj),
...obj
};
// 写法二
const clone2 = Object.assign(
Object.create(Object.getPrototypeOf(obj)),
obj
);
// 写法三
const clone3 = Object.create(
Object.getPrototypeOf(obj),
Object.getOwnPropertyDescriptors(obj)
)
```
扩展运算符可以用于合并两个对象
```js
let ab = { ...a, ...b };
// 等同于
let ab = Object.assign({}, a, b);
```
如果用户自定义的属性,放在扩展运算符后面,则扩展运算符内部的同名属性会被覆盖掉
```js
let aWithOverrides = { ...a, x: 1, y: 2 };
// 等同于
let aWithOverrides = { ...a, ...{ x: 1, y: 2 } };
// 等同于
let x = 1, y = 2, aWithOverrides = { ...a, x, y };
// 等同于
let aWithOverrides = Object.assign({}, a, { x: 1, y: 2 });
let newVersion = {
...previousVersion,
name: 'New Name' // Override the name property
};
//如果把自定义属性放在扩展运算符前面,就变成了设置新对象的默认属性值
let aWithDefaults = { x: 1, y: 2, ...a };
// 等同于
let aWithDefaults = Object.assign({}, { x: 1, y: 2 }, a);
// 等同于
let aWithDefaults = Object.assign({ x: 1, y: 2 }, a);
//与数组的扩展运算符一样,对象的扩展运算符后面可以跟表达式
const obj = {
...(x > 1 ? {a: 1} : {}),
b: 2,
};
//扩展运算符的参数对象之中,如果有取值函数get,这个函数是会执行的
let a = {
get x() {
throw new Error('not throw yet');
}
}
let aWithXGetter = { ...a }; // 报错
```
### 10.7 链判断运算符
编程实践中,如果读取对象内部的某个属性,往往需要判断下改对象是否存在。比如要读取`message.body.user.firstName`
```js
const firstName = (message
&& message.body
&& message.body.user
&& message.body.user.firstName) || 'default';
```
或者使用三元运算符`?:`,判断一个对象是否存在
```js
const fooInput = myForm.querySelector('input[name=foo]')
const fooValue = fooInput ? fooInput.value : undefined
```
[ES2020](https://github.com/tc39/proposal-optional-chaining) 引入了“链判断运算符”(optional chaining operator)`?.`,简化上面的写法
```js
const firstName = message?.body?.user?.firstName || 'default';
const fooValue = myForm.querySelector('input[name=foo]')?.value
```
上面代码使用了`?.`运算符,直接在链式调用的时候判断,左侧的对象是否为`null`或`undefined`。如果是的,就不再往下运算,而是返回`undefined`。
链判断运算符三种用法
- obj?.prop //对象属性
- obj?.[expr] //同上
- func?.(...agrs) //函数或对象方法的调用
下面是判断对象方法是否存在,如果存在就立即执行
```js
iterator.return?.()
```
对于那些可能没有实现的方法,这个运算符尤其有用
```js
if (myForm.checkValidity?.() === false) {
// 表单校验失败
return;
}
```
链判断运算符常见的使用形式
```js
a?.b
// 等同于
a == null ? undefined : a.b
a?.[x]
// 等同于
a == null ? undefined : a[x]
a?.b()
// 等同于
a == null ? undefined : a.b()
a?.()
// 等同于
a == null ? undefined : a()
```
特别注意后两种形式,如果`a?.b()`里面的`a.b`不是函数,不可调用,那么`a?.b()`是会报错的。`a?.()`也是如此,如果`a`不是`null`或`undefined`,但也不是函数,那么`a?.()`会报错。
> (1) 短路机制
>
> ```js
> a?.[++x]
> // 等同于
> a == null ? undefined : a[++x]
> ```
>
> (2)delete运算符
>
> ```js
> delete a?.b
> // 等同于
> a == null ? undefined : delete a.b
> ```
>
> (3)括号的影响
>
> 如果属性链有圆括号,链判断运算符对圆括号外部没有影响,只对圆括号内部有影响
>
> ```js
> (a?.b).c
> // 等价于
> (a == null ? undefined : a.b).c
> ```
>
> (4)报错场合
>
> 以下写法是禁止的,会报错
>
> ```js
> // 构造函数
> new a?.()
> new a?.b()
> // 链判断运算符的右侧有模板字符串
> a?.`{b}`
> a?.b`{c}`
> // 链判断运算符的左侧是 super
> super?.()
> super?.foo
> // 链运算符用于赋值运算符左侧
> a?.b = c
> ```
>
> (5) 右侧不得为十进制数值
>
> 为了保证兼容以前的代码,允许`foo?.3:0`被解析成`foo ? .3 : 0`,因此规定如果`?.`后面紧跟一个十进制数字,那么`?.`不再被看成是一个完整的运算符,而会按照三元运算符进行处理,也就是说,那个小数点会归属于后面的十进制数字,形成一个小数。
### 10.8 Null判断运算符
读取对象属性的时候,如果某个属性的值是`null`或`undefined`,有时候需要为它们指定默认值。常见做法是通过`||`运算符指定默认值。
```js
const headerText = response.settings.headerText || 'Hello, world!';
const animationDuration = response.settings.animationDuration || 300;
const showSplashScreen = response.settings.showSplashScreen || true;
```
上面的三行代码都通过`||`运算符指定默认值,但是这样写是错的。开发者的原意是,只要属性的值为`null`或`undefined`,默认值就会生效,但是属性的值如果为空字符串或`false`或`0`,默认值也会生效。
为了避免这种情况,[ES2020](https://github.com/tc39/proposal-nullish-coalescing) 引入了一个新的 Null 判断运算符`??`。它的行为类似`||`,但是只有运算符左侧的值为`null`或`undefined`时,才会返回右侧的值。
```js
const headerText = response.settings.headerText ?? 'Hello, world!';
const animationDuration = response.settings.animationDuration ?? 300;
const showSplashScreen = response.settings.showSplashScreen ?? true;
```
这个运算符的一个目的,就是跟链判断运算符`?.`配合使用,为`null`或`undefined`的值设置默认值。
```js
const animationDuration = response.settings?.animationDuration ?? 300;
```
上面代码中,`response.settings`如果是`null`或`undefined`,就会返回默认值300。
这个运算符很适合判断函数参数是否赋值。
```js
function Component(props) {
const enable = props.enabled ?? true;
// …
}
```
上面代码判断`props`参数的`enabled`属性是否赋值,等同于下面的写法
```js
function Component(props) {
const {
enabled: enable = true,
} = props;
// …
}
```
`??`有一个运算优先级问题,它与`&&`和`||`的优先级孰高孰低。现在的规则是,如果多个逻辑运算符一起使用,必须用括号表明优先级,否则会报错。
```js
// 报错
lhs && middle ?? rhs
lhs ?? middle && rhs
lhs || middle ?? rhs
lhs ?? middle || rhs
```
上面四个表达式都会报错,必须加入表明优先级的括号.
```js
(lhs && middle) ?? rhs;
lhs && (middle ?? rhs);
(lhs ?? middle) && rhs;
lhs ?? (middle && rhs);
(lhs || middle) ?? rhs;
lhs || (middle ?? rhs);
(lhs ?? middle) || rhs;
lhs ?? (middle || rhs);
```
## 11. 对象的新增方法
### 11.1 Object.is()
ES5 比较两个值是否相等,只有两个运算符:相等运算符(`==`)和严格相等运算符(`===`)。它们都有缺点,前者会自动转换数据类型,后者的`NaN`不等于自身,以及`+0`等于`-0`。JavaScript 缺乏一种运算,在所有环境中,只要两个值是一样的,它们就应该相等。
ES6 提出“Same-value equality”(同值相等)算法,用来解决这个问题。`Object.is`就是部署这个算法的新方法。它用来比较两个值是否严格相等,与严格比较运算符(===)的行为基本一致。
```js
Object.is('foo', 'foo')
// true
Object.is({}, {})
// false
```
不同之处只有两个:一是`+0`不等于`-0`,二是`NaN`等于自身。
```js
+0 === -0 //true
NaN === NaN // false
Object.is(+0, -0) // false
Object.is(NaN, NaN) // true
```
ES5 可以通过下面的代码,部署`Object.is`
```js
Object.defineProperty(Object, 'is', {
value: function(x, y) {
if (x === y) {
// 针对+0 不等于 -0的情况
return x !== 0 || 1 / x === 1 / y;
}
// 针对NaN的情况
return x !== x && y !== y;
},
configurable: true,
enumerable: false,
writable: true
});
```
### 11.2 Object.assign()
`Object.assign`方法用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target)。
```js
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`方法的第一个参数是目标对象,后面的参数都是源对象。
> 注意,如果目标对象与源对象有同名属性,或多个源对象有同名属性,则后面的属性会覆盖前面的属性
```js
const target = { a: 1, b: 1 };
const source1 = { b: 2, c: 2 };
const source2 = { c: 3 };
Object.assign(target, source1, source2);
target // {a:1, b:2, c:3}
//如果只有一个参数,Object.assign会直接返回该参数
const obj = {a: 1};
Object.assign(obj) === obj // true
//如果该函数不是对象,则会先转化成对象,然后返回
typeof Object.assign(2)//'object'
//由于undefined和null无法转成对象,所以如果它们作为参数,就会报错。
Object.assign(undefined) // 报错
Object.assign(null) // 报错
//由于undefined和null无法转成对象,所以如果它们作为参数,就会报错
Object.assign(undefined) // 报错
Object.assign(null) // 报错
//如果非对象参数出现在源对象的位置(即非首参数),那么处理规则有所不同。首先,这些参数都会转成对象,如果无法转成对象,就会跳过。这意味着,如果undefined和null不在首参数,就不会报错
let obj = {a: 1};
Object.assign(obj, undefined) === obj // true
Object.assign(obj, null) === obj // true
//其他类型的值(即数值、字符串和布尔值)不在首参数,也不会报错。但是,除了字符串会以数组形式,拷贝入目标对象,其他值都不会产生效果
const v1 = 'abc';
const v2 = true;
const v3 = 10;
const obj = Object.assign({}, v1, v2, v3);
console.log(obj); // { "0": "a", "1": "b", "2": "c" }
```
`Object.assign`拷贝的属性是有限制的,只拷贝源对象的自身属性(不拷贝继承属性),也不拷贝不可枚举的属性(`enumerable: false`)。
```js
Object.assign({b: 'c'},
Object.defineProperty({}, 'invisible', {
enumerable: false,
value: 'hello'
})
)
// { b: 'c' }
//属性名为 Symbol 值的属性,也会被Object.assign拷贝
Object.assign({ a: 'b' }, { [Symbol('c')]: 'd' })
// { a: 'b', Symbol(c): 'd' }
```
**注意点**
(1) 浅拷贝
`Object.assign`方法实行的是浅拷贝,而不是深拷贝。也就是说,如果源对象某个属性的值是对象,那么目标对象拷贝得到的是这个对象的引用。
```js
const obj1 = {a: {b: 1}};
const obj2 = Object.assign({}, obj1);
obj1.a.b = 2;
obj2.a.b // 2
```
(2)同名属性的替换
对于这种嵌套的对象,一旦遇到同名属性,`Object.assign`的处理方法是替换,而不是添加
```js
const target = { a: { b: 'c', d: 'e' } }
const source = { a: { b: 'hello' } }
Object.assign(target, source)
// { a: { b: 'hello' } }
```
(3)数组的处理
`Object.assign`可以用来处理数组,但是会把数组是为对象。
```js
Object.assign([1, 2, 3], [4, 5])
// [4, 5, 3]
```
(4)取值函数的处理
`Object.assign`只能进行值的复制,如果要复制的值是一个取值函数,那么将求值后再复制。
```js
const source = {
get foo() { return 1 }
};
const target = {};
Object.assign(target, source)
// { foo: 1 }
```
**常见用途**
(1) 为对象添加属性
```js
class Point{
constructor(x,y){
Object.assign(this,{x,y});
}
}
```
(2) 为对象添加方法
```js
Object.assign(SomeClass.prototype, {
someMethod(arg1, arg2) {
···
},
anotherMethod() {
···
}
});
// 等同于下面的写法
SomeClass.prototype.someMethod = function (arg1, arg2) {
···
};
SomeClass.prototype.anotherMethod = function () {
···
};
```
(3) 克隆对象
```js
function clone(origin){
return Object.assign({},origin);
}
//如果想要保持继承链,可以采用下面的代码
function clone(origin) {
let originProto = Object.getPrototypeOf(origin);
return Object.assign(Object.create(originProto), origin);
}
```
(4) 合并多个对象
将多个对象合并到某个对象
```js
const merge=(target,...sources)=>Object.assign(target,...sources);
//如果希望合并后返回一个新对象,可以改写上面函数,对一个空对象合并
const merge =
(...sources) => Object.assign({}, ...sources);
```
(5)为属性指定默认值
```js
const DEFAULTS = {
logLevel: 0,
outputFormat: 'html'
};
function processContent(options) {
options = Object.assign({}, DEFAULTS, options);
console.log(options);
// ...
}
```
注意,由于存在浅拷贝的问题,`DEFAULTS`对象和`options`对象的所有属性的值,最好都是简单类型,不要指向另一个对象。
```js
const DEFAULTS = {
url: {
host: 'example.com',
port: 7070
},
};
processContent({ url: {port: 8000} })
// {
// url: {port: 8000}
// }
```
### 11.3 Object.getOwnPropertyDescriptors()
ES5 的`Object.getOwnPropertyDescriptor()`方法会返回某个对象属性的描述对象(descriptor)。ES2017 引入了`Object.getOwnPropertyDescriptors()`方法,返回指定对象所有自身属性(非继承属性)的描述对象。
```js
const obj = {
foo: 123,
get bar() { return 'abc' }
};
Object.getOwnPropertyDescriptors(obj)
// { foo:
// { value: 123,
// writable: true,
// enumerable: true,
// configurable: true },
// bar:
// { get: [Function: get bar],
// set: undefined,
// enumerable: true,
// configurable: true } }
//该方法实现
function getOwnPropertyDescriptors(obj) {
const result = {};
for (let key of Reflect.ownKeys(obj)) {
result[key] = Object.getOwnPropertyDescriptor(obj, key);
}
return result;
}
```
> 该方法的引入目标,主要是解决`Object.assign()`无法正确拷贝`get`属性和`set`属性的问题
```js
const source = {
set foo(value) {
console.log(value);
}
};
const target1 = {};
Object.assign(target1, source);
Object.getOwnPropertyDescriptor(target1, 'foo')
// { value: undefined,
// writable: true,
// enumerable: true,
// configurable: true }
//正确拷贝方式
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 }
const shallowMerge = (target, source) => Object.defineProperties(
target,
Object.getOwnPropertyDescriptors(source)
);
```
`Object.getOwnPropertyDescriptors()`方法的另一个用处,是配合`Object.create()`方法,将对象属性克隆到一个新对象。这属于浅拷贝。
```js
const clone = Object.create(Object.getPrototypeOf(obj),
Object.getOwnPropertyDescriptors(obj));
// 或者
const shallowClone = (obj) => Object.create(
Object.getPrototypeOf(obj),
Object.getOwnPropertyDescriptors(obj)
);
```
`Object.getOwnPropertyDescriptors()`方法可以实现一个对象继承另一个对象。以前,继承另一个对象,常常写成下面这样。
```js
const obj={
__proto__:prot,
foo:123
};
//ES6 规定__proto__只有浏览器要部署,其他环境不用部署。如果去除__proto__,上面代码就要改成下面这样
const obj = Object.create(prot);
obj.foo = 123;
// 或者
const obj = Object.assign(
Object.create(prot),
{
foo: 123,
}
);
```
`Object.getOwnPropertyDescriptors()`也可以用来实现 Mixin(混入)模式。
```js
let mix = (object) => ({
with: (...mixins) => mixins.reduce(
(c, mixin) => Object.create(
c, Object.getOwnPropertyDescriptors(mixin)
), object)
});
// multiple mixins example
let a = {a: 'a'};
let b = {b: 'b'};
let c = {c: 'c'};
let d = mix(c).with(a, b);
d.c // "c"
d.b // "b"
d.a // "a"
```
上面代码返回一个新的对象`d`,代表了对象`a`和`b`被混入了对象`c`的操作。
### 10.4 `__proto__`属性,Object.setPrototypeOf(),Object.getPrototypeOf()
`__proto__`属性
`__proto__`属性(前后各两个下划线),用来读取或设置当前对象的原型对象(prototype)。目前,所有浏览器(包括 IE11)都部署了这个属性。
```js
// es5 的写法
const obj = {
method: function() { ... }
};
obj.__proto__ = someOtherObj;
// es6 的写法
var obj = Object.create(someOtherObj);
obj.method = function() { ... };
```
使用`Object.setPrototypeOf()`(写操作)、`Object.getPrototypeOf()`(读操作)、`Object.create()`(生成操作)
```js
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),返回参数对象本身。它是 ES6 正式推荐的设置原型对象的方法。
```js
// 格式
Object.setPrototypeOf(object, prototype)
// 用法
const o = Object.setPrototypeOf({}, null);
//等同于下面的函数
function setPrototypeOf(obj, proto) {
obj.__proto__ = proto;
return obj;
}
//实例
let proto = {};
let obj = { x: 10 };
Object.setPrototypeOf(obj, proto);
proto.y = 20;
proto.z = 40;
obj.x // 10
obj.y // 20
obj.z // 40
//如果第一个参数不是对象,会自动转为对象。但是由于返回的还是第一个参数,所以这个操作不会产生任何效果
Object.setPrototypeOf(1, {}) === 1 // true
Object.setPrototypeOf('foo', {}) === 'foo' // true
Object.setPrototypeOf(true, {}) === true // true
//由于undefined和null无法转为对象,所以如果第一个参数是undefined或null,就会报错
```
**Object.getPrototypeOf()**
该方法与`Object.setPrototypeOf`方法配套,用于读取一个对象的原型对象。
```js
Object.getPrototypeOf(obj);
function Rectangle() {
// ...
}
const rec = new Rectangle();
Object.getPrototypeOf(rec) === Rectangle.prototype
// true
Object.setPrototypeOf(rec, Object.prototype);
Object.getPrototypeOf(rec) === Rectangle.prototype
// false
//如果参数不是对象,会被自动转为对象
// 等同于 Object.getPrototypeOf(Number(1))
Object.getPrototypeOf(1)
// Number {[[PrimitiveValue]]: 0}
// 等同于 Object.getPrototypeOf(String('foo'))
Object.getPrototypeOf('foo')
// String {length: 0, [[PrimitiveValue]]: ""}
// 等同于 Object.getPrototypeOf(Boolean(true))
Object.getPrototypeOf(true)
// Boolean {[[PrimitiveValue]]: false}
Object.getPrototypeOf(1) === Number.prototype // true
Object.getPrototypeOf('foo') === String.prototype // true
Object.getPrototypeOf(true) === Boolean.prototype // true
//如果参数是undefined或null,它们无法转为对象,所以会报错
Object.getPrototypeOf(null)
// TypeError: Cannot convert undefined or null to object
Object.getPrototypeOf(undefined)
// TypeError: Cannot convert undefined or null to object
```
### 10.5 Object.keys(),Object.values(),Object.entries()
**Object.keys()**
ES5 引入了`Object.keys`方法,返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键名。
```js
var obj = { foo: 'bar', baz: 42 };
Object.keys(obj)
// ["foo", "baz"]
```
ES2017 [引入](https://github.com/tc39/proposal-object-values-entries)了跟`Object.keys`配套的`Object.values`和`Object.entries`,作为遍历一个对象的补充手段,供`for...of`循环使用。
```js
let {keys, values, entries} = Object;
let obj = { a: 1, b: 2, c: 3 };
for (let key of keys(obj)) {
console.log(key); // 'a', 'b', 'c'
}
for (let value of values(obj)) {
console.log(value); // 1, 2, 3
}
for (let [key, value] of entries(obj)) {
console.log([key, value]); // ['a', 1], ['b', 2], ['c', 3]
}
```
**Object.values()**
`Object.values`方法返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键值。
```js
const obj = { foo: 'bar', baz: 42 };
Object.values(obj)
// ["bar", 42]
```
`Object.create`方法的第二个参数添加的对象属性(属性`p`),如果不显式声明,默认是不可遍历的,因为`p`的属性描述对象的`enumerable`默认是`false`,`Object.values`不会返回这个属性。只要把`enumerable`改成`true`,`Object.values`就会返回属性`p`的值。
```js
const obj = Object.create({}, {p: {value: 42}});
Object.values(obj) // []
const obj = Object.create({}, {p:
{
value: 42,
enumerable: true
}
});
Object.values(obj) // [42]
//Object.values会过滤属性名为 Symbol 值的属性
Object.values({ [Symbol()]: 123, foo: 'abc' });
// ['abc']
//如果Object.values方法的参数是一个字符串,会返回各个字符组成的一个数组
Object.values('foo')
// ['f', 'o', 'o']
//如果参数不是对象,Object.values会先将其转为对象。由于数值和布尔值的包装对象,都不会为实例添加非继承的属性
Object.values(42) // []
Object.values(true) // []
```
**Object.entries()**
`Object.entries()`方法返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键值对数组。
```js
const obj = { foo: 'bar', baz: 42 };
Object.entries(obj)
// [ ["foo", "bar"], ["baz", 42] ]
//如果原对象的属性名是一个 Symbol 值,该属性会被忽略
Object.entries({ [Symbol()]: 123, foo: 'abc' });
// [ [ 'foo', 'abc' ] ]
//Object.entries的基本用途是遍历对象的属性
let obj = { one: 1, two: 2 };
for (let [k, v] of Object.entries(obj)) {
console.log(
`${JSON.stringify(k)}: ${JSON.stringify(v)}`
);
}
// "one": 1
// "two": 2
//Object.entries方法将对象转为真正的Map结构
const obj = { foo: 'bar', baz: 42 };
const map = new Map(Object.entries(obj));
map // Map { foo: "bar", baz: 42 }
// 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;
}
```
### 10.6 Object.fromEntries()
`Object.fromEntries()`方法是`Object.entries()`的逆操作,用于将一个键值对数组转为对象。
```JS
Object.fromEntries([
['foo', 'bar'],
['baz', 42]
])
// { foo: "bar", baz: 42 }
```
该方法的主要目的,是将键值对的数据结构还原为对象,因此特别适合将 Map 结构转为对象。
```js
// 例一
const entries = new Map([
['foo', 'bar'],
['baz', 42]
]);
Object.fromEntries(entries)
// { foo: "bar", baz: 42 }
// 例二
const map = new Map().set('foo', true).set('bar', false);
Object.fromEntries(map)
// { foo: true, bar: false }
```
该方法的一个用处是配合`URLSearchParams`对象,将查询字符串转为对象。
```js
Object.fromEntries(new URLSearchParams('foo=bar&baz=qux'))
// { foo: "bar", baz: "qux" }
```
## 12. Symbol
ES6引入了一个新的原始数据类型`Symbol`,表示独一无二的值。
Symbol 值通过`Symbol`函数生成。凡是属性名属于 Symbol 类型,就都是独一无二的,可以保证不会与其他属性名产生冲突。
```js
let s = Symbol();
typeof s
// "symbol"
```
> 注意,`Symbol`函数前不能使用`new`命令,否则会报错。这是因为生成的 Symbol 是一个原始类型的值,不是对象。也就是说,由于 Symbol 值不是对象,所以不能添加属性。基本上,它是一种类似于字符串的数据类型。
`Symbol`函数可以接受一个字符串作为参数,表示对 Symbol 实例的描述,主要是为了在控制台显示,或者转为字符串时,比较容易区分。
```js
let s1 = Symbol('foo');
let s2 = Symbol('bar');
s1 // Symbol(foo)
s2 // Symbol(bar)
s1.toString() // "Symbol(foo)"
s2.toString() // "Symbol(bar)"
```
如果 Symbol 的参数是一个对象,就会调用该对象的`toString`方法,将其转为字符串,然后才生成一个 Symbol 值。
```js
const obj = {
toString() {
return 'abc';
}
};
const sym = Symbol(obj);
sym // Symbol(abc)
```
注意,`Symbol`函数的参数只是表示对当前 Symbol 值的描述,因此相同参数的`Symbol`函数的返回值是不相等的。
```js
// 没有参数的情况
let s1 = Symbol();
let s2 = Symbol();
s1 === s2 // false
// 有参数的情况
let s1 = Symbol('foo');
let s2 = Symbol('foo');
s1 === s2 // false
```
Symbol 值不能与其他类型的值进行运算,会报错。
```js
let sym = Symbol('My symbol');
"your symbol is " + sym
// TypeError: can't convert symbol to string
`your symbol is ${sym}`
// TypeError: can't convert symbol to string
```
Symbol 值可以显式转为字符串。
```js
let sym = Symbol('My symbol');
String(sym) // 'Symbol(My symbol)'
sym.toString() // 'Symbol(My symbol)'
```
Symbol 值也可以转为布尔值,但是不能转为数值。
```js
let sym = Symbol();
Boolean(sym) // true
!sym // false
if (sym) {
// ...
}
Number(sym) // TypeError
sym + 2 // TypeError
```
### 12.1 Symbol.prototype.description
创建Symbol的时候,可以添加一个描述。
```js
const sym=Symbol('foo');
//读取描述可以将symbol现实转化为字符串
String(sym);
sym.toString();
//ES2019提供了一个description,直接返回Symbol的描述
const sym = Symbol('foo');
sym.description // "foo"
```
### 12.2 作为属性名的Symbol
由于每一个 Symbol 值都是不相等的,这意味着 Symbol 值可以作为标识符,用于对象的属性名,就能保证不会出现同名的属性。这对于一个对象由多个模块构成的情况非常有用,能防止某一个键被不小心改写或覆盖。
```js
let mySymbol = Symbol();
// 第一种写法
let a = {};
a[mySymbol] = 'Hello!';
// 第二种写法
let a = {
[mySymbol]: 'Hello!'
};
// 第三种写法
let a = {};
Object.defineProperty(a, mySymbol, { value: 'Hello!' });
// 以上写法都得到同样结果
a[mySymbol] // "Hello!"
```
> 注意,Symbol值作为对象属性名是,不能使用点运算符
```js
const mySymbol = Symbol();
const a = {};
a.mySymbol = 'Hello!';
a[mySymbol] // undefined
a['mySymbol'] // "Hello!"
//同理,在对象的内部,使用 Symbol 值定义属性时,Symbol 值必须放在方括号之中
let s = Symbol();
let obj = {
[s]: function (arg) { ... }
};
obj[s](123);
```
Symbol 类型还可以用于定义一组常量,保证这组常量的值都是不相等的
```js
const log = {};
log.levels = {
DEBUG: Symbol('debug'),
INFO: Symbol('info'),
WARN: Symbol('warn')
};
console.log(log.levels.DEBUG, 'debug message');
console.log(log.levels.INFO, 'info message');
const COLOR_RED = Symbol();
const COLOR_GREEN = Symbol();
function getComplement(color) {
switch (color) {
case COLOR_RED:
return COLOR_GREEN;
case COLOR_GREEN:
return COLOR_RED;
default:
throw new Error('Undefined color');
}
}
```
Symbol 值作为属性名时,该属性还是公开属性,不是私有属性。
### 12.3 实例消除魔术字符串
魔术字符串指的是,在代码之中多次出现、与代码形成强耦合的某一个具体的字符串或者数值。风格良好的代码,应该尽量消除魔术字符串,改由含义清晰的变量代替。
```js
function getArea(shape, options) {
let area = 0;
switch (shape) {
case 'Triangle': // 魔术字符串
area = .5 * options.width * options.height;
break;
/* ... more code ... */
}
return area;
}
getArea('Triangle', { width: 100, height: 100 }); // 魔术字符串
```
字符串`Triangle`就是一个魔术字符串。它多次出现,与代码形成“强耦合”,不利于将来的修改和维护。
常用的消除魔术字符串的方法,就是把它写成一个变量
```js
const shapeType = {
triangle: 'Triangle'
};
function getArea(shape, options) {
let area = 0;
switch (shape) {
case shapeType.triangle:
area = .5 * options.width * options.height;
break;
}
return area;
}
getArea(shapeType.triangle, { width: 100, height: 100 });
```
如果仔细分析,可以发现`shapeType.triangle`等于哪个值并不重要,只要确保不会跟其他`shapeType`属性的值冲突即可。因此,这里就很适合改用 Symbol 值。
```js
const shapeType = {
triangle: Symbol()
};
```
### 12.4 属性名的遍历
Symbol 作为属性名,遍历对象的时候,该属性不会出现在`for...in`、`for...of`循环中,也不会被`Object.keys()`、`Object.getOwnPropertyNames()`、`JSON.stringify()`返回。
但是,它也不是私有属性,有一个`Object.getOwnPropertySymbols()`方法,可以获取指定对象的所有 Symbol 属性名。该方法返回一个数组,成员是当前对象的所有用作属性名的 Symbol 值。
```js
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)]
```
`Object.getOwnPropertySymbols()`方法与`for...in`循环、`Object.getOwnPropertyNames`方法进行对比的例子。
```js
const obj = {};
const foo = Symbol('foo');
obj[foo] = 'bar';
for (let i in obj) {
console.log(i); // 无输出
}
Object.getOwnPropertyNames(obj) // []
Object.getOwnPropertySymbols(obj) // [Symbol(foo)]
```
`Reflect.ownKeys()`方法可以返回所有类型的键名,包括常规键名和 Symbol 键名。
```js
let obj = {
[Symbol('my_key')]: 1,
enum: 2,
nonEnum: 3
};
Reflect.ownKeys(obj)
// ["enum", "nonEnum", Symbol(my_key)]
```
由于以 Symbol 值作为键名,不会被常规方法遍历得到。我们可以利用这个特性,为对象定义一些非私有的、但又希望只用于内部的方法。
```js
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) // 0
x.add('foo');
Collection.sizeOf(x) // 1
Object.keys(x) // ['0']
Object.getOwnPropertyNames(x) // ['0']
Object.getOwnPropertySymbols(x) // [Symbol(size)]
```
### 12.5 Symbol.for()、Symbol.keyFor()
`Symbol.for()`方法接受一个字符串作为参数,然后搜索有没有以该参数作为名称的Symbol值。如果有,就返回这个 Symbol 值,否则就新建一个以该字符串为名称的 Symbol 值,并将其注册到全局。
```js
let s1=Symbol.for('foo');
let s2=Symbol.for('foo');
s1===s2
//true
```
`Symbol.for()`与`Symbol()`这两种写法,都会生成新的 Symbol。它们的区别是,前者会被登记在全局环境中供搜索,后者不会。`Symbol.for()`不会每次调用就返回一个新的 Symbol 类型的值,而是会先检查给定的`key`是否已经存在,如果不存在才会新建一个值。
```js
Symbol.for("bar") === Symbol.for("bar")
// true
Symbol("bar") === Symbol("bar")
// false
```
`Symbol.keyFor()`方法返回一个已登记的Symbol类型值的`key`
```js
let s1 = Symbol.for("foo");
Symbol.keyFor(s1) // "foo"
let s2 = Symbol("foo");
Symbol.keyFor(s2) // undefined
```
> 注意,`Symbol.for()`为 Symbol 值登记的名字,是全局环境的,不管有没有在全局环境运行。
```js
function foo() {
return Symbol.for('bar');
}
const x = foo();
const y = Symbol.for('bar');
console.log(x === y); // true
```
`Symbol.for()`的这个全局登记特性,可以用在不同的 iframe 或 service worker 中取到同一个值
```js
iframe = document.createElement('iframe');
iframe.src = String(window.location);
document.body.appendChild(iframe);
iframe.contentWindow.Symbol.for('foo') === Symbol.for('foo')
// true
```
### 12.6 实例:模块的Singleton模式
`Singleton`模式值的是调用一个类,任何时候返回的都是同一个实例。
```js
//mod.js
function A(){
this.foo='hello';
}
if(!global._foo){
global._foo=new A();
}
module.exports=global._foo;
//加载模块
const a=require('./mod.js');
console.log(a.foo);
```
> 全局变量`global._foo`是可写的,任何文件都可以修改。
```js
global._foo = { foo: 'world' };
const a = require('./mod.js');
console.log(a.foo);
```
上面的代码,会使得加载`mod.js`的脚本都失真。
可以使用Symbol防止这种情况
```js
//mod.js
const FOO_KEY=Symbol.for('foo');
function A(){
globalThis.foo='hello';
}
if(!globalThis[FOO_KEY]){
globalThis[FOO_KEY]=new A();
}
module.exports=globalThis[FOO_KEY];
//上面代码中,可以保证global[FOO_KEY]不会被无意间覆盖,但还是可以被改写。
global[Symbol.for('foo')] = { foo: 'world' };
const a = require('./mod.js');
```
如果键名使用`Symbol`方法生成,那么外部将无法引用这个值,当然也就无法改写。
### 12.7 内置的Symbol
#### 12.7.1 Symbol.hasInstance
对象的`Symbol.hasInstance`属性,指向一个内部方法。当其他对象使用`instanceof`运算符,判断是否为该对象的实例时,会调用这个方法。比如,`foo instanceof Foo`在语言内部,实际调用的是`Foo[Symbol.hasInstance](foo)`。
```js
class MyClass {
[Symbol.hasInstance](foo) {
return foo instanceof Array;
}
}
[1, 2, 3] instanceof new MyClass() // true
class Even {
static [Symbol.hasInstance](obj) {
return Number(obj) % 2 === 0;
}
}
// 等同于
const Even = {
[Symbol.hasInstance](obj) {
return Number(obj) % 2 === 0;
}
};
1 instanceof Even // false
2 instanceof Even // true
12345 instanceof Even // false
```
#### 12.7.2 Symbol.isConcatSpreadable
对象的`Symbol.isConcatSpreadable`属性等于一个布尔值,表示该对象用于`Array.prototype.concat()`时,是否可以展开。
```js
let arr1 = ['c', 'd'];
['a', 'b'].concat(arr1, 'e') // ['a', 'b', 'c', 'd', 'e']
arr1[Symbol.isConcatSpreadable] // undefined
let arr2 = ['c', 'd'];
arr2[Symbol.isConcatSpreadable] = false;
['a', 'b'].concat(arr2, 'e') // ['a', 'b', ['c','d'], 'e']
```
上面代码说明,数组的默认行为是可以展开,`Symbol.isConcatSpreadable`默认等于`undefined`。该属性等于`true`时,也有展开的效果。
类似数组的对象正好相反,默认不展开。它的`Symbol.isConcatSpreadable`属性设为`true`,才可以展开。
```js
let obj = {length: 2, 0: 'c', 1: 'd'};
['a', 'b'].concat(obj, 'e') // ['a', 'b', obj, 'e']
obj[Symbol.isConcatSpreadable] = true;
['a', 'b'].concat(obj, 'e') // ['a', 'b', 'c', 'd', 'e']
```
`Symbol.isConcatSpreadable`属性也可以定义在类里面。
```js
class A1 extends Array {
constructor(args) {
super(args);
this[Symbol.isConcatSpreadable] = true;
}
}
class A2 extends Array {
constructor(args) {
super(args);
}
get [Symbol.isConcatSpreadable] () {
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]]
```
#### 12.7.3 Symbol.species
对象的`Symbol.species`属性,指向一个构造函数。创建衍生对象时,会使用该属性。
```js
class MyArray extends Array {
}
const a = new MyArray(1, 2, 3);
const b = a.map(x => x);
const c = a.filter(x => x > 1);
b instanceof MyArray // true
c instanceof MyArray // true
```
上面代码中,子类`MyArray`继承了父类`Array`,`a`是`MyArray`的实例,`b`和`c`是`a`的衍生对象。你可能会认为,`b`和`c`都是调用数组方法生成的,所以应该是数组(`Array`的实例),但实际上它们也是`MyArray`的实例。
`Symbol.species`属性就是为了解决这个问题而提供的。现在,我们可以为`MyArray`设置`Symbol.species`属性。
```js
class MyArray extends Array {
static get [Symbol.species]() { return Array; }
}
class MyArray extends Array {
static get [Symbol.species]() { return Array; }
}
const a = new MyArray();
const b = a.map(x => x);
b instanceof MyArray // false
b instanceof Array // true
class T1 extends Promise {
}
class T2 extends Promise {
static get [Symbol.species]() {
return Promise;
}
}
new T1(r => r()).then(v => v) instanceof T1 // true
new T2(r => r()).then(v => v) instanceof T2 // false
```
#### 12.7.4 Symbol.match
对象的`Symbol.match`属性,指向一个函数。当执行`str.match(myObject)`时,如果该属性存在,会调用它,返回该方法的返回值。
```js
String.prototype.match(regexp)
// 等同于
regexp[Symbol.match](this)
class MyMatcher {
[Symbol.match](string) {
return 'hello world'.indexOf(string);
}
}
'e'.match(new MyMatcher()) // 1
```
#### 12.7.5 Symbol.replace
对象的`Symbol.replace`属性,指向一个方法,当该对象被`String.prototype.replace`方法调用时,会返回该方法的返回值。
```js
String.prototype.replace(searchValue, replaceValue)
// 等同于
searchValue[Symbol.replace](this, replaceValue)
const x = {};
x[Symbol.replace] = (...s) => console.log(s);
'Hello'.replace(x, 'World') // ["Hello", "World"]
```
#### 12.7.6 Symbol.search
对象的`Symbol.search`属性,指向一个方法,当该对象被`String.prototype.search`方法调用时,会返回该方法的返回值。
```js
String.prototype.search(regexp)
// 等同于
regexp[Symbol.search](this)
class MySearch {
constructor(value) {
this.value = value;
}
[Symbol.search](string) {
return string.indexOf(this.value);
}
}
'foobar'.search(new MySearch('foo')) // 0
```
#### 12.7.7 Symbol.split
对象的`Symbol.split`属性,指向一个方法,当该对象被`String.prototype.split`方法调用时,会返回该方法的返回值。
```js
String.prototype.split(separator, limit)
// 等同于
separator[Symbol.split](this, limit)
class MySplitter {
constructor(value) {
this.value = value;
}
[Symbol.split](string) {
let index = string.indexOf(this.value);
if (index === -1) {
return string;
}
return [
string.substr(0, index),
//加上分割符的长度
string.substr(index + this.value.length)
];
}
}
'foobar'.split(new MySplitter('foo'))
// ['', 'bar']
'foobar'.split(new MySplitter('bar'))
// ['foo', '']
'foobar'.split(new MySplitter('baz'))
// 'foobar'
```
#### 12.7.8 Symbol.iterator
对象的`Symbol.iterator`属性,指向该对象的默认遍历器方法。
```js
const myIterable = {};
myIterable[Symbol.iterator] = function* () {
yield 1;
yield 2;
yield 3;
};
[...myIterable] // [1, 2, 3]
class Collection {
*[Symbol.iterator]() {
let i = 0;
while(this[i] !== undefined) {
yield this[i];
++i;
}
}
}
let myCollection = new Collection();
myCollection[0] = 1;
myCollection[1] = 2;
for(let value of myCollection) {
console.log(value);
}
// 1
// 2
```
#### 12.7.9 Symbol.toPrimitive
对象的`Symbol.toPrimitive`属性,指向一个方法。该对象被转为原始类型的值时,会调用这个方法,返回该对象对应的原始类型值。
`Symbol.toPrimitive`被调用时,会接受一个字符串参数,表示当前运算的模式,一共有三种模式。
- Number:该场合需要转成数值
- String:该场合需要转成字符串
- Default:该场合可以转成数值,也可以转成字符串
```js
let obj = {
[Symbol.toPrimitive](hint) {
switch (hint) {
case 'number':
return 123;
case 'string':
return 'str';
case 'default':
return 'default';
default:
throw new Error();
}
}
};
2 * obj // 246
3 + obj // '3default'
obj == 'default' // true
String(obj) // 'str'
```
#### 12.7.10 Symbol.toStringTag
对象的`Symbol.toStringTag`属性,指向一个方法。在该对象上面调用`Object.prototype.toString`方法时,如果这个属性存在,它的返回值会出现在`toString`方法返回的字符串之中,表示对象的类型。也就是说,这个属性可以用来定制`[object Object]`或`[object Array]`中`object`后面的那个字符串。
```js
// 例一
({[Symbol.toStringTag]: 'Foo'}.toString())
// "[object Foo]"
// 例二
class Collection {
get [Symbol.toStringTag]() {
return 'xxx';
}
}
let x = new Collection();
Object.prototype.toString.call(x) // "[object xxx]"
```
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’
#### 12.7.11 Symbol.unscopables
对象的`Symbol.unscopables`属性,指向一个对象。该对象指定了使用`with`关键字时,哪些属性会被`with`环境排除。
```js
Array.prototype[Symbol.unscopables]
// {
// copyWithin: true,
// entries: true,
// fill: true,
// find: true,
// findIndex: true,
// includes: true,
// keys: true
// }
Object.keys(Array.prototype[Symbol.unscopables])
// ['copyWithin', 'entries', 'fill', 'find', 'findIndex', 'includes', 'keys']
// 没有 unscopables 时
class MyClass {
foo() { return 1; }
}
var foo = function () { return 2; };
with (MyClass.prototype) {
foo(); // 1
}
// 有 unscopables 时
class MyClass {
foo() { return 1; }
get [Symbol.unscopables]() {
return { foo: true };
}
}
var foo = function () { return 2; };
with (MyClass.prototype) {
foo(); // 2
}
```
## 13. Set和Map数据结构
### 13.1 Set
#### 13.1.1 基本用法
ES6 提供了新的数据结构 Set。它类似于数组,但是成员的值都是唯一的,没有重复的值。
`Set`本身是一个构造函数,用来生成Set数据结构。
```js
const s = new Set();
[2, 3, 5, 4, 5, 2, 2].forEach(x => s.add(x));
for (let i of s) {
console.log(i);
}
// 2 3 5 4
```
上面代码通过`add()`方法向 Set 结构加入成员,结果表明 Set 结构不会添加重复的值。
`Set`函数可以接受一个数组(或者具有 iterable 接口的其他数据结构)作为参数,用来初始化。
```js
// 例一
const set = new Set([1, 2, 3, 4, 4]);
[...set]
// [1, 2, 3, 4]
// 例二
const items = new Set([1, 2, 3, 4, 5, 5, 5, 5]);
items.size // 5
// 例三
const set = new Set(document.querySelectorAll('div'));
set.size // 56
// 类似于
const set = new Set();
document
.querySelectorAll('div')
.forEach(div => set.add(div));
set.size // 56
// 去除数组的重复成员
[...new Set(array)]
[...new Set('ababbc')].join('')
//"abc"
```
向 Set 加入值的时候,不会发生类型转换,所以`5`和`"5"`是两个不同的值。Set 内部判断两个值是否不同,使用的算法叫做“Same-value-zero equality”,它类似于精确相等运算符(`===`),主要的区别是向 Set 加入值时认为`NaN`等于自身,而精确相等运算符认为`NaN`不等于自身。
```js
let set = new Set();
let a = NaN;
let b = NaN;
set.add(a);
set.add(b);
set // Set {NaN}
```
另外,两个对象总是不相等的。
```js
let set = new Set();
set.add({});
set.size // 1
set.add({});
set.size // 2
//上面代码表示,由于两个空对象不相等,所以它们被视为两个值。
```
#### 13.1.2 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()`:清除所有成员,没有返回值。
```js
s.add(1).add(2).add(2);
// 注意2被加入了两次
s.size // 2
s.has(1) // true
s.has(2) // true
s.has(3) // false
s.delete(2);
s.has(2) // false
// 对象的写法
const properties = {
'width': 1,
'height': 1
};
if (properties[someName]) {
// do something
}
// Set的写法
const properties = new Set();
properties.add('width');
properties.add('height');
if (properties.has(someName)) {
// do something
}
```
`Array.from`方法可以将 Set 结构转为数组。
```js
const items = new Set([1, 2, 3, 4, 5]);
const array = Array.from(items);
//去除数组重复成员的另一种方法
function dedupe(array) {
return Array.from(new Set(array));
}
dedupe([1, 1, 2, 3]) // [1, 2, 3]
```
#### 13.1.3 遍历操作
Set 结构的实例有四个遍历方法,可以用于遍历成员。
- `Set.prototype.keys()`:返回键名的遍历器
- `Set.prototype.values()`:返回键值的遍历器
- `Set.prototype.entries()`:返回键值对的遍历器
- `Set.prototype.forEach()`:使用回调函数遍历每个成员
需要特别指出的是,`Set`的遍历顺序就是插入顺序。这个特性有时非常有用,比如使用 Set 保存一个回调函数列表,调用时就能保证按照添加顺序调用。
**(1)`keys()`,`values()`,`entries()`**
`keys`方法、`values`方法、`entries`方法返回的都是遍历器对象。由于 Set 结构没有键名,只有键值(或者说键名和键值是同一个值),所以`keys`方法和`values`方法的行为完全一致。
```js
let set = new Set(['red', 'green', 'blue']);
for (let item of set.keys()) {
console.log(item);
}
// red
// green
// blue
for (let item of set.values()) {
console.log(item);
}
// red
// green
// blue
for (let item of set.entries()) {
console.log(item);
}
// ["red", "red"]
// ["green", "green"]
// ["blue", "blue"]
```
Set 结构的实例默认可遍历,它的默认遍历器生成函数就是它的`values`方法。
```js
Set.prototype[Symbol.iterator] === Set.prototype.values
// true
//直接用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`方法,用于对每个成员执行某种操作,没有返回值。
```js
let set = new Set([1, 4, 9]);
set.forEach((value, key) => console.log(key + ' : ' + value))
// 1 : 1
// 4 : 4
// 9 : 9
```
`forEach`方法的参数就是一个处理函数。该函数的参数与数组的`forEach`一致,依次为键值、键名、集合本身(上例省略了该参数)。
**(3)遍历的应用**
扩展运算符(`...`)内部使用`for...of`循环,所以也可以用于 Set 结构。
```js
let set = new Set(['red', 'green', 'blue']);
let arr = [...set];
// ['red', 'green', 'blue']
```
扩展运算符和 Set 结构相结合,就可以去除数组的重复成员。
```js
let arr = [3, 5, 2, 2, 5, 5];
let unique = [...new Set(arr)];
// [3, 5, 2]
```
而且,数组的`map`和`filter`方法也可以间接用于 Set 了。
```js
let set = new Set([1, 2, 3]);
set = new Set([...set].map(x => x * 2));
// 返回Set结构:{2, 4, 6}
let set = new Set([1, 2, 3, 4, 5]);
set = new Set([...set].filter(x => (x % 2) == 0));
// 返回Set结构:{2, 4}
```
因此使用 Set 可以很容易地实现并集(Union)、交集(Intersect)和差集(Difference)。
```js
let a = new Set([1, 2, 3]);
let b = new Set([4, 3, 2]);
// 并集
let union = new Set([...a, ...b]);
// Set {1, 2, 3, 4}
// 交集
let intersect = new Set([...a].filter(x => b.has(x)));
// set {2, 3}
// (a 相对于 b 的)差集
let difference = new Set([...a].filter(x => !b.has(x)));
// Set {1}
```
如果想在遍历操作中,同步改变原来的 Set 结构,目前没有直接的方法,但有两种变通方法。一种是利用原 Set 结构映射出一个新的结构,然后赋值给原来的 Set 结构;另一种是利用`Array.from`方法。
```js
// 方法一
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
```
### 13.2 WeakSet
**含义**
WeakSet 结构与 Set 类似,也是不重复的值的集合。但是,它与 Set 有两个区别。
首先,WeakSet 的成员只能是对象,而不能是其他类型的值。
```js
const ws = new WeakSet();
ws.add(1)
// TypeError: Invalid value used in weak set
ws.add(Symbol())
// TypeError: invalid value used in weak set
```
上面代码试图向 WeakSet 添加一个数值和`Symbol`值,结果报错,因为 WeakSet 只能放置对象。
其次,WeakSet 中的对象都是弱引用,即垃圾回收机制不考虑 WeakSet 对该对象的引用,也就是说,如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存,不考虑该对象还存在于 WeakSet 之中。
这是因为垃圾回收机制依赖引用计数,如果一个值的引用次数不为`0`,垃圾回收机制就不会释放这块内存。结束使用该值之后,有时会忘记取消引用,导致内存无法释放,进而可能会引发内存泄漏。WeakSet 里面的引用,都不计入垃圾回收机制,所以就不存在这个问题。因此,WeakSet 适合临时存放一组对象,以及存放跟对象绑定的信息。只要这些对象在外部消失,它在 WeakSet 里面的引用就会自动消失。
由于上面这个特点,WeakSet 的成员是不适合引用的,因为它会随时消失。另外,由于 WeakSet 内部有多少个成员,取决于垃圾回收机制有没有运行,运行前后很可能成员个数是不一样的,而垃圾回收机制何时运行是不可预测的,因此 ES6 规定 WeakSet 不可遍历。
这些特点同样适用于本章后面要介绍的 WeakMap 结构。
语法
WeakSet 是一个构造函数,可以使用`new`命令,创建 WeakSet 数据结构。
```js
const ws = new WeakSet();
```
作为构造函数,WeakSet 可以接受一个数组或类似数组的对象作为参数。(实际上,任何具有 Iterable 接口的对象,都可以作为 WeakSet 的参数。)该数组的所有成员,都会自动成为 WeakSet 实例对象的成员。
```js
const a = [[1, 2], [3, 4]];
const ws = new WeakSet(a);
// WeakSet {[1, 2], [3, 4]}
```
注意,是`a`数组的成员成为 WeakSet 的成员,而不是`a`数组本身。这意味着,数组的成员只能是对象。
```js
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 实例之中。
```js
const ws = new WeakSet();
const obj = {};
const foo = {};
ws.add(window);
ws.add(obj);
ws.has(window); // true
ws.has(foo); // false
ws.delete(window);
ws.has(window); // false
```
WeakSet 没有`size`属性,没有办法遍历它的成员。
```js
ws.size // undefined
ws.forEach // undefined
ws.forEach(function(item){ console.log('WeakSet has ' + item)})
// TypeError: undefined is not a function
```
WeakSet 的一个用处,是储存 DOM 节点,而不用担心这些节点从文档移除时,会引发内存泄漏。
```js
const foos = new WeakSet()
class Foo {
constructor() {
foos.add(this)
}
method () {
if (!foos.has(this)) {
throw new TypeError('Foo.prototype.method 只能在Foo的实例上调用!');
}
}
}
```
上面代码保证了`Foo`的实例方法,只能在`Foo`的实例上调用。这里使用 WeakSet 的好处是,`foos`对实例的引用,不会被计入内存回收机制,所以删除实例的时候,不用考虑`foos`,也不会出现内存泄漏。
### 13.3 Map
#### 13.3.1 含义和基本用法
JavaScript 的对象(Object),本质上是键值对的集合(Hash 结构),但是传统上只能用字符串当作键。这给它的使用带来了很大的限制。
```js
const data = {};
const element = document.getElementById('myDiv');
data[element] = 'metadata';
data['[object HTMLDivElement]'] // "metadata"
```
上面代码原意是将一个 DOM 节点作为对象`data`的键,但是由于对象只接受字符串作为键名,所以`element`被自动转为字符串`[object HTMLDivElement]`。
ES6 提供了 Map 数据结构。它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。也就是说,Object 结构提供了“字符串—值”的对应,Map 结构提供了“值—值”的对应,是一种更完善的 Hash 结构实现。如果你需要“键值对”的数据结构,Map 比 Object 更合适。
```js
const m = new Map();
const o = {p: 'Hello World'};
m.set(o, 'content')
m.get(o) // "content"
m.has(o) // true
m.delete(o) // true
m.has(o) // false
```
上面代码使用 Map 结构的`set`方法,将对象`o`当作`m`的一个键,然后又使用`get`方法读取这个键,接着使用`delete`方法删除了这个键。
作为构造函数,Map 也可以接受一个数组作为参数。该数组的成员是一个个表示键值对的数组。
```js
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`构造函数接受数组作为参数,实际上执行的是下面的算法。
```js
const items = [
['name', '张三'],
['title', 'Author']
];
const map = new Map();
items.forEach(
([key, value]) => map.set(key, value)
);
```
事实上,不仅仅是数组,任何具有 Iterator 接口、且每个成员都是一个双元素的数组的数据结构都可以当作`Map`构造函数的参数。这就是说,`Set`和`Map`都可以用来生成新的 Map。
```js
const set = new Set([
['foo', 1],
['bar', 2]
]);
const m1 = new Map(set);
m1.get('foo') // 1
const m2 = new Map([['baz', 3]]);
const m3 = new Map(m2);
m3.get('baz') // 3
```
如果对同一个键多次赋值,后面的值将覆盖前面的值。
```js
const map = new Map();
map
.set(1, 'aaa')
.set(1, 'bbb');
map.get(1) // "bbb"
//如果读取一个未知的键,则返回undefined。
```
> 注意,只有对同一个对象的引用,Map 结构才将其视为同一个键。这一点要非常小心。
>
> ```js
> const map = new Map();
> map.set(['a'], 555);
> map.get(['a']) // undefined
> ```
>
> 上面代码的`set`和`get`方法,表面是针对同一个键,但实际上这是两个不同的数组实例,内存地址是不一样的,因此`get`方法无法读取该键,返回`undefined`。
同理,同样的值的两个实例,在 Map 结构中被视为两个键。
```js
const map = new Map();
const k1 = ['a'];
const k2 = ['a'];
map
.set(k1, 111)
.set(k2, 222);
map.get(k1) // 111
map.get(k2) // 222
```
上面代码中,变量`k1`和`k2`的值是一样的,但是它们在 Map 结构中被视为两个键。
由上可知,Map 的键实际上是跟内存地址绑定的,只要内存地址不一样,就视为两个键。这就解决了同名属性碰撞(clash)的问题,我们扩展别人的库的时候,如果使用对象作为键名,就不用担心自己的属性与原作者的属性同名。
如果 Map 的键是一个简单类型的值(数字、字符串、布尔值),则只要两个值严格相等,Map 将其视为一个键,比如`0`和`-0`就是一个键,布尔值`true`和字符串`true`则是两个不同的键。另外,`undefined`和`null`也是两个不同的键。虽然`NaN`不严格相等于自身,但 Map 将其视为同一个键。
```js
let map = new Map();
map.set(-0, 123);
map.get(+0) // 123
map.set(true, 1);
map.set('true', 2);
map.get(true) // 1
map.set(undefined, 3);
map.set(null, 4);
map.get(undefined) // 3
map.set(NaN, 123);
map.get(NaN) // 123
```
#### 13.3.2 实例的属性和操作方法
**(1)size属性**
`size`属性返回 Map 结构的成员总数。
```js
const map = new Map();
map.set('foo', true);
map.set('bar', false);
map.size // 2
```
**(2)Map.prototype.set(key, value)**
`set`方法设置键名`key`对应的键值为`value`,然后返回整个 Map 结构。如果`key`已经有值,则键值会被更新,否则就新生成该键。
```js
const m = new Map();
m.set('edition', 6) // 键是字符串
m.set(262, 'standard') // 键是数值
m.set(undefined, 'nah') // 键是 undefined
//set方法返回的是当前的Map对象,因此可以采用链式写法。
let map = new Map()
.set(1, 'a')
.set(2, 'b')
.set(3, 'c');
```
**(3)Map.prototype.get(key)**
`get`方法读取`key`对应的键值,如果找不到`key`,返回`undefined`。
```js
const m = new Map();
const hello = function() {console.log('hello');};
m.set(hello, 'Hello ES6!') // 键是函数
m.get(hello) // Hello ES6!
```
**(4)Map.prototype.has(key)**
`has`方法返回一个布尔值,表示某个键是否在当前 Map 对象之中。
```js
const m = new Map();
m.set('edition', 6);
m.set(262, 'standard');
m.set(undefined, 'nah');
m.has('edition') // true
m.has('years') // false
m.has(262) // true
m.has(undefined) // true
```
**(5)Map.prototype.delete(key)**
`delete`方法删除某个键,返回`true`。如果删除失败,返回`false`。
```js
const m = new Map();
m.set(undefined, 'nah');
m.has(undefined) // true
m.delete(undefined)
m.has(undefined) // false
```
**(6)Map.prototype.clear()**
`clear`方法清除所有成员,没有返回值。
```js
let map = new Map();
map.set('foo', true);
map.set('bar', false);
map.size // 2
map.clear()
map.size // 0
```
#### 13.3.3 遍历方法
Map 结构原生提供三个遍历器生成函数和一个遍历方法。
- `Map.prototype.keys()`:返回键名的遍历器。
- `Map.prototype.values()`:返回键值的遍历器。
- `Map.prototype.entries()`:返回所有成员的遍历器。
- `Map.prototype.forEach()`:遍历 Map 的所有成员。
需要特别注意的是,Map 的遍历顺序就是插入顺序。
```js
const map = new Map([
['F', 'no'],
['T', 'yes'],
]);
for (let key of map.keys()) {
console.log(key);
}
// "F"
// "T"
for (let value of map.values()) {
console.log(value);
}
// "no"
// "yes"
for (let item of map.entries()) {
console.log(item[0], item[1]);
}
// "F" "no"
// "T" "yes"
// 或者
for (let [key, value] of map.entries()) {
console.log(key, value);
}
// "F" "no"
// "T" "yes"
// 等同于使用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 结构转为数组结构,比较快速的方法是使用扩展运算符(`...`)。
```js
const map = new Map([
[1, 'one'],
[2, 'two'],
[3, 'three'],
]);
[...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`方法)。
```js
const map0 = new Map()
.set(1, 'a')
.set(2, 'b')
.set(3, 'c');
const map1 = new Map(
[...map0].filter(([k, v]) => k < 3)
);
// 产生 Map 结构 {1 => 'a', 2 => 'b'}
const map2 = new Map(
[...map0].map(([k, v]) => [k * 2, '_' + v])
);
// 产生 Map 结构 {2 => '_a', 4 => '_b', 6 => '_c'}
```
此外,Map 还有一个`forEach`方法,与数组的`forEach`方法类似,也可以实现遍历。
```js
map.forEach(function(value, key, map) {
console.log("Key: %s, Value: %s", key, value);
});
```
`forEach`方法还可以接受第二个参数,用来绑定`this`。
```js
const reporter = {
report: function(key, value) {
console.log("Key: %s, Value: %s", key, value);
}
};
map.forEach(function(value, key, map) {
this.report(key, value);
}, reporter);
```
#### 13.3.4 与其他数组结构的相互转化
**(1)Map 转为数组**
前面已经提过,Map 转为数组最方便的方法,就是使用扩展运算符(`...`)。
```js
const myMap = new Map()
.set(true, 7)
.set({foo: 3}, ['abc']);
[...myMap]
// [ [ true, 7 ], [ { foo: 3 }, [ 'abc' ] ] ]
```
**(2)数组 转为 Map**
将数组传入 Map 构造函数,就可以转为 Map。
```js
new Map([
[true, 7],
[{foo: 3}, ['abc']]
])
// Map {
// true => 7,
// Object {foo: 3} => ['abc']
// }
```
**(3)Map 转为对象**
如果所有 Map 的键都是字符串,它可以无损地转为对象。
```js
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()`。
```js
let obj = {"a":1, "b":2};
let map = new Map(Object.entries(obj));
```
可以自己实现一个转换函数。
```js
function objToStrMap(obj) {
let strMap = new Map();
for (let k of Object.keys(obj)) {
strMap.set(k, obj[k]);
}
return strMap;
}
objToStrMap({yes: true, no: false})
// Map {"yes" => true, "no" => false}
```
**(5)Map 转为 JSON**
Map 转为 JSON 要区分两种情况。一种情况是,Map 的键名都是字符串,这时可以选择转为对象 JSON。
```js
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。
```js
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,正常情况下,所有键名都是字符串。
```js
function jsonToStrMap(jsonStr) {
return objToStrMap(JSON.parse(jsonStr));
}
jsonToStrMap('{"yes": true, "no": false}')
// Map {'yes' => true, 'no' => false}
```
但是,有一种特殊情况,整个 JSON 就是一个数组,且每个数组成员本身,又是一个有两个成员的数组。这时,它可以一一对应地转为 Map。这往往是 Map 转为数组 JSON 的逆操作。
```js
function jsonToMap(jsonStr) {
return new Map(JSON.parse(jsonStr));
}
jsonToMap('[[true,7],[{"foo":3},["abc"]]]')
// Map {true => 7, Object {foo: 3} => ['abc']}
```
### 13.4 WeakMap
`WeakMap`结构与`Map`结构类似,也是用于生成键值对的集合。
```js
// WeakMap 可以使用 set 方法添加成员
const wm1 = new WeakMap();
const key = {foo: 1};
wm1.set(key, 2);
wm1.get(key) // 2
// WeakMap 也可以接受一个数组,
// 作为构造函数的参数
const k1 = [1, 2, 3];
const k2 = [4, 5, 6];
const wm2 = new WeakMap([[k1, 'foo'], [k2, 'bar']]);
wm2.get(k2) // "bar"
```
`WeakMap`与`Map`的区别有两点。
`WeakMap`只接受对象作为键名(`null`除外),不接受其他类型的值作为键名。
```js
const map = new WeakMap();
map.set(1, 2)
// TypeError: 1 is not an object!
map.set(Symbol(), 2)
// TypeError: Invalid value used as weak map key
map.set(null, 2)
// TypeError: Invalid value used as weak map key
```
上面代码中,如果将数值`1`和`Symbol`值作为 WeakMap 的键名,都会报错。
其次,`WeakMap`的键名所指向的对象,不计入垃圾回收机制。
`WeakMap`的设计目的在于,有时我们想在某个对象上面存放一些数据,但是这会形成对于这个对象的引用。请看下面的例子。
```js
const e1 = document.getElementById('foo');
const e2 = document.getElementById('bar');
const arr = [
[e1, 'foo 元素'],
[e2, 'bar 元素'],
];
```
上面代码中,`e1`和`e2`是两个对象,我们通过`arr`数组对这两个对象添加一些文字说明。这就形成了`arr`对`e1`和`e2`的引用。
一旦不再需要这两个对象,我们就必须手动删除这个引用,否则垃圾回收机制就不会释放`e1`和`e2`占用的内存。
```js
// 不需要 e1 和 e2 的时候
// 必须手动删除引用
arr [0] = null;
arr [1] = null;
```
WeakMap 就是为了解决这个问题而诞生的,它的键名所引用的对象都是弱引用,即垃圾回收机制不将该引用考虑在内。因此,只要所引用的对象的其他引用都被清除,垃圾回收机制就会释放该对象所占用的内存。也就是说,一旦不再需要,WeakMap 里面的键名对象和所对应的键值对会自动消失,不用手动删除引用。
基本上,如果你要往对象上添加数据,又不想干扰垃圾回收机制,就可以使用 WeakMap。一个典型应用场景是,在网页的 DOM 元素上添加数据,就可以使用`WeakMap`结构。当该 DOM 元素被清除,其所对应的`WeakMap`记录就会自动被移除。
```js
const wm = new WeakMap();
const element = document.getElementById('example');
wm.set(element, 'some information');
wm.get(element) // "some information"
```
注意,WeakMap 弱引用的只是键名,而不是键值。键值依然是正常引用。
```js
const wm = new WeakMap();
let key = {};
let obj = {foo: 1};
wm.set(key, obj);
obj = null;
wm.get(key)
// Object {foo: 1}
```
## 14.Proxy
### 14.1 概述
Proxy 用于修改某些操作的默认行为,等同于在语言层面做出修改,所以属于一种“元编程”(meta programming),即对编程语言进行编程。
Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。Proxy 这个词的原意是代理,用在这里表示由它来“代理”某些操作,可以译为“代理器”。
```js
var obj = new Proxy({}, {
get: function (target, propKey, receiver) {
console.log(`getting ${propKey}!`);
return Reflect.get(target, propKey, receiver);
},
set: function (target, propKey, value, receiver) {
console.log(`setting ${propKey}!`);
return Reflect.set(target, propKey, value, receiver);
}
});
obj.count=1
//setting count!
++obj.count
//getting count!
//setting count!
//2
```
> Proxy实际上重载(overload)了点运算符,即用自己的定义覆盖了语言的原始定义。
ES6原生提供`Proxy`构造函数,用来生成`Proxy`实例。
```js
let proxy=new Proxy(target,handler);
```
Proxy 对象的所有用法,都是上面这种形式,不同的只是`handler`参数的写法。其中,`new Proxy()`表示生成一个`Proxy`实例,`target`参数表示所要拦截的目标对象,`handler`参数也是一个对象,用来定制拦截行为。
拦截读取属性行为的例子。
```js
let proxy = new Proxy({}, {
get: function(target, propKey) {
return 35;
}
});
proxy.time // 35
proxy.name // 35
proxy.title // 35
```
上面代码中,作为构造函数,`Proxy`接受两个参数。第一个参数是所要代理的目标对象(上例是一个空对象),即如果没有`Proxy`的介入,操作原来要访问的就是这个对象;第二个参数是一个配置对象,对于每一个被代理的操作,需要提供一个对应的处理函数,该函数将拦截对应的操作。
配置对象有一个`get`方法,用来拦截对目标对象属性的访问请求。`get`方法的两个参数分别是目标对象和所要访问的属性。可以看到,由于拦截函数总是返回`35`,所以访问任何属性都得到`35`。
注意,要使得`Proxy`起作用,必须针对`Proxy`实例(上例是`proxy`对象)进行操作,而不是针对目标对象(上例是空对象)进行操作。
如果`handler`没有设置任何拦截,那就等同于直接通向原对象。
```js
let target={},
handler={},
proxy=new Proxy(target,handler);
proxy.a='b';
target.a;//"b"
```
上面代码中,`handler`是一个空对象,没有任何拦截效果,访问`proxy`就等同于访问`target`。
一个技巧是将 Proxy 对象,设置到`object.proxy`属性,从而可以在`object`对象上调用。
```js
var object = { proxy: new Proxy(target, handler) };
```
Proxy 实例也可以作为其他对象的原型对象。
```js
var proxy = new Proxy({}, {
get: function(target, propKey) {
return 35;
}
});
let obj = Object.create(proxy);
obj.time // 35
```
上面代码中,`proxy`对象是`obj`对象的原型,`obj`对象本身并没有`time`属性,所以根据原型链,会在`proxy`对象上读取该属性,导致被拦截。
同一个拦截器函数,可以设置拦截多个操作。
```js
let 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]};
}
}
let 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)`。
### 14.2 Proxy实例的方法
#### 14.2.1 get()
`get`方法用于拦截某个属性的读取操作,可以接受三个参数,依次为目标对象、属性名和 proxy 实例本身(严格地说,是操作行为所针对的对象),其中最后一个参数可选。
```js
let person={
name:"张三"
};
let proxy=new Proxy(person,{
get:function(target,propKey){
if(propKey in target){
return target[propKey];
}else{
throw new ReferenceError("Prop name \"" + propKey + "\" does not exist.");
}
}
});
proxy.name // "张三"
proxy.age // 抛出一个错误
```
上面代码表示,如果访问目标对象不存在的属性,会抛出一个错误。如果没有这个拦截函数,访问不存在的属性,只会返回`undefined`。
`get`方法可以继承。
```js
let proto = new Proxy({}, {
get(target, propertyKey, receiver) {
console.log('GET ' + propertyKey);
return target[propertyKey];
}
});
let obj = Object.create(proto);
obj.foo // "GET foo"
```
上面代码中,拦截操作定义在`Prototype`对象上面,所以如果读取`obj`对象继承的属性时,拦截会生效。
使用`get`拦截,实现数组读取负数的索引。
```js
function createArray(...elements) {
let handler = {
get(target, propKey, receiver) {
let index = Number(propKey);
if (index < 0) {
propKey = String(target.length + index);
}
return Reflect.get(target, propKey, receiver);
}
};
let target = [];
target.push(...elements);
return new Proxy(target, handler);
}
let arr = createArray('a', 'b', 'c');
arr[-1] // c
arr[0] //a
```
利用 Proxy,可以将读取属性的操作(`get`),转变为执行某个函数,从而实现属性的链式操作。
```js
//链式操作
let pipe = function (value) {
let funcStack = [];
let 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;
}
let double = n => n * 2;
let pow = n => n * n;
let reverseInt = n => n.toString().split("").reverse().join("") | 0;
pipe(3).double.pow.reverseInt.get; // 63
```
利用`get`拦截,实现一个生成各种 DOM 节点的通用函数`dom`。
```js
const dom = new Proxy({}, {
get(target, property) {
return function(attrs = {}, ...children) {
const el = document.createElement(property);
for (let prop of Object.keys(attrs)) {
el.setAttribute(prop, attrs[prop]);
}
for (let child of children) {
if (typeof child === 'string') {
child = document.createTextNode(child);
}
el.appendChild(child);
}
return el;
}
}
});
const el = dom.div({},
'Hello, my name is ',
dom.a({href: '//example.com'}, 'Mark'),
'. I like:',
dom.ul({},
dom.li({}, 'The web'),
dom.li({}, 'Food'),
dom.li({}, '…actually that\'s it')
)
);
document.body.appendChild(el);
```
`get`方法的第三个参数的例子,它总是指向原始的读操作所在的那个对象,一般情况下就是 Proxy 实例。
```js
const proxy = new Proxy({}, {
get: function(target, key, receiver) {
return receiver;
}
});
proxy.getReceiver === proxy // true]
const proxy = new Proxy({}, {
get: function(target, key, receiver) {
return receiver;
}
});
const d = Object.create(proxy);
d.a === d // true
```
如果一个属性不可配置(configurable)且不可写(writable),则 Proxy 不能修改该属性,否则通过 Proxy 对象访问该属性会报错。
```js
const target = Object.defineProperties({}, {
foo: {
value: 123,
writable: false,
configurable: false
},
});
const handler = {
get(target, propKey) {
return 'abc';
}
};
const proxy = new Proxy(target, handler);
proxy.foo
// TypeError: Invariant check failed
```
#### 14.2.1 set()
`set`方法用来拦截某个属性的赋值操作,可以接受四个参数,依次为目标对象、属性名、属性值和 Proxy 实例本身,其中最后一个参数可选。
```js
let validator={
set:function(obj,prop,value){
if(prop==='age'){
if(!Number.isInteger(value)){
throw new TypeError("The age is not an integer");
}
if(value<0||value>200){
throw new TypeError("The age seems invalid");
}
}
obj[prop]=value;
}
}
let person=new Proxy({},validator);
person.age=100;
person.age="young";
person.age=0;
```