上篇文章介紹了賦值、淺拷貝和深拷貝,其中介紹了不少賦值和淺拷貝的相關知識以及二者區別,限於篇幅只介紹了一種經常使用深拷貝方案。前端
本篇文章會先介紹淺拷貝 Object.assign
的實現原理,而後帶你手動實現一個淺拷貝,並在文末留下一道面試題,期待你的評論。node
Object.assign
上篇文章介紹了其定義和使用,主要是將全部可枚舉屬性的值從一個或多個源對象複製到目標對象,同時返回目標對象。(來自 MDN)webpack
語法以下所示:git
Object.assign(target, ...sources)
其中 target
是目標對象,sources
是源對象,能夠有多個,返回修改後的目標對象 target
。github
若是目標對象中的屬性具備相同的鍵,則屬性將被源對象中的屬性覆蓋。後來的源對象的屬性將相似地覆蓋早先的屬性。web
咱們知道淺拷貝就是拷貝第一層的基本類型值,以及第一層的引用類型地址。面試
// 木易楊 // 第一步 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"} // }
一、在第一步中,使用 Object.assign
把源對象 b 的值複製到目標對象 a 中,這裏把返回值定義爲對象 c,能夠看出 b 會替換掉 a 中具備相同鍵的值,即若是目標對象(a)中的屬性具備相同的鍵,則屬性將被源對象(b)中的屬性覆蓋。這裏須要注意下,返回對象 c 就是 目標對象 a。 算法
二、在第二步中,修改源對象 b 的基本類型值(name)和引用類型值(book)。跨域
三、在第三步中,淺拷貝以後目標對象 a 的基本類型值沒有改變,可是引用類型值發生了改變,由於 Object.assign()
拷貝的是屬性值。假如源對象的屬性值是一個指向對象的引用,它也只拷貝那個引用地址。數組
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
大體思路以下:
一、判斷原生 Object
是否支持該函數,若是不存在的話建立一個函數 assign
,並使用 Object.defineProperty
將該函數綁定到 Object
上。
二、判斷參數是否正確(目標對象不能爲空,咱們能夠直接設置{}傳遞進去,但必須設置值)。
三、使用 Object()
轉成對象,並保存爲 to,最後返回這個對象 to。
四、使用 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
針對上面的代碼作以下擴展。
原生狀況下掛載在 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 }); }
有些文章判斷參數是否正確是這樣的。
// 木易楊 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'); }
// 木易楊 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" }
上面代碼中的源對象 v二、v三、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]'
可是並非說只要 writable: false
就會報錯,看下面的代碼。
// 木易楊 var myObject = Object('abc'); Object.getOwnPropertyDescriptor(myObject, '0'); // { // value: 'a', // writable: false, // 注意這裏 // enumerable: true, // configurable: false // } myObject[0] = 'd'; // 'd' myObject[0]; // 'a'
這裏並無報錯,緣由在於 JS 對於不可寫的屬性值的修改靜默失敗(silently failed),在嚴格模式下才會提示錯誤。
// 木易楊 'use strict' var myObject = Object('abc'); myObject[0] = 'd'; // TypeError: Cannot assign to read only property '0' of object '[object String]'
因此咱們在模擬實現 Object.assign
時須要使用嚴格模式。
如何在不訪問屬性值的狀況下判斷對象中是否存在某個屬性呢,看下面的代碼。
// 木易楊 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):
一、in
操做符會檢查屬性是否在對象及其 [[Prototype]]
原型鏈中。
二、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]; } }
如何實現一個深拷貝?
MDN 之 Object.assign()
進階系列文章彙總以下,內有優質前端資料,以爲不錯點個star。
https://github.com/yygmind/blog
我是木易楊,網易高級前端工程師,跟着我每週重點攻克一個前端面試重難點。接下來讓我帶你走進高級前端的世界,在進階的路上,共勉!