决定一个人的一生,以及整个命运的,只是一瞬之间。——歌德
今天,小和尚和大伙聊聊一个面试家常菜:请手写一个 JS 深克隆。
在正式开始前,先了解下克隆
英文“clone”的音译,在台湾与港澳一般意译为复制或转殖,是利用生物技术由无性生殖产生与
原个体
有完全相同
基因组织后代的过程。 ——百度百科
简单来说就是单独拷贝一份与原数据一模一样的新数据
JS 中的克隆有以下两种:
浅克隆
将目标对象的的第一层属性拷贝到一个新的对象中,其中对原始类型属性,进行值的拷贝;如果是引用类型属性,则是进行引用地址的拷贝。
-
数组的浅克隆
- array.concat
- array.slice
- [...array]
-
对象的浅克隆
- Object.assign
- { ...object }
-
缺点
- 新对象的引用类型属性修改时,会影响到原对象
- 符号属性(例:[Symbol])不能拷贝
- 不可枚举属性不能拷贝
// 测试用例
var obj = {
bigInt: BigInt(12312),
set:new Set([2]),
map:new Map([['a',22],['b',33]]),
num: 0,
str: '',
boolean: true,
unf: undefined,
nul: null,
obj: {
name: '我是一个对象',
id: 1
},
arr: [0, 1, 2],
func: function () {
console.log('我是一个函数')
},
date: new Date(0),
reg: new RegExp('/我是一个正则/ig'),
[Symbol('1')]: 1,
};
Object.defineProperty(obj, 'innumerable', {
enumerable: false,
value: '不可枚举属性'
});
obj.loop = obj;
// ------------------------------- 测试代码 -------------------------------
var cloneObj = {};
for (var prop in obj) {
cloneObj[prop] = obj[prop];
}
// 修改了新对象,原对象受到了影响
cloneObj.arr[0] = 2;
obj.arr[0]; // 2
深克隆
拷贝目标对象属性到新对象上,对于引用类型属性,是将引用地址指向的堆内存空间里的内容,重新生成一份新的,并拷贝到新对象上。
下面介绍一些常见的深克隆
丐中丐版
使用 JSON.parse(JSON.stringify(obj)) 实现
缺点:
- 不能拷贝
Map
、Set
、RegExp
、Function
类型的属性和符号属性 - 不能拷贝不可枚举属性
Date
类型的属性值,拷贝后会显示为字符串类型- 不能处理循环引用属性
- 反序列化后,新对象的原型会丢失,新对象 的
__proto__
自动指向了Object.prototype
// 测试用例
var obj = {
set:new Set([2]),
map:new Map([['a',22],['b',33]]),
num: 0,
str: '',
boolean: true,
unf: undefined,
nul: null,
obj: {
name: '我是一个对象',
id: 1
},
arr: [0, 1, 2],
func: function () {
console.log('我是一个函数')
},
date: new Date(0),
reg: new RegExp('/我是一个正则/ig'),
[Symbol('1')]: 1,
};
Object.defineProperty(obj, 'innumerable', {
enumerable: false,
value: '不可枚举属性'
});
// obj.loop 属性的值为自身,构成了循环引用属性
// obj.loop = obj;
// ------------------------------- 测试代码 -------------------------------
var cloneObj = JSON.parse(JSON.stringify(obj));
cloneObj.arr[0] = 2;
obj.arr[0]; // 0
大众版
递归遍历原对象的所有属性,将属性一个一个拷贝到新对象
优点:
- 增加了对
Function
类型属性的拷贝
缺点:
- 不能拷贝
Map
、Set
、RegExp
、Date
类型的属性和符号属性 - 不能拷贝不可枚举属性
- 不能处理循环引用属性(会引起爆栈)
- 容易出现栈溢出情况
// 测试代码
var obj = {
bigInt: BigInt(12312),
set:new Set([2]),
map:new Map([['a',22],['b',33]]),
num: 0,
str: '',
boolean: true,
unf: undefined,
nul: null,
obj: {
name: '我是一个对象',
id: 1
},
arr: [0, 1, 2],
func: function () {
console.log('我是一个函数')
},
date: new Date(0),
reg: new RegExp('/我是一个正则/ig'),
[Symbol('1')]: 1,
};
Object.defineProperty(obj, 'innumerable', {
enumerable: false,
value: '不可枚举属性'
});
// obj.loop = obj;
// ------------------------------- 测试代码 -------------------------------
// 深克隆函数
function deepClone(obj) {
var cloneObj;
if (obj && typeof obj === 'object') { // object || array
cloneObj = Array.isArray(obj) ? [] : {};
for (var prop in obj) {
cloneObj[prop] = deepClone(obj[prop]);
}
} else { // primitive
return obj;
}
}
var cloneObj = deepClone(obj);
cloneObj.arr[0] = 2;
obj.arr[0]; // 0
升级版
这个版本就是在大众版的基础上,增加了对其他类型属性的拷贝处理,这里由于代码太多就直接贴图了
终极版
优点:
- 解决了递归栈溢出问题
- 能拷贝目前所有 js 类型属性
- 解决了循环引用属性问题
- 拷贝原型对象属性
// 测试用例
let obj = {
bigInt: BigInt(12312),
set: new Set([2]),
map: new Map([['a', 22], ['b', 33]]),
num: 0,
str: '',
boolean: true,
unf: undefined,
nul: null,
obj: {
name: '我是一个对象',
id: 1,
},
arr: [0, 1, 2],
fn() {
return '我是一函数'
},
date: new Date(0),
reg: new RegExp('/我是一个正则/ig'),
[Symbol('1')]: 1,
};
Object.defineProperty(obj, 'innumerable', {
enumerable: false,
value: '不可枚举属性',
writable: false,
});
obj = Object.create(obj, Object.getOwnPropertyDescriptors(obj));
obj.loop = obj;
// ------------------------------- 测试代码 -------------------------------
// 判断是否是引用数据类型
deepClone.isComplexDataType = obj => obj !== null && (typeof obj === 'function' || typeof obj === 'object');
// 存放 typeof === "object" 的构造器,用于区别 obj 是否是普通对象
deepClone.objType = [Date, RegExp, WeakMap, WeakSet, Map, Set];
function deepClone(obj, hash = new WeakMap()) {
/** tip:能递归进入该函数的都是引用类型数据 **/
// 判断是否有缓存,如果有则直接返回,解决了递归爆栈的情况
// 例:obj.loop = obj:当这样形成环后,如果递归进入 deepClone,会返回第一次创建的 cloneObj
if (hash.has(obj)) return hash.get(obj);
// 如果不是普通对象,则拷贝一个新 obj 返回
if (deepClone.objType.includes(obj.constructor)) return new obj.constructor(obj);
// 获取目标对象的所有属性描述对象
const allDescriptions = Object.getOwnPropertyDescriptors(obj);
// 原型拷贝
const cloneObj = Object.create(Object.getPrototypeOf(obj), allDescriptions);
// 缓存拷贝的对象
hash.set(obj, cloneObj);
// Reflect.ownKeys 以数组形式返回对象的属性名(包括符号属性和不可枚举属性)
for (let key of Reflect.ownKeys(obj)) {
const value = obj[key];
// 原始类型属性直接返回
// 引用类型属性继续递归deepClone
cloneObj[key] = (deepClone.isComplexDataType(value) && typeof value !== 'function') ?
deepClone(value, hash) : value;
}
return cloneObj;
}
const cloneObj = deepClone(obj);
cloneObj.arr[0] = 2;
obj.arr[0]; // 0