文章首發於掘金, 地址JavaScript系列: 1、手撕JS中的深淺拷貝面試
面試官:如何手寫一個深拷貝?
應聘者: JSON.parse(JSON.stringify(object))
面試官:有什麼優缺點?
應聘者:不能序列化函數, 忽略undefined...
面試官:有其餘方法? 怎麼解決循環引用?
應聘者:for循環 。。。數組
Number、String、Boolean、Null、undefined、Symbol、Bigint函數
Bigint 是最近新引入的基本數據類型post
Object、Array、Function等性能
數據類型不是本文重點, 重點是實現深淺拷貝this
下面是要copy的對象, 以後的代碼都會直接使用$obj, 以後不會再次聲明
// lmran var $obj = { func: function () { console.log('this is function') }, date: new Date(), symbol: Symbol(), a: null, b: undefined, c: { a: 1 }, e: new RegExp('regexp'), f: new Error('error') } $obj.c.d = $obj
一句話能夠說就是:對對象而言,它的第一層屬性值若是是基本數據類型則徹底拷貝一份數據,若是是引用類型就拷貝內存地址。確實拷貝的很淺[偷笑]prototype
Object.assign()
// lmran let obj1 = { name: 'yang', res: { value: 123 } } let obj2 = Object.assign({}, obj1) obj2.res.value = 456 console.log(obj2) // {name: "haha", res: {value: 456}} console.log(obj1) // {name: "haha", res: {value: 456}} obj2.name = 'haha' console.log(obj2) // {name: "haha", res: {value: 456}} console.log(obj1) // {name: "yang", res: {value: 456}}
Spread
// lmran let obj1 = { name: 'yang', res: { value: 123 } } let {...obj2} = obj1 obj2.res.value = 456 console.log(obj2) // {name: "haha", res: {value: 456}} console.log(obj1) // {name: "haha", res: {value: 456}} obj2.name = 'haha' console.log(obj2) // {name: "haha", res: {value: 456}} console.log(obj1) // {name: "yang", res: {value: 456}}
Array.prototype.slice
// lmran const arr1 = [ 'yang', { value: 123 } ]; const arr2 = arr1.slice(0); arr2[1].value = 456; console.log(arr2); // ["yang", {value: 456}] console.log(arr1); // ["yang", {value: 456}] arr2[0] = 'haha'; console.log(arr2); // ["haha", {value: 456}] console.log(arr1); // ["yang", {value: 456}]
Array.prototype.concat
// lmran const arr1 = [ 'yang', { value: 123 } ]; const arr2 = [].concat(arr1); arr2[1].value = 456; console.log(arr2); // ["yang", {value: 456}] console.log(arr1); // ["yang", {value: 456}] arr2[0] = 'haha'; console.log(arr2); // ["haha", {value: 456}] console.log(arr1); // ["yang", {value: 456}]
實際上對於數組來講, 只要不修改原數組, 從新返回一個新數組就能夠實現淺拷貝,好比說map、filter、reduce等方法
深拷貝就是無論是基本數據類型仍是引用數據類型都從新拷貝一份, 不存在共用數據的現象code
JSON.parse(JSON.stringify(object))
// lmran let obj = JSON.parse(JSON.stringify($obj)) console.log(obj) // 不能解決循環引用 /* VM348:1 Uncaught TypeError: Converting circular structure to JSON at JSON.stringify (<anonymous>) at <anonymous>:1:17 */ delete $obj.c.d let obj = JSON.parse(JSON.stringify($obj)) console.log(obj) // 丟失了大部分屬性 /* { a: null c: {a: 1} date: "2020-04-05T09:51:32.610Z" e: {} f: {} } */
存在的問題:regexp
一、會忽略 undefined
對象
二、會忽略 symbol
三、不能序列化函數
四、不能解決循環引用的對象
五、不能正確處理new Date()
六、不能處理正則
七、不能處理new Error()
遞歸遍歷對象屬性
// lmran function deepCopy (obj) { if (obj === null || typeof obj !== 'object') { return obj } let copy = Array.isArray(obj) ? [] : {} Object.keys(obj).forEach(v => { copy[key] = deepCopy(obj[key]) }) return copy } deepCopy($obj) /* VM601:23 Uncaught RangeError: Maximum call stack size exceeded at <anonymous>:23:30 at Array.forEach (<anonymous>) at deepCopy (<anonymous>:23:22) */ delete $obj.c.d deepCopy($obj) /* { a: null b: undefined c: {a: 1} date: {} e: {} f: {} func: ƒ () symbol: Symbol() } */
存在的問題是:
一、不能解決循環引用的對象
二、不能正確處理new Date()
三、不能處理正則
四、不能處理new Error()
先解決解決循環遍歷問題, 解決辦法是將對象,對象屬性存儲在數組中查看下次遍歷時有無已經遍歷過的對象,有則直接返回, 不然繼續遍歷
// lmran function deepCopy (obj, cache = []) { if (obj === null || typeof obj !== 'object') { return obj } const item = cache.filter(item => item.original === obj)[0] if (item) return item.copy let copy = Array.isArray(obj) ? [] : {} cache.push({ original: obj, copy }) Object.keys(obj).forEach(key => { copy[key] = deepCopy(obj[key], cache) }) return copy } deepCopy($obj) /* { a: null b: undefined c: {a: 1, d: {…}} date: {} e: {} f: {} func: ƒ () symbol: Symbol() }
完美解決了循環引用問題, 可是仍然存在幾個小問題,都屬於同一類問題
對於最終的幾個對象的處理,能夠判斷類型, 從新new一個返回就能夠了
// lmran function deepCopy (obj, cache = []) { if (obj === null || typeof obj !== 'object') { return obj } if (Object.prototype.toString.call(obj) === '[object Date]') return new Date(obj) if (Object.prototype.toString.call(obj) === '[object RegExp]') return new RegExp(obj) if (Object.prototype.toString.call(obj) === '[object Error]') return new Error(obj) const item = cache.filter(item => item.original === obj)[0] if (item) return item.copy let copy = Array.isArray(obj) ? [] : {} cache.push({ original: obj, copy }) Object.keys(obj).forEach(key => { copy[key] = deepCopy(obj[key], cache) }) return copy } deepCopy($obj) /* { a: null b: undefined c: {a: 1, d: {…}} date: Fri Apr 10 2020 20:06:08 GMT+0800 (中國標準時間) {} e: /regexp/ f: Error: Error: error at deepCopy (<anonymous>:8:74) at <anonymous>:19:21 at Array.forEach (<anonymous>) at deepCopy (<anonymous>:18:22) at <anonymous>:24:1 func: ƒ () symbol: Symbol() } */
到這裏基本功能彷佛已經實現, 可是還存在一個問題, 就是函數引用了同一個內存地址, 對於這個問題網上大部分都是直接返回或者返回爲對象, 包括lodash也是這麼處理的
const isFunc = typeof value == 'function' if (isFunc || !cloneableTags[tag]) { return object ? value : {} }
那麼如何結局這個問題就須要用eval函數, 雖然說這個函數已經不推薦使用, 可是它仍是能解決問題的。 函數這裏分兩種: 普通函數
和箭頭函數
, 區分這二者只須要看有無prototype
, 有prototype
屬性就屬於普通函數, 沒有就是箭頭函數
// lmran function copyFunction(func) { let fnStr = func.toString() return func.prototype ? eval(`(${fnStr})`) : eval(fnStr) } function deepCopy (obj, cache = []) { if (typeof obj === 'function') { return copyFunction(obj) } if (obj === null || typeof obj !== 'object') { return obj } if (Object.prototype.toString.call(obj) === '[object Date]') return new Date(obj) if (Object.prototype.toString.call(obj) === '[object RegExp]') return new RegExp(obj) if (Object.prototype.toString.call(obj) === '[object Error]') return new Error(obj) const item = cache.filter(item => item.original === obj)[0] if (item) return item.copy let copy = Array.isArray(obj) ? [] : {} cache.push({ original: obj, copy }) Object.keys(obj).forEach(key => { copy[key] = deepCopy(obj[key], cache) }) return copy } deepCopy($obj).func === $obj.func // false
## 總結
至此, 深淺拷貝已所有實現,但學無止境,咱們還能夠考慮使用 Proxy
提高來深拷貝性能,經過攔截 set
和 get
實現,固然 Object.defineProperty()
也能夠。感興趣的同窗能夠查看這文章,文章介紹的很詳細頭條面試官:你知道如何實現高性能版本的深拷貝嘛?
針對@Brota掘友提出的對於函數引用相同的問題, 文中現已修復
文章中有什麼問題, 歡迎你們積極指出, 不勝感激!!!