前言:既然说深浅拷贝,就要先理解js到底有那数据类型
数据类型
-
基本类型
七种基本数据类型
Undefined、Null、Boolean、Number、String、symbol和bigint。变量是直接按值存放的,存放在栈内存中的简单数据段,可以直接访问。
-
引用类型
对象类型、函数,对象类型又分为:Object对象、Array数组、RegExp正则、Date时间对象、Math数学对象
存放在堆内存中的对象,变量保存的是一个指针,这个指针指向另一个位置。当需要访问引用类型(如对象,数组等)的值时,首先从栈中获得该对象的地址指针,然后再从堆内存中取得所需的数据
面试题:如何判断数据类型
typeof
- 能够准确检查除了 null 之外的基础数据类型(number, string, symbol, bigInt, undefined, boolean, null)
- 缺点:无法区分引用数据 数组 对象 null 都是'object'
instanceof
- instanceof 是用来判断 A 是否为 B 的实例
- instanceof 只能用来判断两个对象是否属于实例关系, 而不能判断一个对象实例具体属于哪种类型。
constructor
- null 和 undefined 是无效的对象,因此是不会有 constructor 存在的,这两种类型的数据需要通过其他方式来判断。
- 函数的 constructor 是不稳定的,这个主要体现在自定义对象上,当开发者重写 prototype 后,原有的 constructor 引用会丢失,constructor 会默认为 Object
Object.prototype.toString.call 无敌
回到正题 浅拷贝 vs 深拷贝
浅拷贝: 只复制引用,而未复制真正的值。
const originArray = [1,2,3,4,5];
const originObj = {a:'a',b:'b',c:[1,2,3],d:{dd:'dd'}};
const cloneArray = originArray;
const cloneObj = originObj;
console.log(cloneArray); // [1,2,3,4,5]
console.log(originObj); // {a:'a',b:'b',c:Array[3],d:{dd:'dd'}}
cloneArray.push(6);
cloneObj.a = {aa:'aa'};
console.log(cloneArray); // [1,2,3,4,5,6]
console.log(originArray); // [1,2,3,4,5,6]
console.log(cloneObj); // {a:{aa:'aa'},b:'b',c:Array[3],d:{dd:'dd'}}
console.log(originArray); // {a:{aa:'aa'},b:'b',c:Array[3],d:{dd:'dd'}}
利用 = 赋值操作符实现了一个浅拷贝,可以很清楚的看到,随着 cloneArray 和 cloneObj 改变,originArray 和 originObj 也随着发生了变化。
深拷贝: 就连值也都复制了不会存在相互影响
目前实现深拷贝的方法不多,主要是两种:
- 利用
JSON对象中的parse和stringify - 利用递归来实现每一层都重新创建对象并赋值
JSON.stringify/parse的方法
先看看这两个方法吧:
JSON.stringify 是将一个 JavaScript 值转成一个 JSON 字符串。
JSON.parse 是将一个 JSON 字符串转成一个 JavaScript 值或对象。
const originArray = [1,2,3,4,5];
const cloneArray = JSON.parse(JSON.stringify(originArray));
console.log(cloneArray === originArray); // false
const originObj = {a:'a',b:'b',c:[1,2,3],d:{dd:'dd'}};
const cloneObj = JSON.parse(JSON.stringify(originObj));
console.log(cloneObj === originObj); // false
cloneObj.a = 'aa';
cloneObj.c = [1,1,1];
cloneObj.d.dd = 'doubled';
console.log(cloneObj); // {a:'aa',b:'b',c:[1,1,1],d:{dd:'doubled'}};
console.log(originObj); // {a:'a',b:'b',c:[1,2,3],d:{dd:'dd'}};
确实是深拷贝,也很方便。但是,这个方法只能适用于一些简单的情况。
undefined、function、symbol 会在转换过程中被忽略
递归的方法
递归的思想就很简单了,就是对每一层的数据都实现一次 创建对象->对象赋值 的操作
function deepClone(source){
const targetObj = Array.isArray(source) ? [] : {}; // 判断复制的目标是数组还是对象
for(let keys in source){ // 遍历目标
if(source.hasOwnProperty(keys)){
if(source[keys] && typeof source[keys] === 'object'){ // 如果值是对象,就递归一下
targetObj[keys] = Array.isArray( source[keys]) ? [] : {};
targetObj[keys] = deepClone(source[keys]);
}else{ // 如果不是,就直接赋值
targetObj[keys] = source[keys];
}
}
}
return targetObj;
}
JavaScript中的拷贝方法
我们知道在 JavaScript 中,数组有两个方法 concat 和 slice 是可以实现对原数组的拷贝的,这两个方法都不会修改原数组,而是返回一个修改后的新数组。
同时,ES6 中 引入了 Object.assgn 方法和 ... 展开运算符也能实现对对象的拷贝。
concat 和 slice
- concat该方法可以连接两个或者更多的数组
- lice 对数组进行切割
//concat
const originArray = [1,2,3,4,5];
const cloneArray = originArray.concat();
console.log(cloneArray === originArray); // false
cloneArray.push(6); // [1,2,3,4,5,6]
console.log(originArray); [1,2,3,4,5];
//slice
const cloneArray2 = originArray.slice()
console.log(cloneArray2 === originArray)//false
cloneArray2.push(6); // [1,2,3,4,5,6]
console.log(originArray); [1,2,3,4,5];
看上去是深拷贝的。但我们试一下多层
const originArray = [1,[1,2,3],{a:1}];
const cloneArray = originArray.concat();
console.log(cloneArray === originArray); // false
cloneArray[1].push(4);
cloneArray[2].a = 2;
console.log(originArray); // [1,[1,2,3,4],{a:2}]
const cloneArray2 = originArray.slice();
console.log(cloneArray2 === originArray); // false
cloneArray2[1].push(4);
cloneArray2[2].a = 2;
console.log(originArray2); // [1,[1,2,3,4],{a:2}]
结论:concat和slice只是对数组的第一层进行深拷贝。
Object.assign()
const originObj ={a:1,b:{bb:2}}
const cloneObj = Object.assign({},originObj)
cloneObj.b.bb = 3
console.log(originObj) //{a:1,b:{bb:3}}
结论:Object.assign() 拷贝的是属性值。假如源对象的属性值是一个指向对象的引用,它也只拷贝那个引用值。
... 展开运算符
const originArray = [1,2,3,4,5,[6,7,8]];
const originObj = {a:1,b:{bb:1}};
const cloneArray = [...originArray];
cloneArray[0] = 0;
cloneArray[5].push(9);
console.log(originArray); // [1,2,3,4,5,[6,7,8,9]]
const cloneObj = {...originObj};
cloneObj.a = 2;
cloneObj.b.bb = 2;
console.log(originObj); // {a:1,b:{bb:2}}
结论:... 实现的是对象第一层的深拷贝。后面的只是拷贝的引用值。