前言
在开发中涉及到对象或数组的深拷贝,推荐使用lodash库中的cloneDeep
方法,我在github上看这个方法已经好几年没更新了,说明已经很成熟了。哪怕自己封装,封装到最完善也和这个方法一样重复造轮子,这里主要理一下深拷贝函数的封装思路。
lodash.cloneDeep()
使用很简单,就是引库调函数。
var _ = require('lodash');
var obj1 = {
a: 1,
b: { f: { g: 1 } },
c: [1, 2, 3]
};
var obj2 = _.cloneDeep(obj1);
console.log(obj1.b.f === obj2.b.f); // false
JSON.parse(JSON.stringify())
很多人喜欢此方法进行深拷贝,但是此方法很不靠谱,强烈不推荐
。
有很多弊端,首先是对象中有函数方法时,函数方法不会被拷贝;其次属性值为undefined的属性也不会拷贝;还有对象中存在循环引用时会造成内存溢出。
const obj = {
age: null,
name: '',
address: undefined,
friends: { a: '1'},
running: function() {console.log('--')}
}
const newObj = JSON.parse(JSON.stringify(obj))
console.log(newObj); // { age: null, name: '', friends: { a: '1' } }
自己封装深拷贝
首先是最简单的深拷贝封装,需要处理下列数据。
- 处理基本数据类型
- 处理对象类型
function isObj(obj) {
return Object(obj) === obj // 判断传入的参数是否为引入类型
}
function cloneDeep(obj) {
if (!isObj(obj)) { // 基本数据类型直接返回
return obj
}
const newObj = {}
for (const k in obj) {
newObj[k] = cloneDeep(obj[k])
}
return newObj
}
const obj = {
age: null,
name: '',
address: undefined,
friends: { a: '1'},
}
console.log(cloneDeep(obj)); // { age: null, name: '', address: undefined, friends: { a: '1' } }
但是对象中的属性值可能也是数组和函数,所以要加上对数组类型和函数的处理。
- 处理数组类型
- 处理函数
function isObj(obj) {
return Object(obj) === obj
}
function cloneDeep(obj) {
if (typeof obj === 'function') {
return obj
}
if (!isObj(obj)) {
return obj
}
const newObj = Array.isArray(obj) ? [] : {}
for (const k in obj) {
newObj[k] = cloneDeep(obj[k])
}
return newObj
}
const obj = {
age: null,
address: [1,2,3],
friends: { a: '1'},
}
console.log(cloneDeep(obj)); // { age: null, address: [ 1, 2, 3 ], friends: { a: '1' } }
javascript中后来新增了symbol基本数据类型,也可以作为对象的属性,要加上对这个的处理。
- 处理属性是symbol类型
- 处理属性值是symbol类型
function isObj(obj) {
return Object(obj) === obj
}
function cloneDeep(obj) {
if (typeof obj === 'symbol') {
return Symbol(obj.description)
}
if (typeof obj === 'function') {
return obj
}
if (!isObj(obj)) {
return obj
}
const newObj = Array.isArray(obj) ? [] : {}
for (const k in obj) {
newObj[k] = cloneDeep(obj[k])
}
const objSymbolKeys = Object.getOwnPropertySymbols(obj)
for (const k of objSymbolKeys) {
newObj[k] = cloneDeep(obj[k])
}
return newObj
}
const s1 = Symbol('a')
const s2 = Symbol('b')
const obj = {
age: null,
address: [1,2,3],
friends: { a: '1'},
[s1]: s2
}
console.log(cloneDeep(obj));
// {
age: null,
address: [ 1, 2, 3 ],
friends: { a: '1' },
[Symbol(a)]: Symbol(b)
}
下面也可以接着处理属性值是Set和Map数据结构的数据。
- 处理Set数据类型
- 处理Map数据类型
function isObj(obj) {
return Object(obj) === obj
}
function cloneDeep(obj) {
if (typeof obj === 'symbol') {
return Symbol(obj.description)
}
if (obj instanceof Set) {
return new Set([...obj])
}
if (obj instanceof Map) {
return new Map([...obj])
}
if (typeof obj === 'function') {
return obj
}
if (!isObj(obj)) {
return obj
}
const newObj = Array.isArray(obj) ? [] : {}
for (const k in obj) {
newObj[k] = cloneDeep(obj[k])
}
const objSymbolKeys = Object.getOwnPropertySymbols(obj)
for (const k of objSymbolKeys) {
newObj[k] = cloneDeep(obj[k])
}
return newObj
}
const s1 = Symbol('a')
const s2 = Symbol('b')
const obj = {
age: null,
address: [1,2,3],
friends: { a: '1'},
[s1]: s2,
set: new Set(["aaa", "bbb",]),
map: new Map([["aaa", "bbb"], ["bbb", "ccc"]]),
}
console.log(cloneDeep(obj));
// {
age: null,
address: [ 1, 2, 3 ],
friends: { a: '1' },
set: Set(2) { 'aaa', 'bbb' },
map: Map(2) { 'aaa' => 'bbb', 'bbb' => 'ccc' },
[Symbol(a)]: Symbol(b)
}
下面就是处理循环引用的问题了。什么是循环引用呢,看下图就懂了。但是处理循环引用有点麻烦,需要使用弱引用,弱引用的使用方式可自行去了解。
- 处理循环引用
这里一定要加弱引用,否则会无限循环,导致内存溢出。那么问题来了,为啥不用强引用呢,强引用无法使用垃圾回收机制进行垃圾回收。
最终版封装函数
如下所示:
function isObj(obj) {
return Object(obj) === obj
}
function cloneDeep(obj, map = new WeakMap()) {
if (typeof obj === 'symbol') {
return Symbol(obj.description)
}
if (obj instanceof Set) {
return new Set([...obj])
}
if (obj instanceof Map) {
return new Map([...obj])
}
if (typeof obj === 'function') {
return obj
}
if (!isObj(obj)) {
return obj
}
if (map.has(obj)) {
return map.get(obj)
}
const newObj = Array.isArray(obj) ? [] : {}
map.set(obj, newObj)
for (const k in obj) {
newObj[k] = cloneDeep(obj[k], map)
}
const objSymbolKeys = Object.getOwnPropertySymbols(obj)
for (const k of objSymbolKeys) {
newObj[k] = cloneDeep(obj[k], map)
}
return newObj
}
const s1 = Symbol('a')
const s2 = Symbol('b')
const obj = {
age: null,
address: [1,2,3],
friends: { a: '1'},
eat: () => {console.log('aaa')},
[s1]: s2,
set: new Set(["aaa", "bbb",]),
map: new Map([["aaa", "bbb"], ["bbb", "ccc"]]),
}
obj.info = obj // 循环引用
console.log(cloneDeep(obj));
// <ref *1> {
age: null,
address: [ 1, 2, 3 ],
friends: { a: '1' },
eat: [Function: eat],
set: Set(2) { 'aaa', 'bbb' },
map: Map(2) { 'aaa' => 'bbb', 'bbb' => 'ccc' },
info: [Circular *1],
[Symbol(a)]: Symbol(b)
}