在平時開發中,通常處理兩種數據類型:git
基本數據類型存在棧中,引用數據類型存放在堆中。github
在談到深淺拷貝時,也是圍繞這兩種數據類型展開的。數組
建立一個對象,從新複製或引用的源對象的值。函數
JS
提供了以下方法來對象和數據進行淺拷貝。oop
能夠在函數調用/數組構造時, 將數組表達式或者 string
在語法層面展開;還能夠在構造字面量對象時, 將對象表達式按 key-value
的方式展開。測試
// 數據的拷貝 let arr = [1, 2, 3, 4, { a: 1 }] let arrCopy = [...arr] arrCopy[4].a = 100 console.log(arrCopy[4].a) // 100 console.log(arr[4].a) // 100 // 對象的拷貝 let obj = { a: 1, b: { c: 3 } } console.log(obj.b.c) // 3 let objCopy = { ...obj } objCopy.b.c = 4 console.log(obj.b.c) // 4 console.log(objCopy.b.c) // 4
不難發現數據類型都是基本類型,使用展開運算符拷貝很是方便。spa
Object.assign
方法用於將全部可枚舉屬性的值從一個或多個源對象分配到目標對象。它將返回目標對象。3d
let t = {} let s = { a: { b: 100 } } Object.assign(t, s) console.log(s.a.b) // 100 t.a.b = 200 console.log(s.a.b) // 200 console.log(t.a.b) // 200
須要注意的幾個地方指針
Object.assign
會遍歷原對象的屬性,經過複製的方式將其賦值給目標對象的相應屬性(包括 Symbol 類型的對象)
對於基本類型直接進行拷貝複製,對於引用類型,在內存中開闢一塊新的空間進行拷貝複製。
const isObject = (obj) => typeof obj === 'object' && obj !== null; const shallowClone = (obj) => { if (!is(obj)) return obj const cloneObj = Array.isArray(obj) ? [] : {}; for (let prop in obj) { if (obj.hasOwnProperty(prop)) { cloneObj[prop] = obj[prop]; } } return cloneObj; }
將一個對象從內存中完整地拷貝出來一份給目標對象,並從堆內存中開闢一個全新的空間存放新對象,且新對象的修改並不會改變原對象,兩者實現真正的分離。
JSON.stringfy
是前端中最簡單的深拷貝方式,把對象序列化成爲 JSON
的字符串,並將對象裏面的內容轉換成字符串,再用 JSON.parse
將 JSON
字符串生成一個新的對象。
let obj1 = { a: 1, b: [1, 2, 3] } let str = JSON.stringify(obj1); let obj2 = JSON.parse(str); console.log(obj2); //{a:1,b:[1,2,3]} obj1.a = 2; obj1.b.push(4); console.log(obj1); //{a:2,b:[1,2,3,4]} console.log(obj2); //{a:1,b:[1,2,3]}
可是 JSON.stringfy
在深拷貝會出現如下的問題:
undefined
、symbol
這幾種類型,通過 JSON.stringify
序列化以後的字符串中這個鍵值對會消失。Date
引用類型會變成字符串。RegExp
引用類型會變成空對象。NaN
、Infinity
以及 -Infinity
,JSON
序列化的結果會變成 null
。let testObj = { [Symbol('test')]: 1, number: -1, string: 'Hello World', boolean: true, undefined: undefined, nul: null, obj: { name: '我是一個對象', id: 1 }, list: [11, 22, 33], fn: function() { console.log('Hello World') }, date: new Date(), regExp: new RegExp('/[\w]/ig'), }; Object.defineProperty(testObj, 'innumerable', { enumerable: false, value: 'innumerable' }); obtestObjj = Object.create(testObj, Object.getOwnPropertyDescriptors(testObj)) // 設置循環引用, 不能拷貝,將會報錯 // testObj.loop = testObj let cloneObj = JSON.parse(JSON.stringify(testObj)) cloneObj.list.shift() console.log('testObj', testObj) console.log('cloneObj', cloneObj)
對於上述 JSON.stringfy
缺點,在實現深拷貝進行以下的改善:
Date
、RegExp
類型,直接返回一個新的實例。symbol
屬性,採用 Reflect.ownKeys
。Reflect.ownKeys
方法用於返回對象的全部屬性,基本等同於Object.getOwnPropertyNames
與Object.getOwnPropertySymbols
之和。Object.getPrototypeOf
獲取對象的原型對象,以及對象的屬性描述對象Object.getOwnPropertyDescriptors
。WeakMap
, WeakMap
是弱引用類型,能夠有效防止內存泄漏。const isComplexDataType = obj => (typeof obj === 'object' || typeof obj === 'function') && (obj !== null) const deepClone = function(obj, hash = new WeakMap()) { // 處理 RegExp 對象 if (obj.constructor === RegExp) return new RegExp(obj) // 處理 Date 對象 if (obj.constructor === Date) return new Date(obj) // 處理循環引用 if (hash.has(obj)) return hash.get(obj) // 獲取屬性的描述器 let allDesc = Object.getOwnPropertyDescriptors(obj) // 建立新的對象,設置原型鏈 let cloneObj = Object.create(Object.getPrototypeOf(obj), allDesc) // 處理循環引用 hash.set(obj, cloneObj) // Reflect.ownKeys 能夠獲取不可枚舉和 symbol 屬性 for (let key of Reflect.ownKeys(obj)) { cloneObj[key] = (isComplexDataType(obj[key]) ? deepClone(obj[key], hash) : obj[key] } return cloneObj } }
接下來深拷貝測試
// 測試 對象 let testObj = { [Symbol('test')]: 1, number: -1, string: 'Hello World', boolean: true, undefined: undefined, nul: null, obj: { name: '我是一個對象', id: 1 }, list: [11, 22, 33], fn: function() { console.log('Hello World') }, date: new Date(), regExp: new RegExp('/[\w]/ig'), }; Object.defineProperty(testObj, 'innumerable', { enumerable: false, value: 'innumerable' }); obtestObjj = Object.create(testObj, Object.getOwnPropertyDescriptors(testObj)) // 設置循環引用 testObj.loop = testObj let cloneObj = deepClone(testObj) cloneObj.list.shift() console.log('cloneObj', testObj) console.log('cloneObj', cloneObj)
能夠看出解決了 JSON.stringfy
在進行深拷貝的時候的缺點。
在平時開發過程,咱們能夠採用第三方庫來實現深拷貝,好比 loadsh.cloneDeep
。