你要知道的 - Spread Operator for objects 技巧

Yzz發表於2019-01-09

今天看到了你必須收藏的 ES6 語法密糖 - Spread Operator 技巧,這篇文章,收穫很多,同時也想起來 ... 也有一些操作物件的用法,總結了一下。

在 ECMAScript 2018 中 Spread Operator 增加了對物件的支援,使得它的應用更為廣泛,本文重點介紹如何將它與 Object 一起使用以及與 Object.assgin 的區別。

可以通過BABEL,檢視示例程式碼 babel 編譯後的結果。

... 解構賦值

除去已經宣告的屬性,剩餘的所有屬性都會賦給 ... 後的屬性名

let { x, ...y } = { x: 1, y: 2, a: 3, b: 4 };
console.log(x); // 1
console.log(y); // {y: 2, a: 3, b: 4}
複製程式碼
... 刪除屬性值

利用 ... 來刪除物件中的某一個屬性

let { x: deleted, ...y } = { x: 1, y: 2, a: 3, b: 4 };
console.log(y); // {y: 2, a: 3, b: 4}
複製程式碼
... 複製物件

在 JavaScript 中,有一個常見賦值語法如下

var cat = { age: 4 };
var kitten = cat;
kitten.age = 1;
複製程式碼

此時, catkitten 引用同一個物件,如果修改了 kitten 的屬性,相應的 cat 也會發生變化。

console.log(kitten.age); // 1
console.log(cat.age); // 1 <-- problem!
複製程式碼

使用 Spread Operator 可以輕鬆地建立一個具有現有物件的所有相同屬性的新物件。

const cat = { age: 4 };
const kitten = { ...cat }; // <-- changed
kitten.age = 1;

console.log(kitten.age); // 1
console.log(cat.age); // 4 <-- fixed!
複製程式碼

但是,利用 Spread Operator 去賦值物件,只能完成淺複製,也就是說利用 ... 去複製物件時,並不能遞迴地複製所有層級。

const cat = { age: 4, toys: ["mouse", "catnip"] };
const kitten = { ...cat };
// const kitten = Object.assign({}, cat); <-- same result
kitten.toys[1] = "yarn";
console.log(kitten.toys); // ["mouse", "yarn"]
console.log(cat.toys); // ["mouse", "yarn"] <-- problem!
複製程式碼
... 擴充套件物件

利用 ... 來擴充物件,就是將新屬性新增到使用 Spread Operator 建立的物件上

const cat = { legs: 4 };
const dog = {
    ...cat,
    sound: "woof"
};
console.log(cat); // { legs: 4 }
console.log(dog); // { legs: 4, sound: "woof" }
複製程式碼

同樣,可以看到 cat 物件未被更改,但新 dog 物件具有來自 catlegs 屬性以及新 sound 屬性,如果sound 已經存在的話,則會覆蓋。

const cat = { legs: 4, sound: "meow" };
const dog = {
    ...cat,
    sound: "woof"
};
console.log(cat); // { legs: 4, sound: "meow" }
console.log(dog); // { legs: 4, sound: "woof" }
複製程式碼

但是,使用 ... 擴充物件時,要注意行順序,也就是

const cat = { legs: 4, sound: "meow" };
const dog = {
    sound: "woof",
    ...cat
};
console.log(cat); // { legs: 4, sound: "meow" }
console.log(dog); // { legs: 4, sound: "meow" }
複製程式碼

上述 ...catsound: "woof" 改寫為 sound: "meow"

...Object.assign 的區別

在上述利用 ... 處理物件的過程中,會發現 ... 有些時候與 Object.assgin 的操作近乎與等價的,那麼他們具體的區別是什麼。

...Object.assign() 整體的用法非常下關係,主要區別在於 Object.assign() 函式會觸發 setters,而 ... 語法則不會,也就是說 ... 是定義了新屬性,而 Object.assign() 則是設定了它們。

Object.assign() 的基本用法

  • 改變原有物件

    Object.assign(target, source1, source2);
    複製程式碼

    target 已經被修改,source1 以及 source2 會被複制到其中。

  • 建立新的物件

    const result = Object.assign({}, source1, source2);
    複製程式碼

    result 是一個新的物件,source1 以及 source2 被複制到其中。

在第二種方法上,...Object.assign() 是非常類似的。接下來,闡述它們之間具體的相似點和不同點。

Object.assign... 的相同點

  • ...Object.assign() 都是通過 get 運算子來取值

    在將它們寫入目標之前,這兩個操作都會使用 get 操作從源物件讀取相應的屬性值。因此,在此過程中,getter 將轉換為正常的資料屬性,具體如下

    const original = {
        get foo() {
            console.log('getter');
            return 123;
        }
    };
    複製程式碼

    original 物件有 getter foo,而 setterundeined

    Object.getOwnPropertyDescriptor(original, 'foo')
    /* log
    { 
        get: [Function: foo],
      	set: undefined,
      	enumerable: true,
      	configurable: true 
    }
    */
    複製程式碼

    但是,利用 Object.assgin() 以及 ...original 物件進行克隆時,會發現

    const clone1 = {...original};
    // 觸發 original 的 getter 會 log "getter"
    Object.getOwnPropertyDescriptor(clone1, 'foo');
    
    /* log getter 以及被轉換為正常的資料屬性
    { 
    	value: 123,
      	writable: true,
      	enumerable: true,
      	configurable: true 
    }
    */
    
    const clone2 = Object.assign({}, original);
    // 觸發 original 的 getter 會 log "getter"
    Object.getOwnPropertyDescriptor(clone2, 'foo')
    { 
    	value: 123,
      	writable: true,
      	enumerable: true,
      	configurable: true 
    }
    複製程式碼

    上述結果表明,在得到的 clone1clone2中,foo 只是一個普通的資料屬性(它的屬性描述符具有屬性值和可寫);

  • ...Object.assign 只會處理可列舉資料

    這兩個操作都會忽略所有繼承的屬性和所有不可列舉的屬性。

    const proto = {
        inheritedEnumerable: 1,
    };
    const obj = Object.create(proto, {
        ownEnumerable: {
            value: 2,
            enumerable: true,
        },
        ownNonEnumerable: {
            value: 3,
            enumerable: false,
        },
    });
    console.log(obj);
    // { ownEnumerable: 2, ownNonEnumerable: 3, __proto__: { inheritedEnumerable: 1 } }
    console.log({ ...obj });
    // { ownEnumerable: 2 }
    console.log(Object.assign({}, obj));
    // { ownEnumerable: 2 }
    複製程式碼

Object.assign... 的不同點

它們的不同點在於 ... 會定義屬性,而 Object.assign() 會設定它們,也就是說 ... 定義了目標物件中的新屬性,Object.assign() 則是使用 set 操作符進行寫入。

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

console.log(Object.assign({}, obj));
// 會觸發 set, log SET 123
// log {}
console.log({ ...obj });
// log { foo: 123 }
複製程式碼
參考

相關文章