面试常问到什么是深复制和浅复制(深拷贝和浅拷贝),怎么实现深复制,那么我们一起来探讨学习下。
下面三句话可以初步理解深浅复制的区别:
- 首先深复制和浅复制只针对像 Object, Array 这样的复杂对象的。
- 深复制:复制的是引用(地址),浅复制:复制的是实例。
- 简单来说,浅复制只复制一层对象的属性,而深复制则递归复制了所有层级。
首先先了解ECMAScript 中的变量类型,分为两类:
- 基本类型:number、string、boolean、null、undefined
- 引用类型:Object(Object,Array,Date,RegExp,Function)
关于null和undefined的区别
- null表示"没有对象",即该处不应该有值。(转为数值为‘O’,例 :5 + null // 5)
- undefined表示"缺少值",就是此处应该有一个值,但是还没有定义。(转为数值为‘NaN’,例:5 + undefined // NaN)
关于两种数据类型的存储区别
基本类型的数据是存放在栈内存中的,而引用类型的数据是存放在堆内存中的。
下面列举两个例子来理解:
1.基本类型的数据存储:(实质上存储形式只是变量的标识符以及变量的值)
var a = 'A';
2.引用类型的数据存储:(实质上存储形式只是变量的标识符以及对象在堆内存中的存储地址)
var a = {name:“jack”};
关于两种数据类型的复制区别
1. 基本类型 的复制:当你在复制基本类型的时候,相当于把值也一并复制给了新的变量。看如下例子:
var a = 1;
var b = a;
console.log(a === b); // true
var a = 2;
console.log(a); // 2
console.log(b); // 1
上述代码执行结果用图表方式理解如下:
2. 引用类型 的复制:当你在复制引用类型的时候,实际上只是复制了指向堆内存的地址,即原来的变量与复制的新变量指向了同一个东西。 看如下例子:
var a = {name:"jack",age:20};
var b = a;
console.log(a === b); // true
a.age = 30;
console.log(a); // {name:"jack",age:30};
console.log(b); // {name:"jack",age:30};
上述代码执行结果用图表方式理解如下:
通过上述对引用类型数据“浅复制”的例子,我们将下图“a复制给b”实现效果称为‘深复制’:
那么如何实现深复制呢?我的总结如下:
1 》》利用 递归 来实现,对属性中所有引用类型的值,遍历到是基本类型的值为止。
function deepClone(source){
if(!source && typeof source !== 'object'){
throw new Error('error arguments', 'shallowClone');
}
var targetObj = Array.isArray(source) ? [] : {};
for(var keys in source){
if(source.hasOwnProperty(keys)){
if(source[keys] && typeof source[keys] === 'object'){
targetObj[keys] = deepClone(source[keys]); //递归
}else{
targetObj[keys] = source[keys];
}
}
}
return targetObj;
}
//检测深复制代码
var a = {name:"jack",age:20};
var b = deepClone(a);
console.log(a === b); // false
a.age = 30;
console.log(a); // {name:"jack",age:30};
console.log(b); // {name:"jack",age:20};
2 》》通过JSON 对象中的 stringify 把一个 js 对象序列化为一个 JSON 字符串,然后用parse 可以把 JSON 字符串反序列化为一个 js 对象也可实现深复制。举个例子:
var a = {name: 'hjs',friends: {name:['ali','qq','baidu']}};
var b = JSON.parse(JSON.stringify(a);
// b 是 a 的深拷贝
3 》》Array 的 slice 和 concat 方法
var arr1 = ["1","2","3"];
var arr2 = arr1.slice(0);
或 var arr2 = arr1.concact()
// arr2 是 arr1 的深拷贝
其实这里有个误区,slice和concat方法并不是严格意义上的深复制,举个例子你就理解了:
var a = [[1,2,3],4,5];
var b = a.slice();
或 var b = a.concat();
console.log(a === b); // false
a[0][0] = 6;
console.log(a); // [[6,2,3],4,5]
console.log(b); // [[6,2,3],4,5]
细心的你会发现,a中数组内的值发生改变,b也随之改变。其实对此我的总结如下:
通过Array的slice和concat方法进行复制,只会对第一层级的属性进行所谓深复制,并不是真正意义上的深复制。
4 》》通过第三方库的实现
jQuery —— $.extend(true, {}, ...)
var x = {
a: 1,
b: { f: { g: 1 } },
c: [ 1, 2, 3 ]
};
var y = $.extend({}, x), //浅复制
z = $.extend(true, {}, x); //深复制
y.b.f === x.b.f // true
z.b.f === x.b.f // false
lodash —— _.clone() / _.cloneDeep()
_.clone(obj, true)
等价于_.cloneDeep(obj)
var $ = require("jquery"),
_ = require("lodash");
var arr = new Int16Array(5),
obj = { a: arr },
obj2;
arr[0] = 5;
arr[1] = 6;
// 1. jQuery
obj2 = $.extend(true, {}, obj);
console.log(obj2.a); // [5, 6, 0, 0, 0]
Object.prototype.toString.call(obj2); // [object Int16Array]
obj2.a[0] = 100;
console.log(obj); // [100, 6, 0, 0, 0]
//此处jQuery不能正确处理Int16Array的深复制!!!
// 2. lodash
obj2 = _.cloneDeep(obj);
console.log(obj2.a); // [5, 6, 0, 0, 0]
Object.prototype.toString.call(arr2); // [object Int16Array]
obj2.a[0] = 100;
console.log(obj); // [5, 6, 0, 0, 0]
通过上面代码很好理解lodash库是相较于jQuery更好的更全面的处理深复制的第三方库。