淺拷貝 Object.assign
上篇文章介紹了其定義和使用,主要是將所有可枚舉屬性的值從一個或多個源對象復制到目標對象,同時返回目標對象。(來自 MDN)
語法如下所示:
Object.assign(target, ...sources)
其中 target
是目標對象,sources
是源對象,可以有多個,返回修改后的目標對象 target
。
如果目標對象中的屬性具有相同的鍵,則屬性將被源對象中的屬性覆蓋。后來的源對象的屬性將類似地覆蓋早先的屬性。
示例1
我們知道淺拷貝就是拷貝第一層的基本類型值,以及第一層的引用類型地址。
// 第一步
let a = {
name: "advanced",
age: 18
}
let b = {
name: "muyiy",
book: {
title: "You Don't Know JS",
price: "45"
}
}
let c = Object.assign(a, b);
console.log(c);
// {
// name: "muyiy",
// age: 18,
// book: {title: "You Don't Know JS", price: "45"}
// }
console.log(a === c);
// true
// 第二步
b.name = "change";
b.book.price = "55";
console.log(b);
// {
// name: "change",
// book: {title: "You Don't Know JS", price: "55"}
// }
// 第三步
console.log(a);
// {
// name: "muyiy",
// age: 18,
// book: {title: "You Don't Know JS", price: "55"}
// }
1、在第一步中,使用 Object.assign
把源對象 b 的值復制到目標對象 a 中,這里把返回值定義為對象 c,可以看出 b 會替換掉 a 中具有相同鍵的值,即如果目標對象(a)中的屬性具有相同的鍵,則屬性將被源對象(b)中的屬性覆蓋。這里需要注意下,返回對象 c 就是 目標對象 a。
2、在第二步中,修改源對象 b 的基本類型值(name)和引用類型值(book)。
3、在第三步中,淺拷貝之后目標對象 a 的基本類型值沒有改變,但是引用類型值發生了改變,因為 Object.assign()
拷貝的是屬性值。假如源對象的屬性值是一個指向對象的引用,它也只拷貝那個引用地址。
示例2
String
類型和 Symbol
類型的屬性都會被拷貝,而且不會跳過那些值為 null
或 undefined
的源對象。
// 第一步
let a = {
name: "muyiy",
age: 18
}
let b = {
b1: Symbol("muyiy"),
b2: null,
b3: undefined
}
let c = Object.assign(a, b);
console.log(c);
// {
// name: "muyiy",
// age: 18,
// b1: Symbol(muyiy),
// b2: null,
// b3: undefined
// }
console.log(a === c);
// true
Object.assign 模擬實現
實現一個 Object.assign
大致思路如下:
1、判斷原生 Object
是否支持該函數,如果不存在的話創建一個函數assign
,並使用 Object.defineProperty
將該函數綁定到 Object
上。
2、判斷參數是否正確(目標對象不能為空,我們可以直接設置{}傳遞進去,但必須設置值)
3、使用 Object()
轉成對象,並保存為 to,最后返回這個對象 to
4、使用 for..in
循環遍歷出所有可枚舉的自有屬性。並復制給新的目標對象(hasOwnProperty返回非原型鏈上的屬性)
實現代碼如下,這里為了驗證方便,使用 assign2
代替 assign
。注意此模擬實現不支持 symbol
屬性,因為ES5
中根本沒有 symbol
。
if (typeof Object.assign2 != 'function') {
// Attention 1
Object.defineProperty(Object, "assign2", {
value: function (target) {
'use strict';
if (target == null) { // Attention 2
throw new TypeError('Cannot convert undefined or null to object');
}
// Attention 3
var to = Object(target);
for (var index = 1; index < arguments.length; index++) {
var nextSource = arguments[index];
if (nextSource != null) { // Attention 2
// Attention 4
for (var nextKey in nextSource) {
if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
to[nextKey] = nextSource[nextKey];
}
}
}
}
return to;
},
writable: true,
configurable: true
});
}
測試一下
// 測試用例
let a = {
name: "advanced",
age: 18
}
let b = {
name: "muyiy",
book: {
title: "You Don't Know JS",
price: "45"
}
}
let c = Object.assign2(a, b);
console.log(c);
// {
// name: "muyiy",
// age: 18,
// book: {title: "You Don't Know JS", price: "45"}
// }
console.log(a === c);
// true
針對上面的代碼做如下擴展。
注意1:可枚舉性
原生情況下掛載在 Object
上的屬性是不可枚舉的,但是直接在Object
上掛載屬性 a
之后是可枚舉的,所以這里必須使用Object.defineProperty
,並設置 enumerable: false
以及writable: true, configurable: true
。
for(var i in Object) {
console.log(Object[i]);
}
// 無輸出
Object.keys( Object );
// []
上面代碼說明原生 Object
上的屬性不可枚舉。
我們可以使用 2 種方法查看 Object.assign
是否可枚舉,使用Object.getOwnPropertyDescriptor
或者Object.propertyIsEnumerable
都可以,其中propertyIsEnumerable(..)
會檢查給定的屬性名是否直接存在於對象中(而不是在原型鏈上)並且滿足 enumerable: true
。具體用法如下:
// 方法1
Object.getOwnPropertyDescriptor(Object, "assign");
// {
// value: ƒ,
// writable: true, // 可寫
// enumerable: false, // 不可枚舉,注意這里是 false
// configurable: true // 可配置
// }
// 方法2
Object.propertyIsEnumerable("assign");
// false
上面代碼說明 Object.assign
是不可枚舉的。
介紹這么多是因為直接在 Object
上掛載屬性 a
之后是可枚舉的,我們來看如下代碼。
Object.a = function () {
console.log("log a");
}
Object.getOwnPropertyDescriptor(Object, "a");
// {
// value: ƒ,
// writable: true,
// enumerable: true, // 注意這里是 true
// configurable: true
// }
Object.propertyIsEnumerable("a");
// true
所以要實現 Object.assign
必須使用 Object.defineProperty
,並設置 writable: true, enumerable: false, configurable: true
,當然默認情況下不設置就是 false
。
Object.defineProperty(Object, "b", {
value: function() {
console.log("log b");
}
});
Object.getOwnPropertyDescriptor(Object, "b");
// {
// value: ƒ,
// writable: false, // 注意這里是 false
// enumerable: false, // 注意這里是 false
// configurable: false // 注意這里是 false
// }
所以具體到本次模擬實現中,相關代碼如下。
// 判斷原生 Object 中是否存在函數 assign2
if (typeof Object.assign2 != 'function') {
// 使用屬性描述符定義新屬性 assign2
Object.defineProperty(Object, "assign2", {
value: function (target) {
...
},
// 默認值是 false,即 enumerable: false
writable: true,
configurable: true
});
}
注意2:判斷參數是否正確
有些文章判斷參數是否正確是這樣的
if (target === undefined || target === null) {
throw new TypeError('Cannot convert undefined or null to object');
}
這樣肯定沒問題,但是這樣寫沒有必要,因為 undefined
和 null
是相等的(高程 3 P52 ),即 undefined == null
返回 true
,只需要按照如下方式判斷就好了。
if (target == null) { // TypeError if undefined or null
throw new TypeError('Cannot convert undefined or null to object');
}
注意3:原始類型被包裝為對象
var v1 = "abc";
var v2 = true;
var v3 = 10;
var v4 = Symbol("foo");
var obj = Object.assign({}, v1, null, v2, undefined, v3, v4);
// 原始類型會被包裝,null 和 undefined 會被忽略。
// 注意,只有字符串的包裝對象才可能有自身可枚舉屬性。
console.log(obj);
// { "0": "a", "1": "b", "2": "c" }
上面代碼中的源對象 v2、v3、v4 實際上被忽略了,原因在於他們自身沒有可枚舉屬性。
var v1 = "abc";
var v2 = true;
var v3 = 10;
var v4 = Symbol("foo");
var v5 = null;
// Object.keys(..) 返回一個數組,包含所有可枚舉屬性
// 只會查找對象直接包含的屬性,不查找[[Prototype]]鏈
Object.keys( v1 ); // [ '0', '1', '2' ]
Object.keys( v2 ); // []
Object.keys( v3 ); // []
Object.keys( v4 ); // []
Object.keys( v5 );
// TypeError: Cannot convert undefined or null to object
// Object.getOwnPropertyNames(..) 返回一個數組,包含所有屬性,無論它們是否可枚舉
// 只會查找對象直接包含的屬性,不查找[[Prototype]]鏈
Object.getOwnPropertyNames( v1 ); // [ '0', '1', '2', 'length' ]
Object.getOwnPropertyNames( v2 ); // []
Object.getOwnPropertyNames( v3 ); // []
Object.getOwnPropertyNames( v4 ); // []
Object.getOwnPropertyNames( v5 );
// TypeError: Cannot convert undefined or null to object
但是下面的代碼是可以執行的。
var a = "abc";
var b = {
v1: "def",
v2: true,
v3: 10,
v4: Symbol("foo"),
v5: null,
v6: undefined
}
var obj = Object.assign(a, b);
console.log(obj);
// {
// [String: 'abc']
// v1: 'def',
// v2: true,
// v3: 10,
// v4: Symbol(foo),
// v5: null,
// v6: undefined
// }
原因很簡單,因為此時 undefined
、true
等不是作為對象,而是作為對象 b 的屬性值,對象 b 是可枚舉的。
// 接上面的代碼
Object.keys( b ); // [ 'v1', 'v2', 'v3', 'v4', 'v5', 'v6' ]
這里其實又可以看出一個問題來,那就是目標對象是原始類型,會包裝成對象,對應上面的代碼就是目標對象 a 會被包裝成 [String: 'abc']
,那模擬實現時應該如何處理呢?很簡單,使用 Object(..)
就可以了。
var a = "abc";
console.log( Object(a) );
// [String: 'abc']
到這里已經介紹很多知識了,讓我們再來延伸一下,看看下面的代碼能不能執行。
var a = "abc";
var b = "def";
Object.assign(a, b);
答案是否定的,會提示以下錯誤。
TypeError: Cannot assign to read only property '0' of object '[object String]'
原因在於 Object("abc")
時,其屬性描述符為不可寫,即 writable: false
。
var myObject = Object( "abc" );
Object.getOwnPropertyNames( myObject );
// [ '0', '1', '2', 'length' ]
Object.getOwnPropertyDescriptor(myObject, "0");
// {
// value: 'a',
// writable: false, // 注意這里
// enumerable: true,
// configurable: false
// }
同理,下面的代碼也會報錯。
var a = "abc";
var b = {
0: "d"
};
Object.assign(a, b);
// TypeError: Cannot assign to read only property '0' of object '[object String]'
注意4:存在性
如何在不訪問屬性值的情況下判斷對象中是否存在某個屬性呢,看下面的代碼。
var anotherObject = {
a: 1
};
// 創建一個關聯到 anotherObject 的對象
var myObject = Object.create( anotherObject );
myObject.b = 2;
("a" in myObject); // true
("b" in myObject); // true
myObject.hasOwnProperty( "a" ); // false
myObject.hasOwnProperty( "b" ); // true
這邊使用了 in
操作符和 hasOwnProperty
方法,區別如下(你不知道的JS上卷 P119):
1、in
操作符會檢查屬性是否在對象及其 [[Prototype]]
原型鏈中。
2、hasOwnProperty(..)
只會檢查屬性是否在 myObject
對象中,不會檢查 [[Prototype]]
原型鏈。
Object.assign
方法肯定不會拷貝原型鏈上的屬性,所以模擬實現時需要用 hasOwnProperty(..)
判斷處理下,但是直接使用myObject.hasOwnProperty(..)
是有問題的,因為有的對象可能沒有連接到 Object.prototype
上(比如通過 Object.create(null)
來創建),這種情況下,使用 myObject.hasOwnProperty(..)
就會失敗。
var myObject = Object.create( null );
myObject.b = 2;
("b" in myObject);
// true
myObject.hasOwnProperty( "b" );
// TypeError: myObject.hasOwnProperty is not a function
解決方法也很簡單,使用我們在【進階3-3期】中介紹的 call
就可以了,使用如下。
var myObject = Object.create( null );
myObject.b = 2;
Object.prototype.hasOwnProperty.call(myObject, "b");
// true
所以具體到本次模擬實現中,相關代碼如下。
// 使用 for..in 遍歷對象 nextSource 獲取屬性值
// 此處會同時檢查其原型鏈上的屬性
for (var nextKey in nextSource) {
// 使用 hasOwnProperty 判斷對象 nextSource 中是否存在屬性 nextKey
// 過濾其原型鏈上的屬性
if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
// 賦值給對象 to,並在遍歷結束后返回對象 to
to[nextKey] = nextSource[nextKey];
}
}