摘要:木易楊注意原始類型被包裝為對(duì)象木易楊原始類型會(huì)被包裝,和會(huì)被忽略。木易楊原因在于時(shí),其屬性描述符為不可寫,即。木易楊解決方法也很簡(jiǎn)單,使用我們?cè)谶M(jìn)階期中介紹的就可以了,使用如下。
引言
上篇文章介紹了賦值、淺拷貝和深拷貝,其中介紹了很多賦值和淺拷貝的相關(guān)知識(shí)以及兩者區(qū)別,限于篇幅只介紹了一種常用深拷貝方案。
本篇文章會(huì)先介紹淺拷貝 Object.assign 的實(shí)現(xiàn)原理,然后帶你手動(dòng)實(shí)現(xiàn)一個(gè)淺拷貝,并在文末留下一道面試題,期待你的評(píng)論。
淺拷貝 Object.assign上篇文章介紹了其定義和使用,主要是將所有可枚舉屬性的值從一個(gè)或多個(gè)源對(duì)象復(fù)制到目標(biāo)對(duì)象,同時(shí)返回目標(biāo)對(duì)象。(來自 MDN)
語法如下所示:
Object.assign(target, ...sources)
其中 target 是目標(biāo)對(duì)象,sources 是源對(duì)象,可以有多個(gè),返回修改后的目標(biāo)對(duì)象 target。
如果目標(biāo)對(duì)象中的屬性具有相同的鍵,則屬性將被源對(duì)象中的屬性覆蓋。后來的源對(duì)象的屬性將類似地覆蓋早先的屬性。
示例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 把源對(duì)象 b 的值復(fù)制到目標(biāo)對(duì)象 a 中,這里把返回值定義為對(duì)象 c,可以看出 b 會(huì)替換掉 a 中具有相同鍵的值,即如果目標(biāo)對(duì)象(a)中的屬性具有相同的鍵,則屬性將被源對(duì)象(b)中的屬性覆蓋。這里需要注意下,返回對(duì)象 c 就是 目標(biāo)對(duì)象 a。
2、在第二步中,修改源對(duì)象 b 的基本類型值(name)和引用類型值(book)。
3、在第三步中,淺拷貝之后目標(biāo)對(duì)象 a 的基本類型值沒有改變,但是引用類型值發(fā)生了改變,因?yàn)?Object.assign() 拷貝的是屬性值。假如源對(duì)象的屬性值是一個(gè)指向?qū)ο蟮囊茫?strong>只拷貝那個(gè)引用地址。
示例2String 類型和 Symbol 類型的屬性都會(huì)被拷貝,而且不會(huì)跳過那些值為 null 或 undefined 的源對(duì)象。
// 木易楊 // 第一步 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); // trueObject.assign 模擬實(shí)現(xiàn)
實(shí)現(xiàn)一個(gè) Object.assign 大致思路如下:
1、判斷原生 Object 是否支持該函數(shù),如果不存在的話創(chuàng)建一個(gè)函數(shù) assign,并使用 Object.defineProperty 將該函數(shù)綁定到 Object 上。
2、判斷參數(shù)是否正確(目標(biāo)對(duì)象不能為空,我們可以直接設(shè)置{}傳遞進(jìn)去,但必須設(shè)置值)。
3、使用 Object() 轉(zhuǎn)成對(duì)象,并保存為 to,最后返回這個(gè)對(duì)象 to。
4、使用 for..in 循環(huán)遍歷出所有可枚舉的自有屬性。并復(fù)制給新的目標(biāo)對(duì)象(使用 hasOwnProperty 獲取自有屬性,即非原型鏈上的屬性)。
實(shí)現(xiàn)代碼如下,這里為了驗(yàn)證方便,使用 assign2 代替 assign。注意此模擬實(shí)現(xiàn)不支持 symbol 屬性,因?yàn)?b>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 }); }
測(cè)試一下
// 木易楊 // 測(cè)試用例 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
針對(duì)上面的代碼做如下擴(kuò)展。
注意1:可枚舉性原生情況下掛載在 Object 上的屬性是不可枚舉的,但是直接在 Object 上掛載屬性 a 之后是可枚舉的,所以這里必須使用 Object.defineProperty,并設(shè)置 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(..) 會(huì)檢查給定的屬性名是否直接存在于對(duì)象中(而不是在原型鏈上)并且滿足 enumerable: true。具體用法如下:
// 木易楊 // 方法1 Object.getOwnPropertyDescriptor(Object, "assign"); // { // value: ?, // writable: true, // 可寫 // enumerable: false, // 不可枚舉,注意這里是 false // configurable: true // 可配置 // } // 方法2 Object.propertyIsEnumerable("assign"); // false
上面代碼說明 Object.assign 是不可枚舉的。
介紹這么多是因?yàn)橹苯釉?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
所以要實(shí)現(xiàn) Object.assign 必須使用 Object.defineProperty,并設(shè)置 writable: true, enumerable: false, configurable: true,當(dāng)然默認(rèn)情況下不設(shè)置就是 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 // }
所以具體到本次模擬實(shí)現(xiàn)中,相關(guān)代碼如下。
// 木易楊 // 判斷原生 Object 中是否存在函數(shù) assign2 if (typeof Object.assign2 != "function") { // 使用屬性描述符定義新屬性 assign2 Object.defineProperty(Object, "assign2", { value: function (target) { ... }, // 默認(rèn)值是 false,即 enumerable: false writable: true, configurable: true }); }注意2:判斷參數(shù)是否正確
有些文章判斷參數(shù)是否正確是這樣的。
// 木易楊 if (target === undefined || target === null) { throw new TypeError("Cannot convert undefined or null to object"); }
這樣肯定沒問題,但是這樣寫沒有必要,因?yàn)?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:原始類型被包裝為對(duì)象
// 木易楊 var v1 = "abc"; var v2 = true; var v3 = 10; var v4 = Symbol("foo"); var obj = Object.assign({}, v1, null, v2, undefined, v3, v4); // 原始類型會(huì)被包裝,null 和 undefined 會(huì)被忽略。 // 注意,只有字符串的包裝對(duì)象才可能有自身可枚舉屬性。 console.log(obj); // { "0": "a", "1": "b", "2": "c" }
上面代碼中的源對(duì)象 v2、v3、v4 實(shí)際上被忽略了,原因在于他們自身沒有可枚舉屬性。
// 木易楊 var v1 = "abc"; var v2 = true; var v3 = 10; var v4 = Symbol("foo"); var v5 = null; // Object.keys(..) 返回一個(gè)數(shù)組,包含所有可枚舉屬性 // 只會(huì)查找對(duì)象直接包含的屬性,不查找[[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(..) 返回一個(gè)數(shù)組,包含所有屬性,無論它們是否可枚舉 // 只會(huì)查找對(duì)象直接包含的屬性,不查找[[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
但是下面的代碼是可以執(zhí)行的。
// 木易楊 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 // }
原因很簡(jiǎn)單,因?yàn)榇藭r(shí) undefined、true 等不是作為對(duì)象,而是作為對(duì)象 b 的屬性值,對(duì)象 b 是可枚舉的。
// 木易楊 // 接上面的代碼 Object.keys( b ); // [ "v1", "v2", "v3", "v4", "v5", "v6" ]
這里其實(shí)又可以看出一個(gè)問題來,那就是目標(biāo)對(duì)象是原始類型,會(huì)包裝成對(duì)象,對(duì)應(yīng)上面的代碼就是目標(biāo)對(duì)象 a 會(huì)被包裝成 [String: "abc"],那模擬實(shí)現(xiàn)時(shí)應(yīng)該如何處理呢?很簡(jiǎn)單,使用 Object(..) 就可以了。
// 木易楊 var a = "abc"; console.log( Object(a) ); // [String: "abc"]
到這里已經(jīng)介紹很多知識(shí)了,讓我們?cè)賮硌由煲幌拢纯聪旅娴拇a能不能執(zhí)行。
// 木易楊 var a = "abc"; var b = "def"; Object.assign(a, b);
答案是否定的,會(huì)提示以下錯(cuò)誤。
// 木易楊 TypeError: Cannot assign to read only property "0" of object "[object String]"
原因在于 Object("abc") 時(shí),其屬性描述符為不可寫,即 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 // }
同理,下面的代碼也會(huì)報(bào)錯(cuò)。
// 木易楊 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 就會(huì)報(bào)錯(cuò),看下面的代碼。
// 木易楊 var myObject = Object("abc"); Object.getOwnPropertyDescriptor(myObject, "0"); // { // value: "a", // writable: false, // 注意這里 // enumerable: true, // configurable: false // } myObject[0] = "d"; // "d" myObject[0]; // "a"
這里并沒有報(bào)錯(cuò),原因在于 JS 對(duì)于不可寫的屬性值的修改靜默失敗(silently failed),在嚴(yán)格模式下才會(huì)提示錯(cuò)誤。
// 木易楊 "use strict" var myObject = Object("abc"); myObject[0] = "d"; // TypeError: Cannot assign to read only property "0" of object "[object String]"
所以我們?cè)谀M實(shí)現(xiàn) Object.assign 時(shí)需要使用嚴(yán)格模式。
注意4:存在性如何在不訪問屬性值的情況下判斷對(duì)象中是否存在某個(gè)屬性呢,看下面的代碼。
// 木易楊 var anotherObject = { a: 1 }; // 創(chuàng)建一個(gè)關(guān)聯(lián)到 anotherObject 的對(duì)象 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 方法,區(qū)別如下(你不知道的JS上卷 P119):
1、in 操作符會(huì)檢查屬性是否在對(duì)象及其 [[Prototype]] 原型鏈中。
2、hasOwnProperty(..) 只會(huì)檢查屬性是否在 myObject 對(duì)象中,不會(huì)檢查 [[Prototype]] 原型鏈。
Object.assign 方法肯定不會(huì)拷貝原型鏈上的屬性,所以模擬實(shí)現(xiàn)時(shí)需要用 hasOwnProperty(..) 判斷處理下,但是直接使用 myObject.hasOwnProperty(..) 是有問題的,因?yàn)橛械膶?duì)象可能沒有連接到 Object.prototype 上(比如通過 Object.create(null) 來創(chuàng)建),這種情況下,使用 myObject.hasOwnProperty(..) 就會(huì)失敗。
// 木易楊 var myObject = Object.create( null ); myObject.b = 2; ("b" in myObject); // true myObject.hasOwnProperty( "b" ); // TypeError: myObject.hasOwnProperty is not a function
解決方法也很簡(jiǎn)單,使用我們?cè)凇具M(jìn)階3-3期】中介紹的 call 就可以了,使用如下。
// 木易楊 var myObject = Object.create( null ); myObject.b = 2; Object.prototype.hasOwnProperty.call(myObject, "b"); // true
所以具體到本次模擬實(shí)現(xiàn)中,相關(guān)代碼如下。
// 木易楊 // 使用 for..in 遍歷對(duì)象 nextSource 獲取屬性值 // 此處會(huì)同時(shí)檢查其原型鏈上的屬性 for (var nextKey in nextSource) { // 使用 hasOwnProperty 判斷對(duì)象 nextSource 中是否存在屬性 nextKey // 過濾其原型鏈上的屬性 if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) { // 賦值給對(duì)象 to,并在遍歷結(jié)束后返回對(duì)象 to to[nextKey] = nextSource[nextKey]; } }本期思考題
如何實(shí)現(xiàn)一個(gè)深拷貝?參考
MDN 之 Object.assign()進(jìn)階系列目錄ES2015系列(二) 理解 Object.assign
【進(jìn)階1期】 調(diào)用堆棧
【進(jìn)階2期】 作用域閉包
【進(jìn)階3期】 this全面解析
【進(jìn)階4期】 深淺拷貝原理
【進(jìn)階5期】 原型Prototype
【進(jìn)階6期】 高階函數(shù)
【進(jìn)階7期】 事件機(jī)制
【進(jìn)階8期】 Event Loop原理
【進(jìn)階9期】 Promise原理
【進(jìn)階10期】Async/Await原理
【進(jìn)階11期】防抖/節(jié)流原理
【進(jìn)階12期】模塊化詳解
【進(jìn)階13期】ES6重難點(diǎn)
【進(jìn)階14期】計(jì)算機(jī)網(wǎng)絡(luò)概述
【進(jìn)階15期】瀏覽器渲染原理
【進(jìn)階16期】webpack配置
【進(jìn)階17期】webpack原理
【進(jìn)階18期】前端監(jiān)控
【進(jìn)階19期】跨域和安全
【進(jìn)階20期】性能優(yōu)化
【進(jìn)階21期】VirtualDom原理
【進(jìn)階22期】Diff算法
【進(jìn)階23期】MVVM雙向綁定
【進(jìn)階24期】Vuex原理
【進(jìn)階25期】Redux原理
【進(jìn)階26期】路由原理
【進(jìn)階27期】VueRouter源碼解析
【進(jìn)階28期】ReactRouter源碼解析
交流進(jìn)階系列文章匯總?cè)缦拢瑑?nèi)有優(yōu)質(zhì)前端資料,覺得不錯(cuò)點(diǎn)個(gè)star。
https://github.com/yygmind/blog
我是木易楊,網(wǎng)易高級(jí)前端工程師,跟著我每周重點(diǎn)攻克一個(gè)前端面試重難點(diǎn)。接下來讓我?guī)阕哌M(jìn)高級(jí)前端的世界,在進(jìn)階的路上,共勉!
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://specialneedsforspecialkids.com/yun/104636.html
摘要:展開語法木易楊通過代碼可以看出實(shí)際效果和是一樣的。木易楊可以看出,改變之后的值并沒有發(fā)生變化,但改變之后,相應(yīng)的的值也發(fā)生變化。深拷貝使用場(chǎng)景木易楊完全改變變量之后對(duì)沒有任何影響,這就是深拷貝的魔力。木易楊情況下,轉(zhuǎn)換結(jié)果不正確。 一、賦值(Copy) 賦值是將某一數(shù)值或?qū)ο筚x給某個(gè)變量的過程,分為下面 2 部分 基本數(shù)據(jù)類型:賦值,賦值之后兩個(gè)變量互不影響 引用數(shù)據(jù)類型:賦址,兩個(gè)...
摘要:今天這篇文章我們來看看一道必會(huì)面試題,即如何實(shí)現(xiàn)一個(gè)深拷貝。木易楊注意這里使用上面測(cè)試用例測(cè)試一下一個(gè)簡(jiǎn)單的深拷貝就完成了,但是這個(gè)實(shí)現(xiàn)還存在很多問題。 引言 上篇文章詳細(xì)介紹了淺拷貝 Object.assign,并對(duì)其進(jìn)行了模擬實(shí)現(xiàn),在實(shí)現(xiàn)的過程中,介紹了很多基礎(chǔ)知識(shí)。今天這篇文章我們來看看一道必會(huì)面試題,即如何實(shí)現(xiàn)一個(gè)深拷貝。本文會(huì)詳細(xì)介紹對(duì)象、數(shù)組、循環(huán)引用、引用丟失、Symbo...
摘要:返回的綁定函數(shù)也能使用操作符創(chuàng)建對(duì)象這種行為就像把原函數(shù)當(dāng)成構(gòu)造器,提供的值被忽略,同時(shí)調(diào)用時(shí)的參數(shù)被提供給模擬函數(shù)。 bind() bind() 方法會(huì)創(chuàng)建一個(gè)新函數(shù),當(dāng)這個(gè)新函數(shù)被調(diào)用時(shí),它的 this 值是傳遞給 bind() 的第一個(gè)參數(shù),傳入bind方法的第二個(gè)以及以后的參數(shù)加上綁定函數(shù)運(yùn)行時(shí)本身的參數(shù)按照順序作為原函數(shù)的參數(shù)來調(diào)用原函數(shù)。bind返回的綁定函數(shù)也能使用 n...
摘要:首次運(yùn)行代碼時(shí),會(huì)創(chuàng)建一個(gè)全局執(zhí)行上下文并到當(dāng)前的執(zhí)行棧中。執(zhí)行上下文的創(chuàng)建執(zhí)行上下文分兩個(gè)階段創(chuàng)建創(chuàng)建階段執(zhí)行階段創(chuàng)建階段確定的值,也被稱為。 (關(guān)注福利,關(guān)注本公眾號(hào)回復(fù)[資料]領(lǐng)取優(yōu)質(zhì)前端視頻,包括Vue、React、Node源碼和實(shí)戰(zhàn)、面試指導(dǎo)) 本周正式開始前端進(jìn)階的第一期,本周的主題是調(diào)用堆棧,,今天是第一天 本計(jì)劃一共28期,每期重點(diǎn)攻克一個(gè)面試重難點(diǎn),如果你還不了解本進(jìn)...
摘要:本期推薦文章從作用域鏈談閉包,由于微信不能訪問外鏈,點(diǎn)擊閱讀原文就可以啦。推薦理由這是一篇譯文,深入淺出圖解作用域鏈,一步步深入介紹閉包。作用域鏈的頂端是全局對(duì)象,在全局環(huán)境中定義的變量就會(huì)綁定到全局對(duì)象中。 (關(guān)注福利,關(guān)注本公眾號(hào)回復(fù)[資料]領(lǐng)取優(yōu)質(zhì)前端視頻,包括Vue、React、Node源碼和實(shí)戰(zhàn)、面試指導(dǎo)) 本周開始前端進(jìn)階的第二期,本周的主題是作用域閉包,今天是第6天。 本...
閱讀 1688·2021-10-13 09:39
閱讀 3159·2021-10-12 10:11
閱讀 554·2021-09-28 09:36
閱讀 2637·2019-08-30 15:55
閱讀 1389·2019-08-30 13:04
閱讀 630·2019-08-29 17:08
閱讀 1908·2019-08-29 14:14
閱讀 3403·2019-08-28 18:23