Appearance
JS 实现深拷贝
JSON 序列化和反序列化
对于一般(狭义)的对象,最简单快速的方法就是使用 JSON 的序列化和反序列化:
javascript
const o1 = {
name: 'xxx',
age: 18,
child: {
name: 'zzz'
}
}
const o2 = JSON.parse(JSON.stringify(o1))
o1 === o2 // false
o1.name === o2.name // true
o1.child === o2.child // false
o1.child.name === o2.child.name // true
缺点:仅支持 JSON 所支持的数据类型和结构。
例如:
- 不支持函数,会被直接忽略
- 不支持 undefined,会被直接忽略
- 不支持循环引用,例如 xxx.self = xxx,会报错
对于更复杂的对象,就无法使用上面的办法,需要使用递归来进行深拷贝。
搭建测试环境
安装 mocha、chai 和 sinon-chai:
yarn init -y
yarn add mocha chai sinon-chai --dev
package.json 添加运行测试命令:
json
"scripts": {
"test": "mocha test/**/*.js"
}
导出 deepClone:
javascript
// src/index.js
function deepClone() {}
module.exports = deepClone
编写测试用例:
javascript
// test/index.js
const chai = require('chai')
const sinonChai = require('sinon-chai')
const deepClone = require('../src/index')
chai.use(sinonChai)
const assert = chai.assert
describe('测试深拷贝', () => {
it('成功引入深拷贝', () => {
assert.isFunction(deepClone)
})
})
运行测试:
yarn test
递归深拷贝
拷贝基本类型
添加测试用例:
javascript
it('拷贝基本类型', () => {
const s1 = 'xxx'
const n1 = 123
const u1 = undefined
const b1 = true
const empty1 = null
const s2 = deepClone(s1)
const n2 = deepClone(n1)
const u2 = deepClone(u1)
const b2 = deepClone(b1)
const empty2 = deepClone(empty1)
assert(s2 === s1)
assert(n2 === n1)
assert(u2 === u1)
assert(b2 === b1)
assert(empty2 === empty1)
})
因为基本类型不会存在深浅拷贝的问题,所以直接返回它的值就可以了。
javascript
function deepClone(resource) {
return resource
}
拷贝一般(狭义)对象
添加测试用例:
javascript
it('拷贝一般(狭义)对象', () => {
const o1 = { name: 'xxx', child: { name: 'zzz' } }
const o2 = deepClone(o1)
assert(o1 !== o2)
assert(o1.name === o2.name)
assert(o1.child !== o2.child)
assert(o1.child.name === o2.child.name)
})
使用递归来进行深拷贝:
javascript
function deepClone(resource) {
if (resource instanceof Object) {
const result = new Object()
for(let key in resource) {
result[key] = deepClone(resource[key])
}
return result
}
return resource
}
拷贝数组
添加测试用例:
javascript
it('拷贝数组', () => {
const a1 = [[12, 23], [34, 45], [56, 67]]
const a2 = deepClone(a1)
assert(a1 !== a2)
assert(a1[0] !== a2[0])
assert(a1[1] !== a2[1])
assert(a1[2] !== a2[2])
assert.deepEqual(a1, a2)
})
还是使用递归来进行深拷贝,不过在最初生成的时候需要 new Array(),而不是 new Object()。
javascript
function deepClone(resource) {
if (resource instanceof Object) {
if (resource instanceof Array) {
const result = new Array()
for (let key in resource) {
result[key] = deepClone(resource[key])
}
return result
} else {
const result = new Object()
for (let key in resource) {
result[key] = deepClone(resource[key])
}
return result
}
}
return resource
}
拷贝函数
添加测试用例:
javascript
it('拷贝函数', () => {
const f1 = (a, b) => {
return a + b
}
f1.xxx = { yyy: { zzz: 'aaa' } }
const f2 = deepClone(f1)
assert(f1 !== f2)
assert(f1(1, 2) === f2(1, 2))
assert(f1.xxx !== f2.xxx)
assert(f1.xxx.yyy !== f2.xxx.yyy)
assert(f1.xxx.yyy.zzz === f2.xxx.yyy.zzz)
})
通过直接调用源函数的方法,来达到类似深拷贝的目的。
javascript
...
else if (resource instanceof Function) {
const result = function () {
return resource.apply(this, arguments)
}
for (let key in resource) {
result[key] = deepClone(resource[key])
}
return result
}
...
拷贝正则表达式
添加测试用例:
javascript
it('拷贝正则表达式', () => {
const reg1 = /hi\d+/gi
reg1.xxx = { yyy: { zzz: 'aaa' } }
const reg2 = deepClone(reg1)
assert(reg1 !== reg2)
assert(reg1.source === reg2.source)
assert(reg1.flags === reg2.flags)
assert(reg1.xxx !== reg2.xxx)
assert(reg1.xxx.yyy !== reg2.xxx.yyy)
assert(reg1.xxx.yyy.zzz === reg2.xxx.yyy.zzz)
})
正则表达式有两个非常重要的属性:
- source 返回主体内容
- flags 返回标记
可以根据这两个属性,new RegExp() 一个新的正则表达式。
javascript
...
else if (resource instanceof RegExp) {
const result = new RegExp(resource.source, resource.flags)
for (let key in resource) {
result[key] = deepClone(resource[key])
}
return result
}
...
拷贝日期
添加测试用例:
javascript
it('拷贝日期', () => {
const d1 = new Date()
d1.xxx = { yyy: { zzz: 'aaa' } }
const d2 = deepClone(d1)
assert(d1 !== d2)
assert(d1.getTime() === d2.getTime())
assert(d1.xxx !== d2.xxx)
assert(d1.xxx.yyy !== d2.xxx.yyy)
assert(d1.xxx.yyy.zzz === d2.xxx.yyy.zzz)
})
可以使用 new Date() 和源日期对象来初始化一个新的日期对象。
javascript
...
else if (resource instanceof Date) {
const result = new Date(resource)
for (let key in resource) {
result[key] = deepClone(resource[key])
}
return result
}
...
跳过原型属性
使用 for ... in 来遍历对象的 key 的时候,会默认遍历原型上的属性:
javascript
const obj = Object.create({ name: 'xxx' }) // name 会在 obj 的 __proto__ 中
obj.age = 18
for(let key in obj) {
console.log(key)
}
// 'age'
// 'name'
一般来说,不拷贝原型上的属性。
添加测试用例:
javascript
it('跳过原型属性', () => {
const o1 = Object.create({ name: 'xxx' })
o1.xxx = { yyy: { zzz: 'aaa' } }
const o2 = deepClone(o1)
assert(o1 !== o2)
assert.isTrue('name' in o1)
assert.isFalse('name' in o2)
assert(o1.xxx !== o2.xxx)
assert(o1.xxx.yyy !== o2.xxx.yyy)
assert(o1.xxx.yyy.zzz === o2.xxx.yyy.zzz)
})
所以在递归的时候,需要判断 key 是不是它自身的属性。
for (let key in resource) {
if(resource.hasOwnProperty(key)) {
result[key] = deepClone(resource[key])
}
}
代码优化
整合上面的代码,进行优化:
javascript
// src/index.js
function deepClone(resource) {
if (resource instanceof Object) {
let result
if (resource instanceof Array) {
result = new Array()
} else if (resource instanceof Function) {
result = function () {
return resource.apply(this, arguments)
}
} else if (resource instanceof RegExp) {
result = new RegExp(resource.source, resource.flags)
} else if (resource instanceof Date) {
result = new Date(resource)
} else {
result = new Object()
}
for (let key in resource) {
if(resource.hasOwnProperty(key)) {
result[key] = deepClone(resource[key])
}
}
return result
}
return resource
}
module.exports = deepClone
可以直观的看出,对于不同的复杂对象,只需要对其进行不同的特殊处理即可。
环处理
递归拷贝还有一个问题,就是对于环的处理。
例如 window.self 就是一个环,它指向 window 自己。当递归拷贝遇到环,就会陷入无限的循环。
编写测试用例:
javascript
it('环拷贝', () => {
const o1 = { name: 'xxx', child: { name: 'zzz' } }
o1.self = o1
const o2 = deepClone(o1)
assert(o1 !== o2)
assert(o1.name === o2.name)
assert(o1.child !== o2.child)
assert(o1.child.name === o2.child.name)
assert(o1.self !== o2.self)
})
处理的思路:对已处理过的对象进行缓存,在递归的同时,检查是否有缓存,如果发现有缓存,则代表有环,就直接返回该对象,达到环拷贝的目的。
javascript
// src/index.js
const cacheStack = []
function deepClone(resource) {
if (resource instanceof Object) {
const cache = findCache(resource)
if (cache) {
return cache
} else {
let result
if (resource instanceof Array) {
result = new Array()
} else if (resource instanceof Function) {
result = function () {
return resource.apply(this, arguments)
}
} else if (resource instanceof RegExp) {
result = new RegExp(resource.source, resource.flags)
} else if (resource instanceof Date) {
result = new Date(resource)
} else {
result = new Object()
}
cacheStack.push([resource, result])
for (let key in resource) {
if (resource.hasOwnProperty(key)) {
result[key] = deepClone(resource[key])
}
}
return result
}
}
return resource
}
function findCache(resource) {
for (let i = 0; i < cacheStack.length; i++) {
if (cacheStack[i][0] === resource) {
return cacheStack[i][1]
}
}
return null
}
module.exports = deepClone