面试的时候,经常会问深拷贝与浅拷贝的问题。因为它可以考察一个人的很多方面,比如基本功,逻辑能力,编码能力等等。
另外在实际工作中,也常会遇到它。比如用于页面展示的数据状态,与需要传给后端的数据包中,有部分字段的值不一致的话,就需要在传参时根据接口文档覆写那几个字段的值。
最常见的可能就是 status 这个参数了。界面上的展示需要 Boolean 值,而后端同学希望拿到的是 Number 值,1 或者 0。为了不影响展示效果,往往就需要深拷贝一下,再进行覆写,否则界面上就会因为某些值的变化,出现奇怪的现象。
至于为什么会这样,下文会讲到。马上开始今天的主题,让我们先从赋值开始说起。
赋值
Javascript 的原始数据类型有这几种:Boolean、Null、Undefined、Number、String、Symbol(ES6)。它们的赋值很简单,且赋值后两个变量互不影响。
let test1 = 'chao';
let test2 = test1;
// test2: chao
test1 = 'chao_change';
// test2: chao
// test1: chao_change
另外的引用数据类型有:Object 和 Array。深拷贝与浅拷贝的出现,就与这两个数据类型有关。
const obj = {a:1, b:2};
const obj2 = obj;
obj2.a = 3;
console.log(obj.a); // 3
依照赋值的思路,对 Object 引用类型进行拷贝,就会出问题。很多情况下,这不是我们想要的。这时,就需要用浅拷贝来实现了。
浅拷贝
什么是浅拷贝?可以这么理解:创建一个新的对象,把原有的对象属性值,完整地拷贝过来。其中包括了原始类型的值,还有引用类型的内存地址。只拷贝第一层的原始类型值,和第一层的引用类型地址。
深拷贝
我们当然希望当拷贝多层级的对象时,也能实现互不影响的效果。所以,深拷贝的概念也就油然而生了。我将深拷贝定义为:拷贝所有的属性值,以及属性地址指向的值的内存空间。
也就是说,当遇到对象时,就再新开一个对象,然后将第二层源对象的属性值,完整地拷贝到这个新开的对象中。
深拷贝和浅拷贝最根本的区别在于是否是真正获取了一个对象的复制实体,而不是引用,
深拷贝在计算机中开辟了一块内存地址用于存放复制的对象,而浅拷贝仅仅是指向被拷贝的内存地址,如果原地址中对象被改变了,那么浅拷贝出来的对象也会相应改变。
按照浅拷贝的思路,很容易就想到了递归调用。所以,就自己封装了个深拷贝的方法:
function deepClone(obj){
if(!obj && typeof obj !== 'object'){
return;
}
var newObj= toString.call(obj) === '[object Array]' ? [] : {};
for (var key in obj) {
if (obj[key] && typeof obj[key] === 'object') {
newObj[key] = deepClone(obj[key]);
} else {
newObj[key] = obj[key];
}
}
return newObj;
}
let arr = [{a:1,b:2}, {a:3,b:4}];
let newArr = deepClone(arr);
newArr.length = 1; // 为了方便区分,只保留新数组的第一个元素
console.log(newArr); // [{a:1, b:2}]
console.log(arr); // [{a:1, b:2}, {a:3, b:4}]
newArr[0].a = 123; // 修改 newArr 中第一个元素的 a
console.log(arr[0]); // {a:1, b:2}
一行代码的深拷贝
当然,还有最简单粗暴的深拷贝方法,就是利用 JSON 了。像这样:
let newArr2 = JSON.parse(JSON.stringify(arr));
console.log(arr[0]); // {a:1, b:2}
newArr2[0].a = 123;
console.log(arr[0]); // {a:1, b:2}
但是,JSON 内部用了递归的方式。数据一但过多,就会有递归爆栈的风险。
深拷贝的终极方案
有位大佬给出了深拷贝的终极方案,利用了“栈”的思想。
function cloneForce(x) {
// 用来去重
const uniqueList = [];
let 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] = {};
}
// 数据已经存在
let uniqueData = uniqueList.find((item) => item.source === data );
if (uniqueData) {
parent[key] = uniqueData.target;
// 中断本次循环
continue;
}
// 数据不存在
// 保存源数据,在拷贝数据中对应的引用
uniqueList.push({
source: data,
target: res,
});
for(let k in data) {
if (data.hasOwnProperty(k)) {
if (typeof data[k] === 'object') {
// 下一次循环
loopList.push({
parent: res,
key: k,
data: data[k],
});
} else {
res[k] = data[k];
}
}
}
}
return root;
}
总结
所谓深拷贝与浅拷贝,指的是 Object 和 Array 样的引用数据类型。
浅拷贝,只拷贝第一层的原始类型值,和第一层的引用类型地址。
深拷贝,拷贝所有的属性值,以及属性地址指向的值的内存空间。通过递归调用,或者 JSON 来做深拷贝,都会有一些问题。而 cloneForce 方法倒是目前看来最完美的解决方案了。
在日常的工作中,我们要特别注意,对象的 Object.assign(),数组的 Array.prototype.slice() 和 Array.prototype.concat(),还有 ES6 的 扩展运算符,都属于浅拷贝。当需要做数据组装时,一定要用深拷贝,以免影响界面展示效果。