JS 浅拷贝与深拷贝
关于深拷贝与浅拷贝的概念在此不在赘述,本篇只讲述如何实现深拷贝(针对数组与对象)
1.切实可行的深拷贝
1.1 自定义拷贝函数
const cloneDeep = (value) => {
// 非数组和非对象直接返回值即可
if (value == null || typeof value !== 'object') {
return value;
}
// 初始化
let result = Array.isArray(value) ? [] : {};
for (let key in value) {
if (value.hasOwnProperty(key)) {
result[key] = cloneDeep(value[key]);
}
}
return result;
}
注:为什么要使用 hasOwnProperty
这是因为 for … in 的原因,使用 for … in 的遍历对象或数组,会把原型上自定义增加的属性或方法一起遍历出来,因此需要通过 hasOwnProperty 来过滤 key 是否属于当前的对象的键或数组下标。想要了解更多可以查阅 JS 异步(下)第 5.6 节 for…in 和 for…of的区别
这里针对数组与对象的深拷贝,想要了解更加全面的深拷贝,可以查阅 lodash 的 cloneDeep 函数
1.2 JSON.parse()与JSON.stringify()
const arr = [
// ...
];
const clonedArr = JSON.parse(JSON.stringify(arr));
const obj = {
// ...
};
const clonedObj = JSON.parse(JSON.stringify(obj));
注:这种方法虽然简单粗暴,但是也有其局限性,当值为undefined、function、symbol 会在转换过程中被忽略。具体体现是,在拷贝后的数组中对应索引位置的值为null,在拷贝后的对象中直接失去对应的键。
1.3 示例
给出需要拷贝的数组和对象:
const arr = [
[1, 2, 3],
{ a: 4, b: 5 },
null,
undefined,
true,
0,
function() {
console.log('function')
},
Symbol('Symbol'),
];
const obj = {
a: [1, 2, 3],
b: { c: 4, d: 5 },
e: null,
f: undefined,
g: true,
h: 0,
i: function() {
console.log('function')
},
j: Symbol('Symbol'),
};
自定义拷贝函数的拷贝展示:
const newArr = cloneDeep(arr);
console.log(arr[0] === newArr[0]); // false
arr[1].a = 400;
console.log(arr[1].a, newArr[1].a); // 400 4
可以看到,修改了 arr 中的对象,不会影响克隆出来的 newArr。拷贝对象类似,不再赘述。
JSON.parse()与JSON.stringify():
const newArr = JSON.parse(JSON.stringify(arr));
console.log(arr[0] === newArr[0]); // false
arr[1].a = 400;
console.log(arr[1].a, newArr[1].a); // 400 4
console.log(newArr); // undefined、function、symbol 拷贝后为 null
const newObj = JSON.parse(JSON.stringify(obj));
console.log(newObj); // undefined、function、symbol 在对象中对应的键被忽略
同样实现了深拷贝,只是需要注意的是 undefined、function、symbol 在拷贝后的情况。
2.非完全深拷贝情况
上面讲述了即使在多层级的数组或对象中仍然可以实现深拷贝的方法,但是有一些方法,当数组或对象的第一层级中不包含引用类型时也可以实现深拷贝,包含引用类型的值则无法实现深拷贝,在这我称为非完全深拷贝方法。
2.1 数组方法 - concat()
concat() 方法用于连接两个或多个数组,不会改变原数组。
const newArr = arr.concat([]);
console.log(arr[0] === newArr[0]); // true
arr[1].a = 400;
console.log(arr[1].a, newArr[1].a); // 400 400
console.log(newArr);
console.log(arr === newArr); // false
以看到,通过 concat() 拷贝之后,引用类型的值是没有实现深拷贝的,而值类型的值是实现了深拷贝。这表明在第一层级只要不包含引用类型即可实现深拷贝,在使用时需要注意这一点。
为什么上面的例子中引用类型没有实现深拷贝,这是因为,通过 concat 拷贝后的数组,数组中引用类型拷贝的是其值在栈中的地址。
2.2 数组方法 - slice()
slice() 方法可从已有的数组中返回选定的元素,不会改变原数组。当 slice 不传任何参数时,表示返回所有元素。
const newArr = arr.slice();
效果类似于 2.1 concat()
2.3 数组 / 对象 - Object.assign()
Object.assign() 方法用于将所有可枚举属性的值从一个或多个源对象分配到目标对象。它将返回目标对象。
// Object.assign() 用法示例
const source = { a: 1, b: 2 };
const returned = Object.assign({}, source);
针对深拷贝,需要使用其他办法,因为 Object.assign()拷贝的是(可枚举)属性值。假如源值是一个对象的引用,它仅仅会复制其引用值。
效果类似于 2.1 concat()
这里针对 Object.assign() 仅仅是简单的使用,想了解的更全面可以查阅 Object.assign()
2.4 小结
针对上面几种非完全深拷贝方法,可以视情况使用,如果能确认被拷贝对象内的值无引用类型,可以使用上述方法,但实际上如果是请求接口返回结果,就很难去判断。因此推荐使用第一点中的两种方式,或者使用成熟的 lodash 库中的 cloneDeep 方法。
若有错误欢迎大家指正