方案1.序列化反序列化
var a = {name: 'lifa',age:18}
var b = JSON.parse(JSON.stringify(a))
上面的代碼b就是a的深拷貝當(dāng)我們修改b里面的值的時候,a的值不會跟著變化窃蹋。
1.1.缺點(diǎn)
1). 不支持function
如果被深拷貝的對象里面有函數(shù)的話蛤育,我們深拷貝后的對象會直接將函數(shù)忽略
2). 不支持undefined
如果被深拷貝的對象里面有undefined有滑,我們使用JSON對其深拷貝后恐锦,深拷貝后的對象也會將undefined忽略
3). 不支持循環(huán)引用(不能把自己賦值給自己內(nèi)部的屬性)
報錯,JSON只支持豎狀的結(jié)構(gòu)不支持環(huán)狀結(jié)構(gòu)
4). 會把Date類型變成字符串
Date類型經(jīng)過JSON深拷貝后變成了ISO8601格式的字符串
5). 不支持正則表達(dá)式
正則表達(dá)式經(jīng)過JSON后會變成空對象
6). 不支持Symbol
方案2. 遞歸克隆
2.1 思路
- 遞歸
看節(jié)點(diǎn)的類型(7種)
如果是基本類型直接拷貝
如果是object就分情況討論(普通object筑公、數(shù)組array、函數(shù)function尊浪、日期Date)
2.2 步驟
創(chuàng)建目錄
引入chai和sinon
開始驅(qū)動測試開發(fā)
測試失敗=> 改代碼 => 測試成功 =>加測試 => 測試失敗
- src/index.js
function deepCLone() {
}
module.exports = deepCLone
- test/index.js
const sinon = require('sinon')
const sinonChai = require('sinon-chai')
const deepCLone = require('../src/index')
chai.use(sinonChai)
const assert = chai.assert
const deepClone = require('../src/index')
describe('deepClone', () => {
it('是一個函數(shù)', () => {
assert.isFunction(deepCLone)
})
})
- package.json
{
"name": "deep-clone",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"scripts": {
"test": "mocha test/**/*.js"
},
"devDependencies": {
"chai": "^4.2.0",
"mocha": "^6.2.0",
"sinon": "^7.4.1",
"sinon-chai": "^3.3.0"
}
}
運(yùn)行yarn test
2.2.1. 對基本類型的拷貝
對于基本類型直接傳什么就返回什么
function deepClone(source) {
return source
}
單元測試
it('可以復(fù)制基本類型', () => {
const n = 123
const n2 = deepClone(n)
assert(n ===n2)
const b = '123456'
const b2 = deepClone(b)
assert(b === b2)
const c = undefined
const c2 = deepClone(c)
assert(c === c2)
const d = null
const d2 = deepClone(d)
assert(d === d2)
const e = true
const e2 = deepClone(e)
assert(e === e2)
const f = Symbol()
const f2 = deepClone(f)
assert(f === f2)
})
2.2.2. 對于普通對象的深拷貝
先判斷傳入的參數(shù)類型是不是對象匣屡,然后創(chuàng)建一個新的對象封救,遍歷入?yún)ο蟮拿恳豁梜ey,將每一項的值都進(jìn)行深拷貝然后把當(dāng)前項賦值給新的對象
if (source instanceof Object) {
const deepObj = new Object()
for (let key in source) {
// 對對象里的每一項進(jìn)行深拷貝并把這一項賦值給新的對象
deepObj[key] = deepClone(source[key])
}
return deepObj
}
單元測試
it('可以復(fù)制普通對象', () => {
const a = { name: '立發(fā)', child: { name: '小立發(fā)'}}
const a2 = deepClone(a)
assert(a !== a2)
assert(a.name === a2.name)
assert(a.child !== a2.child)
assert(a.child.name === a2.child.name)
})
2.2.3. 對于數(shù)組對象的深拷貝
if (source instanceof Object) {
if (source instanceof Array) {
const deepObj = new Array()
for (let key in source) {
// 對對象里的每一項進(jìn)行深拷貝并把這一項賦值給新的對象
deepObj[key] = deepClone(source[key])
}
return deepObj
}
}
單元測試
it('可以復(fù)制數(shù)組對象', () => {
const a = [[1, 2], [4, 5], [6, 7]]
const a2 = deepClone(a)
assert(a !== a2)
assert(a[0] !== a2[0])
assert(a[1] !== a2[1])
assert(a[2] !== a2[2])
assert.deepEqual(a, a2)
})
2.2.4. 對于函數(shù)對象的深拷貝
判斷一個函數(shù)是不是另一個函數(shù)的深拷貝捣作,首先函數(shù)也是一個對象所以它應(yīng)該滿足對象深拷貝的條件
1).深拷貝后的函數(shù)應(yīng)該不等于源函數(shù)
2).深拷貝后的函數(shù)應(yīng)該和源函數(shù)的基本類型的屬性相等
3).深拷貝后的函數(shù)應(yīng)該和源函數(shù)的引用類型的屬性不相等
其次針對于函數(shù)對象本身
4).深拷貝后的函數(shù)的執(zhí)行結(jié)果應(yīng)該和源函數(shù)的執(zhí)行結(jié)果相等
針對上面四點(diǎn)的單元測試如下:
it('可以復(fù)制函數(shù)', () => {
const a = function(x, y) {
return x + y
}
a.xxx = { yyy: { zzz: 1 } }
const a2 = deepClone(a)
assert(a !== a2) //上面的1)
assert(a.xxx.yyy.zzz === a2.xxx.yyy.zzz) //2)
assert(a.xxx.yyy !== a2.xxx.yyy) //3)
assert(a.xxx !== a2.xxx) //3)
assert(a(1,2) === a2(1,2)) //4)
})
- index.js
else if (source instanceof Function) {
deepObj = function() {
return source.apply(this, arguments)
}
for (let key in source) {
// 對對象里的每一項進(jìn)行深拷貝并把這一項賦值給新的對象
deepObj[key] = deepClone(source[key])
}
return deepObj
}
實(shí)際上就是聲明一個新的函數(shù)誉结,然后在這個新的函數(shù)里把你傳入的函數(shù)調(diào)用一遍(把當(dāng)前新的函數(shù)的this傳給傳入的函數(shù),然后把參數(shù)放進(jìn)去)然后再return它
2.2.5. 對于環(huán)的深拷貝
我們上面的代碼如果出現(xiàn)環(huán)引用(某個屬性的值等于本身的引用地址)的話會出現(xiàn)死循環(huán)現(xiàn)象券躁,所以我們需要先把我們每次的值存下來(以此來確定某個屬性的值有沒有被深拷貝過)搓彻,如果下次調(diào)用發(fā)現(xiàn)某個屬性的值已經(jīng)深拷貝過了就直接返回第一次深拷貝過的值,否則就繼續(xù)遞歸嘱朽。
上面圖中黑色的環(huán)路是我們傳入的需要深拷貝的源對象旭贬,紅色的環(huán)路是我們深拷貝后的。我們看到了一個1就把1直接存儲下來(不經(jīng)過深拷貝處理搪泳,如果是引用類型直接把引用地址存儲下來)稀轨,然后對1進(jìn)行深拷貝生成一個新的1',然后看到2把2直接存儲下來岸军,然后對2進(jìn)行深拷貝生成一個新的2'奋刽,然后走到我們的3后面我們應(yīng)該接的實(shí)際上是我們第一次拷貝好的1',所以就需要我們每次存儲的時候把我們一開始的原始值和深拷貝后的值都存儲下來艰赞,通過原始值來得到我們第一次深拷貝后的值
let cache = []
function deepClone(source) {
if (source instanceof Object) {
let cacheDist = findCache(source)
if (cacheDist) {
return cacheDist
} else {
let deepObj
if (source instanceof Array) {
deepObj = new Array()
} else if (source instanceof Function) {
deepObj = function() {
return source.apply(this, arguments)
}
} else {
deepObj = new Object()
}
cache.push([source, deepObj])
for (let key in source) {
// 對對象里的每一項進(jìn)行深拷貝并把這一項賦值給新的對象
deepObj[key] = deepClone(source[key])
}
return deepObj
}
}
return source
}
function findCache(source) {
for (let i = 0; i < cache.length; i++) {
if (cache[i][0] === source) {
return cache[i][1]
}
}
return undefined
}
單元測試
it('環(huán)也能復(fù)制', () => {
const a = { name: '立發(fā)'}
a.self = a
const a2 = deepClone(a)
assert(a !== a2)
assert(a.name === a2.name)
assert(a.self !== a2.self)
})
2.2.6. 正則表達(dá)式的深拷貝
我們對正則表達(dá)式的聲明主要有兩部分佣谐,一部分是它的文本,一部分是它的標(biāo)志
const a = new RegExp('hi\\d+', 'gi')
上面的hi\d+就是它的文本方妖,gi就是標(biāo)志狭魂,所以我們要對它進(jìn)行深拷貝的話只需要拿到它的文本和標(biāo)志然后放到一個新的RegExp對象里即可,那么我們?nèi)绾文玫竭@兩部分的值那党觅,通過a.source就可以拿到它的文本值雌澄,a.flags就可以拿到標(biāo)志的值
if (source instanceof Object) {
else if (source instanceof RegExp) {
deepObj = new RegExp(source.source, source.flags)
}
}
單元測試
it('可以復(fù)制正則表達(dá)式', () => {
const a = new RegExp("hi\\d+", 'gi')
a.xxx = { yyy: { zzz: 1} }
const a2 = deepClone(a)
assert(a.source === a2.source)
assert(a.flags === a2.flags)
assert(a !== a2)
assert(a.xxx.yyy.zzz === a2.xxx.yyy.zzz)
assert(a.xxx.yyy !== a2.xxx.yyy)
assert(a.xxx !== a2.xxx)
})
2.2.6. Date類型的深拷貝
我們只需要判斷如果是Date類型,就直接把當(dāng)前的Date放到一個新的Date對象里即可杯瞻,我們想要判斷兩個Date的值是否相等只需要通過getTime()方法就可以
if (source instanceof Object) {
else if (source instanceof Date) {
deepObj = new Date(source)
}
}
單元測試
it('可以復(fù)制日期', () => {
const a = new Date()
a.xxx = { yyy: { zzz: 1} }
const a2 = deepClone(a)
assert(a.getTime() === a2.getTime())
assert(a !== a2)
assert(a.xxx.yyy.zzz === a2.xxx.yyy.zzz)
assert(a.xxx.yyy !== a2.xxx.yyy)
assert(a.xxx !== a2.xxx)
})
2.2.7. 自動跳過原型屬性
如果我們通過Object.create給對象添加一個屬性的時候镐牺,這個屬性會被添加到它的proto里也就是原型上
上圖中我們通過Object.create創(chuàng)建了一個a對象并添加了name屬性,但是a本身沒有name屬性魁莉,而是在proto里睬涧,對于這種在原型上的屬性我們不應(yīng)該去拷貝(原因:如果原型上的每一層都拷貝的話會造成內(nèi)存爆掉),那么我們怎么實(shí)現(xiàn)只拷貝本身的屬性哪旗唁?
辦法:在for in里加上限制條件
for (let key in source) {
// 如果key是source對象自身的屬性才去進(jìn)行拷貝
if (source.hasOwnProperty(key)) {
// 對對象里的每一項進(jìn)行深拷貝并把這一項賦值給新的對象
deepObj[key] = deepClone(source[key])
}
}
單元測試
it('自行跳過原型屬性', () => {
const a = Object.create( { name: 'lifa' })
a.xxx = { yyy: { zzz: 1} }
const a2 = deepClone(a)
//a2上沒有name屬性
assert.isFalse('name' in a2)
assert(a !== a2)
assert(a.xxx.yyy.zzz === a2.xxx.yyy.zzz)
assert(a.xxx.yyy !== a2.xxx.yyy)
assert(a.xxx !== a2.xxx)
})
2.3.問題
上面的代碼的問題:cache每次復(fù)制完一個對象的時候都沒有被清空畦浓,所以下一次深拷貝的時候就會互相影響
解決方法:使用面向?qū)ο螅看螌?shí)例化的時候生成一個新的cache
完整代碼
class DeepCloner {
constructor () {
this.cache = []
}
clone(source) {
if (source instanceof Object) {
let cacheDist = this.findCache(source)
if (cacheDist) {
return cacheDist
} else {
let deepObj
if (source instanceof Array) {
deepObj = new Array()
} else if (source instanceof Function) {
deepObj = function() {
return source.apply(this, arguments)
}
} else if (source instanceof RegExp) {
deepObj = new RegExp(source.source, source.flags)
} else if (source instanceof Date) {
deepObj = new Date(source)
} else {
deepObj = new Object()
}
this.cache.push([source, deepObj])
for (let key in source) {
if (source.hasOwnProperty(key)) {
// 對對象里的每一項進(jìn)行深拷貝并把這一項賦值給新的對象
deepObj[key] = this.clone(source[key])
}
}
return deepObj
}
}
return source
}
findCache(source) {
for (let i = 0; i < this.cache.length; i++) {
if (this.cache[i][0] === source) {
return this.cache[i][1]
}
}
return undefined
}
}
module.exports = DeepCloner