【譯】ES2018 新特性:Rest/Spread 特性

Sebastian Markb?ge 提出的 Rest/Spread Properties 提案包括兩部分:

  • 用于對象解構的 rest 操作符(...)。目前,這個操作符只能在數組解構和參數定義中使用
  • 對象字面量中的 spread 操作符(...)。目前,這個操作符只能用于數組字面量和在函數方法中調用。

對象解構中的 rest 操作符(...)

在對象解構模式下,rest 操作符會將解構源的除了已經在對象字面量中指明的屬性之外的,所有可枚舉自有屬性拷貝到它的運算對象中。

const obj = {foo: 1, bar: 2, baz: 3};
const {foo, ...rest} = obj;
    // Same as:
    // const foo = 1;
    // const rest = {bar: 2, baz: 3};

如果你正在使用對象解構來處理命名參數,rest 操作符讓你可以收集所有剩余參數:

function func({param1, param2, ...rest}) { // rest operator
    console.log('All parameters: ',{param1, param2, ...rest}); // spread operator
    return param1 + param2;
}

語法限制

在每個對象字面量的頂層,可以使用 rest 操作符最多一次,并且必須只能在末尾出現:

const {...rest, foo} = obj; // SyntaxError
const {foo, ...rest1, ...rest2} = obj; // SyntaxError

如果是嵌套結構,你可以多次使用 rest 操作符:

const obj = {
    foo: {
        a: 1,
        b: 2,
        c: 3,
    },
    bar: 4,
    baz: 5,
};

const {foo: {a, ...rest1}, ...rest2} = obj;

// Same as:
// const a = 1;
// const rest1 = {b: 2, c: 3};
// const rest2 = {bar: 4, baz: 5};

對象字面量中的 spread 操作符

對象字面量內部,spread 操作符將自身運算對象的所有可枚舉的自有屬性,插入到通過字面量創建的對象中:

> const obj = {foo: 1, bar: 2, baz: 3};
> {...obj, qux: 4}
{ foo: 1, bar: 2, baz: 3, qux: 4 }

要注意的是順序問題,即使屬性 key 并不沖突,因為對象會記錄插入順序:

> {qux: 4, ...obj}
{ qux: 4, foo: 1, bar: 2, baz: 3 }

如果 key 出現了沖突,后面的會覆蓋前面的屬性:

> const obj = {foo: 1, bar: 2, baz: 3};
> {...obj, foo: true}
{ foo: true, bar: 2, baz: 3 }
> {foo: true, ...obj}
{ foo: 1, bar: 2, baz: 3 }

對象 spread 操作符的使用場景

這一節,我們會看看 spread 操作符的使用場景。我也會用 Object.assign() 實現一遍,它和 spread 操作符很相似(之后我們會更詳細地比較它們)。

拷貝對象

拷貝對象 obj 的可枚舉自有屬性:

const clone1 = {...obj};
const clone2 = Object.assign({}, obj);

clone 對象們的原型都是 Object.prototype,它是所有通過對象字面量創建的對象的默認原型:

> Object.getPrototypeOf(clone1) === Object.prototype
true
> Object.getPrototypeOf(clone2) === Object.prototype
true
> Object.getPrototypeOf({}) === Object.prototype
true

拷貝一個對象 obj,包括它的原型:

const clone1 = {__proto__: Object.getPrototypeOf(obj), ...obj};
const clone2 = Object.assign(
    Object.create(Object.getPrototypeOf(obj)), obj);

注意,一般來說,對象字面量內部的 proto 只是瀏覽器內置的特性,并非 JavaScript 引擎所有。

對象的真拷貝

有時候,你需要老老實實地拷貝對象的所有自有屬性(properties)和特性(writable, enumerable, …),包括 getters 和 setters。這時候 Object.assign() 和 spread 操作符就回天乏術了。你需要使用屬性描述符(property descriptors):

const clone1 = Object.defineProperties({},
    Object.getOwnPropertyDescriptors(obj));

如果還希望保留 obj 的原型,可以用 Object.create()

const clone2 = Object.create(
    Object.getPrototypeOf(obj),
    Object.getOwnPropertyDescriptors(obj));

“探索 ES2016 and ES2017” 里介紹了 Object.getOwnPropertyDescriptors()

陷阱:總是淺拷貝

我們之前見過的所有拷貝對象的方式,都是淺拷貝:如果原始屬性值是一個對象,拷貝的對象將指向同一個對象,它不會(遞歸的、深度的)拷貝自身:

const original = { prop: {} };
const clone = Object.assign({}, original);

console.log(original.prop === clone.prop); // true
original.prop.foo = 'abc';
console.log(clone.prop.foo); // abc

其他使用場景

合并 obj1 和 obj2 兩個對象:

const merged = {...obj1, ...obj2};
const merged = Object.assign({}, obj1, obj2);

給用戶數據填充默認值

const DEFAULTS = {foo: 'a', bar: 'b'};
const userData = {foo: 1};

const data = {...DEFAULTS, ...userData};
const data = Object.assign({}, DEFAULTS, userData);
    // {foo: 1, bar: 'b'}

安全地更新屬性 foo:

const obj = {foo: 'a', bar: 'b'};
const obj2 = {...obj, foo: 1};
const obj2 = Object.assign({}, obj, {foo: 1});
    // {foo: 1, bar: 'b'}

指定屬性 foo 和 bar 的默認值:

const userData = {foo: 1};
const data = {foo: 'a', bar: 'b', ...userData};
const data = Object.assign({}, {foo:'a', bar:'b'}, userData);
    // {foo: 1, bar: 'b'}

