克隆的意义和常见场景
- 意义:保证原数据的完整性和独立性
- 常用场景:复制数据、函数入参、clss 构造函数等等
克隆
浅克隆
- 只克隆对象的第一层级
- 如果属性值是原始数据类型,拷贝其值,也就是常说的值拷贝
- 如果属性值是引用类型,拷贝其内存地址,也就是常说的引用拷贝
常用的浅克隆
- ES6 拓展运算符
- Object.assign
- for in
看个使用扩展运算符实现克隆的例子:
const person = {
name: 'Jae',
getName: function() {
return this.name;
},
address: {
province: '福建'
}
}
const person2 = {...person};
person2.nae = 'Tom';
person2.getName = function() {
return 'person2' + this.name
}
person2.address.province = '广东'
console.log("person.name: ", person.name); // Jae, 正常
console.log("person.getName: ", person.getName()); // Jae, 正常
console.log("person.address.province: ", person.address.province); // 广东, 受到影响
数组常用的浅拷贝
- ES6 拓展运算符
- slice
- [].concat
同样看个使用拓展运算符实现数组拷贝的例子:
const arr = [1, 2, 3];
const arr2 = [...arr];
const arr3 = arr.slice(0);
const arr4 = [].concat(arr);
console.log("arr: ", arr); // [1, 2, 3]
console.log("arr2: ", arr2, arr2 == arr); // [1, 2, 3] false, 完美克隆
console.log("arr3: ", arr3, arr3 == arr); // [1, 2, 3] false, 完美克隆
console.log("arr4: ", arr4, arr4 == arr); // [1, 2, 3] false, 完美克隆
深度克隆
- 克隆对象的每个层级
- 如果属性值是原始数据类型,拷贝其值,也就是常说的值拷贝
- 如果属性值是引用类型,递归克隆
下面来看具体的几种深度克隆方法
JSON.stringify() + JSON.parse()
JSON.parse(JSON.stringify(obj));
- 优点:纯天然,无污染
- 局限性:
- 只能复制普通键的属性,Symbol类型无能为力
- 循环引用对象,比如 Window 不能复制
- 函数、Date、RegExp、Blob 等类型不能赋值
- 性能差
- 可能出现溢出栈的情况
看一下代码示例:
const obj = {
a: 1,
b: {
c: 2
}
}
const date = { date: new Date() };
const reg = { reg: /[0 - 9]/ };
const bolb = { blob: new Blob(['123']) }
const func = { fun() {} }
console.log(JSON.parse(JSON.stringify(obj)));
// {a:1,b:{c:2}} 完美
console.log(JSON.parse(JSON.stringify(date)));
// {date: '2022-03-30T07:20:14.135Z'} 变成字符串了
console.log(JSON.parse(JSON.stringify(reg)));
// {reg: {}} 变成空对象了
console.log(JSON.parse(JSON.stringify(blob)));
// {blob: {}} 变成空对象了
console.log(JSON.parse(JSON.stringify(func)));
// {} 变成空对象了
console.log(JSON.parse(JSON.stringify(window)));
// Uncaught TypeError: Converting circular structure to JSON
消息通讯 - BroadcastChannel 等等
实际上是用浏览器内置的各种 API 进行通信,通信的过程中会对传递的参数进行复制,可以利用这个特点进行深度克隆
let chId = 0;
function clone(data) {
chId ++;
let cname = `__clone__${chId}`;
let ch1 = new BroadcastChannel(cname);
let ch2 = new BroadcastChannel(cname);
return new Promise(resolve => {
ch2.addEventListener('message', ev => resolve(ev.data), { once: true });
ch1.postMessage(data);
})
}
const obj = {
a: 1,
b: {
c: 2
},
d: {
date: new Date()
},
e: {
reg: /[0 - 9]/
}
};
clone(obj).then(res => console.log(res)); // 完美克隆 obj, 可以自己试下
const obj2 = { func() {} }
clone(obj2).then(res => console.log(res));
// Uncaught (in promise) DOMException: Failed to execute 'postMessage' on 'BroadcastChannel': func() {} could not be cloned
clone(window).then(res => console.log(res));
// Uncaught (in promise) DOMException: Failed to execute 'postMessage' on 'BroadcastChannel': #<Window> could not be cloned
可以看到,除了函数和 window 无法被克隆,对象都能完美克隆。
局限性:
- 循环引用对象不能赋值,如 Windows
- 函数不能赋值
- 同步变成异步,需要用 Promise 进行包装
手写简单版深度克隆
function isObject(obj) {
return obj !== null && typeof obj == "object";
}
function isArray(obj) {
return Array.isArray(obj)
}
function hasOwn(obj, key) {
return Object.prototype.hasOwnProperty.call(obj, key)
}
function deepClone(obj) {
if (!isObject(obj)) return obj;
let data;
if (isArray(obj)) {
data = [];
for (let i = 0; i < obj.length; i++) {
data[i] = deepClone(obj[i]);
}
} else if (isObject(obj)) {
data = {};
for (let key in obj) {
if (hasOwn(obj, key)) {
data[key] = deepClone(obj[key]);
}
}
}
return data;
}
可以自己去试一试,这里直接给结论:
- 只能复制一般的数组和对象
- 循环引用没有处理
- 递归(可能爆栈)
- 特殊类型未处理(函数、Date、RegExp、Blob 等)
循环引用的问题如何解决
从前面的例子中可以看出,循环引用是很难成功复制的,那么怎么解决这个问题呢?
这里使用 ES6 中的 WeakMap 来解决。
function isObject(obj) {
return obj !== null && typeof obj == "object";
}
function isArray(obj) {
return Array.isArray(obj)
}
function hasOwn(obj, key) {
return Object.prototype.hasOwnProperty.call(obj, key)
}
function deepClone(obj) {
const wmap = new WeakMap();
wmap.set(obj, 1);
function deepCloneInner() {
if (!isObject(obj)) return obj;
const data = isArray(obj) ? [] : {};
for (let key in obj) {
const val = obj[key];
if (hasOwn(obj, key)) {
// 原始数据类型
if (!isObject(val)) {
data[key] = val
continue;
}
if (wmap.has(val)) {
continue;
}
wmap.set(val, 1);
data[key] = deepCloneInner(val);
}
}
return data;
}
return deepCloneInner(obj);
}
// 循环引用
let obj = {
name: 'obj'
}
obj['obj2'] = obj;
console.log(deepClone(obj)); // {name: 'obj'} 完美复制
爆栈问题如何解决
问题是由于使用递归引起的,这里将递归用循环代替来实现:
function hasOwnProp(obj, property) {
return Object.prototype.hasOwnProperty.call(obj, property)
}
function getType(obj) {
return Object.prototype.toString.call(obj).slice(8, -1).toLowerCase();
}
function isObject(obj) {
return obj !== null && typeof obj == "object";
}
function isArray(obj) {
return Array.isArray(obj)
}
function isCloneObject(obj) {
return isObject(obj) || isArray(obj)
}
// 循环
function cloneDeep(x) {
// 先设置默认值
let root = x;
if (isArray(x)) {
root = [];
} else if (isObject(x)) {
root = {};
}
// 循环数组
const loopList = [{
parent: root,
key: undefined,
data: x,
}];
while (loopList.length) {
// 深度优先
// 出栈
const node = loopList.pop();
const parent = node.parent;
const key = node.key;
const data = node.data;
// 初始化赋值目标,key为undefined则拷贝到父元素,否则拷贝到子元素
let res = parent;
if (typeof key !== 'undefined') {
res = parent[key] = isArray(data) ? [] : {};
}
if (isArray(data)) {
for (let i = 0; i < data.length; i++) {
// 避免一层死循环 a.b = a
if (data[i] === data) {
res[i] = res;
} else if (isCloneObject(data[i])) { // 需要深度复制的属性值
// 下一次循环, 入栈
loopList.push({
parent: res,
key: i,
data: data[i],
});
} else {
res[i] = data[i];
}
}
} else if (isObject(data)) {
for (let k in data) {
if (hasOwnProp(data, k)) {
// 避免一层死循环 a.b = a
if (data[k] === data) {
res[k] = res;
} else if (isCloneObject(data[k])) { // 需要深度复制的属性值
// 下一次循环
loopList.push({
parent: res,
key: k,
data: data[k],
});
} else {
res[k] = data[k];
}
}
}
}
}
return root;
}
function createData(deep) {
var data = {};
var temp = data;
for (var i = 0; i < deep; i++) {
temp = temp['data'] = {};
temp[i + 1] = i + 1
}
return data;
}
const data = createData(10000);
console.log(cloneDeep(data)); // 完美复制
特殊类型如何处理
通过构造函数识别来处理,直接上代码:
function hasOwnProp(obj, property) {
return Object.prototype.hasOwnProperty.call(obj, property)
}
function getType(obj) {
return Object.prototype.toString.call(obj).slice(8, -1).toLowerCase();
}
function isObject(obj) {
return obj !== null && typeof obj == "object";
}
function isArray(obj) {
return Array.isArray(obj)
}
function isCloneObject(obj) {
return isObject(obj) || isArray(obj)
}
function cloneDeep(x) {
//使用WeakMap
let uniqueData = new WeakMap();
let root = x;
if (isArray(x)) {
root = [];
} else if (isObject(x)) {
root = {};
}
// 循环数组
const loopList = [{
parent: root,
key: undefined,
data: x,
}];
while (loopList.length) {
// 深度优先
const node = loopList.pop();
const parent = node.parent;
const key = node.key;
const source = node.data;
// 初始化赋值目标,key为undefined则拷贝到父元素,否则拷贝到子元素
let target = parent;
if (typeof key !== 'undefined') {
target = parent[key] = isArray(source) ? [] : {};
}
// 复杂数据需要缓存操作
if (isCloneObject(source)) {
// 命中缓存,直接返回缓存数据
let uniqueTarget = uniqueData.get(source);
if (uniqueTarget) {
parent[key] = uniqueTarget;
continue; // 中断本次循环
}
// 未命中缓存,保存到缓存
uniqueData.set(source, target);
}
if (isArray(source)) {
for (let i = 0; i < source.length; i++) {
if (isCloneObject(source[i])) {
// 下一次循环
loopList.push({
parent: target,
key: i,
data: source[i],
});
} else {
target[i] = source[i];
}
}
} else if (isObject(source)) {
for (let k in source) {
if (hasOwnProp(source, k)) {
if (isCloneObject(source[k])) {
// 下一次循环
loopList.push({
parent: target,
key: k,
data: source[k],
});
} else {
target[k] = source[k];
}
}
}
}
}
uniqueData = null;
return root;
}
var obj = {
p1: "p1",
p2: ["p22", {
p23: undefined,
p24: 666
}],
null: null,
p4: new RegExp(),
p3: undefined,
func: function () {
console.log("func");
return 1
},
Symbol: Symbol(2),
bigint: BigInt(100),
};
obj.loop = obj;
console.log(cloneDeep(obj)); // 完美复制
几种深度克隆方式的小结
深度克隆 | 循环引用 | 递归爆栈 | 特殊类型 |
---|---|---|---|
JSON | × | × | × |
消息通讯 | × | × | ✓(大部分) |
手写 | ✓ | ✓ | ✓ |
浅克隆 VS 深度克隆
方式方法 | 优点 | 缺点 | 备注 |
---|---|---|---|
浅克隆 | 性能高 | 数据可能不完全独立 | 一层属性全是值类型,等于深度克隆 |
深度克隆 | 不影响原对象 | 性能低,时间和空间消耗更大 | 保持数据独立,让函数无副作用等 |
参考资料:https://segmentfault.com/a/1190000016672263