深拷貝能夠說是前端面試中很是高頻的問題,也是一道基礎題。所謂的基礎不是說深拷貝自己是一個很是簡單、很是基礎的問題,而是面試官要經過深拷貝來考察候選人的JavaScript基礎,甚至是程序設計能力。前端
第一個問題,也是最淺顯的問題,爲何 JavaScript 中須要深拷貝?或者說若是不使用深拷貝複製對象會帶來哪些問題?面試
咱們知道在 JavaScript 中存在「引用類型「和「值類型「的概念。由於「引用類型「的特殊性,致使咱們複製對象不能經過簡單的clone = target
,因此須要把原對象的屬性值一一賦給新對象。數組
而對象的屬性其值也多是另外一個對象,因此咱們須要遞歸。spa
經過for...in
可以遍歷對象上的屬性;也能夠經過Object.keys(target)
獲取到對象上的屬性數組後再進行遍歷。
這裏選用for...in
由於相比Object.keys(target)
它還會遍歷對象原型鏈上的屬性。prototype
ES6 Symbol 類型也能夠做爲對象的 key ,如何獲取它們?設計
可使用typeof
判斷目標是否爲引用類型,這裏有一處須要注意:typeof null
也是object
:code
function deepClone(target) { const targetType = typeof target; if (targetType === 'object' || targetType === 'function') { let clone = Array.isArray(target)?[]:{} for (const key in target) { clone[key] = deepClone(target[key]) } return clone; } return target; }
上述代碼就完成了一個很是基礎的深拷貝。可是對於引用類型的處理,它仍然是不完善的:對象
它無法處理Date或者正則這樣的對象。爲何?blog
獲取一個對象具體類型有哪些方式?教程
經常使用的方式有target.constructor.name
、Object.prototype.toString.call(target)
和instanceOf
。
instacneOf
能夠用來判斷對象類型,可是Date
的實例同時也是Object
的實例,此處用於判斷是不許確的;target.constructor.name
獲得的是構造器名稱,而構造器是能夠被修改的;Object.prototype.toString.call(target)
返回的是類名,而在ES5
中只有內置類型對象纔有類名。因此此處咱們最合適的選擇是Object.prototype.toString.call(target)
。
Object.prototype.toString.call(target)
也存在一些問題,你知道嗎?
稍微改進一下代碼,作一些簡單的類型判斷:
function deepClone(target) { const targetType = typeof target; if (targetType === 'object' || targetType === 'function') { let clone = Array.isArray(target)?[]:{}; if(Object.prototype.toString.call(target) === '[object Date]'){ clone = new Date(target) } if(Object.prototype.toString.call(target) === '[object Object]' ||Object.prototype.toString.call(target) === '[object Array]'){ for (const key in target) { clone[key] = deepClone(target[key]) } } return clone; } return target; }
怎麼可以更優雅的作類型判斷?
假如目標對象的屬性間接或直接的引用了自身,就會造成循環引用,致使在遞歸的時候爆棧。
因此咱們的代碼須要循環檢測,設置一個Map
用於存儲已拷貝過的對象,當檢測到對象已存在於Map
中時,取出該值並返回便可避免爆棧。
function deepClone(target, map = new Map()) { const targetType = typeof target; if (targetType === 'object' || targetType === 'function') { let clone = Array.isArray(target)?[]:{}; if (map.get(target)) { return map.get(target); } map.set(target, clone); if(Object.prototype.toString.call(target) === '[object Date]'){ clone = new Date(target) } if(Object.prototype.toString.call(target) === '[object Object]' ||Object.prototype.toString.call(target) === '[object Array]'){ for (const key in target) { clone[key] = deepClone(target[key],map) } } return clone; } return target; }
好多教程使用 WeakMap 作存儲,相比Map,WeakMap好在哪兒?
以上咱們就完成了一個基礎的深拷貝。可是它僅僅是及格而已,想要作到優秀,還要處理一下以前留下的幾個問題。
ES6Symbol
類型也能夠做爲對象的 key ,可是for...in
和Object.keys(target)
都拿不到 Symbol
類型的屬性名。
好在咱們能夠經過Object.getOwnPropertySymbols(target)
獲取對象上全部的Symbol
屬性,再結合for...in
、Object.keys()
就可以拿到所有的 key。不過這種方式有些麻煩,有沒有更好用的方法?
有!Reflect.ownKeys(target)
正是這樣一個集優雅與強大與一身的方法。可是正如同人無完人,這個方法也不完美:顧名思義,ownKeys
是拿不到原型鏈上的屬性的。因此須要結合具體場景來組合使用上述方法。
Date
、Error
等特殊的內置類型雖然是對象,可是並不能遍歷屬性,因此針對這些類型須要從新調用對應的構造器進行初始化。JavaScript 內置了許多相似的特殊類型,然而咱們並非無情的 API 機器,面試中可以回答上述要點也就足夠了。
上述內置類型咱們均可以經過Object.prototype.toString.call(target)
的方式拿到,因此這裏能夠封裝一個類型判斷的方法用於判斷target
是否可以繼續遍歷,以便於及後續的處理。
然而 ES6 新增了Symbol.toStringTag
方法,能夠用來自定義類名,這就致使 Object.prototype.toString.call(target)
拿到的類型名也可能不夠準確:
class ValidatorClass { get [Symbol.toStringTag]() { return "Validator"; } } Object.prototype.toString.call(new ValidatorClass()); // "[object Validator]"
原生的WeakMap
持有的是每一個鍵對象的「弱引用」,這意味着在沒有其餘引用存在時垃圾回收能正確進行。若是 target 很是龐大,那麼使用Map
後若是沒有進行手動釋放,這塊內存就會持續的被佔用。而WeakMap
則不須要擔憂這個問題。
若是上面幾個問題都獲得了妥善的處理,那麼這樣的深拷貝就能夠說是一個足夠打動面試官的深拷貝了。固然這個深拷貝還不夠優秀,有不少待完善的地方,相信善於思考的你已經有了本身的思路。
但本文的重點並不僅僅是實現一個深拷貝,更多的是但願它可以幫助你更好的理解面試官的思路,從而更好的發揮自身的能力。
關注「JS漫步指南」公衆號,獲取更多面試祕籍!