展開對象 VS Object.assign()

spread 操作符和 Object.assign() 很相似。主要的區別在于前者定義了新屬性,而后者還進行了賦值。稍后將解釋這究竟意味著什么。

Object.assign() 的兩種使用方式

Object.assign() 有兩種使用方式:
第一種,帶有破壞性的(修改已有對象):

Object.assign(target, source1, source2);

這里的 target 對象被修改了;source1 和 source2 被拷貝進去了。
第二種,非破壞性的(已有對象不會被修改):

const result = Object.assign({}, source1, source2);

新對象是通過將 source1 和 source2 拷貝進一個空對象而生成的。最終,這個新對象被返回并賦值給 result。
spread 操作符類似于 Object.assign() 的第二種方式。接下來,我們來看看兩者的相似和不同之處。

都是通過 "get" 操作符讀值

在寫對象之前,兩者都使用了 ”get“ 操作符去讀取源對象的屬性值。這一過程會將 getter 被轉換成正常的數據屬性。
來看個例子:

const original = {
    get foo() {
        return 123;
    }
};

original 有一個 foo getter(它的屬性描述符有 get 和 set 屬性)

> Object.getOwnPropertyDescriptor(original, 'foo')
{ get: [Function: foo],
  set: undefined,
  enumerable: true,
  configurable: true }

但是在它拷貝的結果 clone1 和 clone2 里,foo 是一個正常的數據屬性(屬性描述符有value 和 writable 屬性):

> const clone1 = {...original};
> Object.getOwnPropertyDescriptor(clone1, 'foo')
{ value: 123,
  writable: true,
  enumerable: true,
  configurable: true }

> const clone2 = Object.assign({}, original);
> Object.getOwnPropertyDescriptor(clone2, 'foo')
{ value: 123,
  writable: true,
  enumerable: true,
  configurable: true }

spread 定義屬性,Object.assign() 設置屬性

spread 操作符在目標對象上定義了新的屬性,而Object.assign() 使用了一個 "set" 操作符來創建屬性。這會導致兩個結果:

目標對象帶有 setter

首先,Object.assign() 觸發 setter,而 spread 不會:

Object.defineProperty(Object.prototype, 'foo', {
    set(value) {
        console.log('SET', value);
    },
});
const obj = {foo: 123};

以上代碼段設置了一個 foo setter,它會被所有普通對象繼承。
如果我們通過 Object.assign() 拷貝 obj,繼承的 setter 會被觸發:

> Object.assign({}, obj)
SET 123
{}

而 spread 就不會:

> { ...obj }
{ foo: 123 }

Object.assign() 在拷貝時還會觸發自有 setter,這里并沒有發生重寫。

目標對象帶有只讀屬性

第二,你可以通過繼承只讀屬性,來阻止 Object.assign() 創建自有屬性,但 spread 上這是做不到的:

Object.defineProperty(Object.prototype, 'bar', {
    writable: false,
    value: 'abc',
});

以上代碼設置了只讀屬性 bar,它會被所有普通對象繼承。
這樣,你就再也不能使用賦值語句去創建自有屬性 bar(嚴格模式下會拋一個異常,寬松模式會靜默失敗):

> const tmp = {};
> tmp.bar = 123;
TypeError: Cannot assign to read only property 'bar'

下列代碼,我們使用對象字面量成功地創建了屬性 bar。因為對象字面量沒有設置屬性,它只是定義了它們:

const obj = {bar: 123};

然而,Object.assign() 使用賦值語句創建屬性,這就是不能拷貝 obj 的原因:

> Object.assign({}, obj)
TypeError: Cannot assign to read only property 'bar'

通過 spread 操作符拷貝沒有問題:

> { ...obj }
{ bar: 123 }

spread 和 Object.assign() 都只拷貝自有可枚舉屬性

它們都會忽略所有繼承的屬性和不可枚舉的自有屬性。
對象 obj 從 proto 繼承了一個可枚舉屬性,并且有兩個自有屬性:

const proto = {
    inheritedEnumerable: 1,
};
const obj = Object.create(proto, {
    ownEnumerable: {
        value: 2,
        enumerable: true,
    },
    ownNonEnumerable: {
        value: 3,
        enumerable: false,
    },
});

如果拷貝 obj,結果將只有屬性 ownEnumerable。屬性 inheritedEnumerable 和 ownNonEnumerable 沒有被拷貝:

> {...obj}
{ ownEnumerable: 2 }
> Object.assign({}, obj)
{ ownEnumerable: 2 }

原文:http://exploringjs.com/es2018-es2019/ch_rest-spread-properties.html

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 1.ios高性能編程 (1).內層 最小的內層平均值和峰值(2).耗電量 高效的算法和數據結構(3).初始化時...
    歐辰_OSR閱讀 29,582評論 8 265
  • 屬性的簡潔表示法 ES6允許直接寫入變量和函數,作為對象的屬性和方法。 上面代碼表明,ES6允許在對象之中,直接寫...
    oWSQo閱讀 523評論 0 0
  • 函數和對象 1、函數 1.1 函數概述 函數對于任何一門語言來說都是核心的概念。通過函數可以封裝任意多條語句,而且...
    道無虛閱讀 4,635評論 0 5
  • 佛陀和弟子在一起,弟子問佛陀:“佛陀,世間為何這般苦?”佛陀曰:“只因不識自我。”人一生為什么會做錯這么多的事情,...
    勤勵閱讀 633評論 0 0
  • 昨晚偷懶了,今天也不想寫。我在猶豫要去睡覺呢還是看書呢
    Shadowsnow閱讀 172評論 0 0