## 擴展運算符
### 含義
擴展運算符(spread)是三個點(`...`)。它好比 rest 參數的逆運算,將一個數組轉為用逗號分隔的參數序列。
~~~javascript
console.log(...[1, 2, 3])
// 1 2 3
console.log(1, ...[2, 3, 4], 5)
// 1 2 3 4 5
[...document.querySelectorAll('div')]
// [<div>, <div>, <div>]
~~~
該運算符主要用于函數調用。
~~~javascript
function push(array, ...items) {
array.push(...items);
}
function add(x, y) {
return x + y;
}
const numbers = [4, 38];
add(...numbers) // 42
~~~
上面代碼中,`array.push(...items)`和`add(...numbers)`這兩行,都是函數的調用,它們都使用了擴展運算符。該運算符將一個數組,變為參數序列。
擴展運算符與正常的函數參數可以結合使用,非常靈活。
~~~javascript
function f(v, w, x, y, z) { }
const args = [0, 1];
f(-1, ...args, 2, ...[3]);
~~~
擴展運算符后面還可以放置表達式。
~~~javascript
const arr = [
...(x > 0 ? ['a'] : []),
'b',
];
~~~
如果擴展運算符后面是一個空數組,則不產生任何效果。
~~~javascript
[...[], 1]
// [1]
~~~
注意,只有函數調用時,擴展運算符才可以放在圓括號中,否則會報錯。
~~~javascript
(...[1, 2])
// Uncaught SyntaxError: Unexpected number
console.log((...[1, 2]))
// Uncaught SyntaxError: Unexpected number
console.log(...[1, 2])
// 1 2
~~~
上面三種情況,擴展運算符都放在圓括號里面,但是前兩種情況會報錯,因為擴展運算符所在的括號不是函數調用。
## 對象的擴展運算符
《數組的擴展》一章中,已經介紹過擴展運算符(`...`)。ES2018 將這個運算符[引入](https://github.com/sebmarkbage/ecmascript-rest-spread)了對象。
### 解構賦值
對象的解構賦值用于從一個對象取值,相當于將目標對象自身的所有可遍歷的(enumerable)、但尚未被讀取的屬性,分配到指定的對象上面。所有的鍵和它們的值,都會拷貝到新對象上面。
~~~javascript
let { x, y, ...z } = { x: 1, y: 2, a: 3, b: 4 };
x // 1
y // 2
z // { a: 3, b: 4 }
~~~
上面代碼中,變量`z`是解構賦值所在的對象。它獲取等號右邊的所有尚未讀取的鍵(`a`和`b`),將它們連同值一起拷貝過來。
由于解構賦值要求等號右邊是一個對象,所以如果等號右邊是`undefined`或`null`,就會報錯,因為它們無法轉為對象。
~~~javascript
let { ...z } = null; // 運行時錯誤
let { ...z } = undefined; // 運行時錯誤
~~~
解構賦值必須是最后一個參數,否則會報錯。
~~~javascript
let { ...x, y, z } = someObject; // 句法錯誤
let { x, ...y, ...z } = someObject; // 句法錯誤
~~~
上面代碼中,解構賦值不是最后一個參數,所以會報錯。
注意,解構賦值的拷貝是淺拷貝,即如果一個鍵的值是復合類型的值(數組、對象、函數)、那么解構賦值拷貝的是這個值的引用,而不是這個值的副本。
~~~javascript
let obj = { a: { b: 1 } };
let { ...x } = obj;
obj.a.b = 2;
x.a.b // 2
~~~
上面代碼中,`x`是解構賦值所在的對象,拷貝了對象`obj`的`a`屬性。`a`屬性引用了一個對象,修改這個對象的值,會影響到解構賦值對它的引用。
另外,擴展運算符的解構賦值,不能復制繼承自原型對象的屬性。
~~~javascript
let o1 = { a: 1 };
let o2 = { b: 2 };
o2.__proto__ = o1;
let { ...o3 } = o2;
o3 // { b: 2 }
o3.a // undefined
~~~
上面代碼中,對象`o3`復制了`o2`,但是只復制了`o2`自身的屬性,沒有復制它的原型對象`o1`的屬性。
下面是另一個例子。
~~~javascript
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
~~~
上面代碼中,變量`x`是單純的解構賦值,所以可以讀取對象`o`繼承的屬性;變量`y`和`z`是擴展運算符的解構賦值,只能讀取對象`o`自身的屬性,所以變量`z`可以賦值成功,變量`y`取不到值。ES6 規定,變量聲明語句之中,如果使用解構賦值,擴展運算符后面必須是一個變量名,而不能是一個解構賦值表達式,所以上面代碼引入了中間變量`newObj`,如果寫成下面這樣會報錯。
~~~javascript
let { x, ...{ y, z } } = o;
// SyntaxError: ... must be followed by an identifier in declaration contexts
~~~
解構賦值的一個用處,是擴展某個函數的參數,引入其他操作。
~~~javascript
function baseFunction({ a, b }) {
// ...
}
function wrapperFunction({ x, y, ...restConfig }) {
// 使用 x 和 y 參數進行操作
// 其余參數傳給原始函數
return baseFunction(restConfig);
}
~~~
上面代碼中,原始函數`baseFunction`接受`a`和`b`作為參數,函數`wrapperFunction`在`baseFunction`的基礎上進行了擴展,能夠接受多余的參數,并且保留原始函數的行為。
### 擴展運算符
對象的擴展運算符(`...`)用于取出參數對象的所有可遍歷屬性,拷貝到當前對象之中。
~~~javascript
let z = { a: 3, b: 4 };
let n = { ...z };
n // { a: 3, b: 4 }
~~~
由于數組是特殊的對象,所以對象的擴展運算符也可以用于數組。
~~~javascript
let foo = { ...['a', 'b', 'c'] };
foo
// {0: "a", 1: "b", 2: "c"}
~~~
如果擴展運算符后面是一個空對象,則沒有任何效果。
~~~javascript
{...{}, a: 1}
// { a: 1 }
~~~
如果擴展運算符后面不是對象,則會自動將其轉為對象。
~~~javascript
// 等同于 {...Object(1)}
{...1} // {}
~~~
上面代碼中,擴展運算符后面是整數`1`,會自動轉為數值的包裝對象`Number{1}`。由于該對象沒有自身屬性,所以返回一個空對象。
下面的例子都是類似的道理。
~~~javascript
// 等同于 {...Object(true)}
{...true} // {}
// 等同于 {...Object(undefined)}
{...undefined} // {}
// 等同于 {...Object(null)}
{...null} // {}
~~~
但是,如果擴展運算符后面是字符串,它會自動轉成一個類似數組的對象,因此返回的不是空對象。
~~~javascript
{...'hello'}
// {0: "h", 1: "e", 2: "l", 3: "l", 4: "o"}
~~~
對象的擴展運算符等同于使用`Object.assign()`方法。
~~~javascript
let aClone = { ...a };
// 等同于
let aClone = Object.assign({}, a);
~~~
上面的例子只是拷貝了對象實例的屬性,如果想完整克隆一個對象,還拷貝對象原型的屬性,可以采用下面的寫法。
~~~javascript
// 寫法一
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)
)
~~~
上面代碼中,寫法一的`__proto__`屬性在非瀏覽器的環境不一定部署,因此推薦使用寫法二和寫法三。
擴展運算符可以用于合并兩個對象。
~~~javascript
let ab = { ...a, ...b };
// 等同于
let ab = Object.assign({}, a, b);
~~~
如果用戶自定義的屬性,放在擴展運算符后面,則擴展運算符內部的同名屬性會被覆蓋掉。
~~~javascript
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 });
~~~
上面代碼中,`a`對象的`x`屬性和`y`屬性,拷貝到新對象后會被覆蓋掉。
這用來修改現有對象部分的屬性就很方便了。
~~~javascript
let newVersion = {
...previousVersion,
name: 'New Name' // Override the name property
};
~~~
上面代碼中,`newVersion`對象自定義了`name`屬性,其他屬性全部復制自`previousVersion`對象。
如果把自定義屬性放在擴展運算符前面,就變成了設置新對象的默認屬性值。
~~~javascript
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);
~~~
與數組的擴展運算符一樣,對象的擴展運算符后面可以跟表達式。
~~~javascript
const obj = {
...(x > 1 ? {a: 1} : {}),
b: 2,
};
~~~
擴展運算符的參數對象之中,如果有取值函數`get`,這個函數是會執行的。
~~~javascript
let a = {
get x() {
throw new Error('not throw yet');
}
}
let aWithXGetter = { ...a }; // 報錯
~~~
上面例子中,取值函數`get`在擴展`a`對象時會自動執行,導致報錯。