# 赋值与浅拷贝、深拷贝
首先不要把引用类型的赋值(=)归结为浅拷贝,深拷贝和浅拷贝只针对像 Object, Array 这样的复杂对象的。简单来说,浅拷贝只拷贝一层对象的属性,而深拷贝则递归拷贝了所有层级。
# js数据类型
基本类型为Number、String、Boolean、Undefined、Null,引用类型为Object。
基本类型是储存存放在栈内存中的简单数据段,数据大小确定,内存空间大小可以分配,是直接按值存放的,所以可以直接访问。
引用类型存放在堆内存中,它的变量实际是存放在栈中的一个地址(指针)指向堆中的实际内存地址。
# 区分赋值、浅拷贝与深拷贝
赋值
基本类型赋值(=)直接在栈中开辟新内存,把值赋值到新内存中,而引用类型的赋值(=)是直接复制栈内存中的地址(指针),并没有在堆内存总开辟新的内存,因此他们都指向同一个堆内存,任何操作都会影响原来的数据。
let obj2 = obj1;
浅拷贝与深拷贝
首先不要把赋值和浅拷贝和深拷贝混在一起。
浅拷贝和深拷贝都在堆内存中开辟新的内存,都进行了复制。
浅拷贝就是进行简单的复制,只会将对象的各个属性进行依次复制,不会递归复制,所以那些子属性如果再是引用类型(对象、数组、函数)的话,就只复制了他们的地址(指针),实际的内存地址没有被复制。
所以就理解为什么数组方法slice、concat、扩展运算符、Object.assign()这些都是浅拷贝了。它们都进行了简单的拷贝,没有再对深层次的数据进行拷贝。
深拷贝是对对象以及对象的所有子对象进行拷贝。
# 实现浅拷贝
1、常见的浅拷贝方法
扩展运算符(...);
Object.assign();
Array.prototype.concat();
Array.prototype.slice();
lodash(_.clone());
2、简单封装一个
function clone(target) {
var cloneTarget = {};
for(var key in target) {
cloneTarget[key] = target[key]
}
return cloneTarget;
}
# 实现深拷贝
1、JSON.parse(JSON.stringify());
虽然用起来很方便,但是,只适合一些简单的情景(Number, String, Boolean, Array, Object),扁平对象,那些能够被 json 直接表示的数据结构。function对象,RegExp对象是无法通过这种方式深拷贝。
let obj2=JSON.parse(JSON.stringify(obj1));
2、递归赋值
实习深拷贝,其实思路和浅拷贝一样,如果是基础类型就直接赋值,如果是引用类型,就循环遍历复制每一个子元素,子元素如果再是再接着遍历复制。就是用了递归思想。
简单实现一个对象递归复制:
function deepClone(target) {
if (typeof target === 'object') {
var cloneTarget = {};
for(var key in target) {
cloneTarget[key] = deepClone(target[key]);
}
return cloneTarget;
} else {
return target; // 不是引用类型的话直接返回,省去循环
}
}
// 另一种方式,这种方式没有考虑基本类型
function deepClone(target) {
var cloneTarget = {};
for(var key in target) {
if (target[key] && typeof target[key] === 'object') {
cloneTarget[key] = deepClone(target[key]);
} else {
cloneTarget[key] = target[key];
}
}
return cloneTarget;
}
兼容数组:
function deepClone(target) {
if (typeof target === 'object') {
var cloneTarget = Array.isArray(target) ? [] : {};
for(var key in target) {
cloneTarget[key] = deepClone(target[key]);
}
return cloneTarget;
} else {
return target; // 不是引用类型的话直接返回,省去循环
}
}
到这一个基本的深拷贝就已经实现了,如果想继续深入,可以往下看。
循环引用:
当一个对象的子元素的值是自身时就会被无限循环,陷入死循环。
var target = {
field1: 1,
field2: undefined,
field3: {
child: 'child'
},
field4: [2, 4, 8]
};
target.target = target;
解决办法:额外开辟一个空间,来保存已经拷贝过的对象,当前对象需要拷贝时,去这个空间查找有没有拷贝过这个对象,有的话直接返回,没有继续拷贝。
这个存储空间,需要可以存储key-value形式的数据,且key可以是一个引用类型,我们可以选择Map这种数据结构:
- 检查map中有无克隆过的对象
- 有 - 直接返回
- 没有 - 将当前对象作为key,克隆对象作为value进行存储
- 继续克隆
function deepClone(target, map = new Map()) {
if (typeof target === 'object') {
var cloneTarget = Array.isArray(target) ? [] : {};
if (map.get(target)) return map.get(target);
map.set(target, cloneTarget);
for (var key in target) {
cloneTarget[key] = deepClone(target[key], map);
}
return cloneTarget;
} else {
return target;
}
}
优点: 使用WeakMap
代替map
, WeakMap
是弱引用,其键必须是对象,值是任意的。我们使用map是强引用,设置的map值会一直存储在内存中,不会自动清除。所以如果对象特别大的话使用弱引用的好处就体现了,js垃圾回收机制会自动帮我们回收,而不需要我们手动清除。
function deepClone(target, map = new WeakMap()) {
// ...
};
兼容其他数据类型:日期/正则等特殊对象
// 判断是否是对象
// function isObject(target) {
// return (typeof target === 'object' || typeof target === 'function') && target !== null;
// }
// function deepClone(target, map = new Map()) {
// // 先判断该引用类型是否被 拷贝过
// if (map.get(target)) {
// return target;
// }
// // 获取当前值的构造函数:获取它的类型
// var Ctor = target.constructor;
// // 检测当前对象target是否与 正则、日期格式对象匹配
// if (/^(RegExp|Date)$/i.test(Ctor.name)) {
// return new Ctor(target); // 创建一个新的特殊对象(正则类/日期类)的实例
// }
// if (isObject(target)) {
// map.set(target, true); // 为循环引用的对象做标记
// var cloneTarget = Array.isArray(target) ? [] : {};
// for (var key in target) {
// if (target.hasOwnProperty(key)) {
// cloneTarget[key] = deepClone(target[key], map);
// }
// }
// return cloneTarget;
// } else {
// return target;
// }
// }
参考链接: javascript中的深拷贝和浅拷贝 (opens new window)
js 深拷贝 vs 浅拷贝 (opens new window)