深拷贝与浅拷贝

浅拷贝

浅拷贝就是拷贝第一层的基本类型值,以及第一层的引用类型地址。
如果属性是引用类型,拷贝的就是内存地址 ,所以其中一个对象改变了这个地址,就会影响到另一个对象。
即 浅拷贝只拷贝一层 ,而深拷贝是拷贝多层

  • Object.assign()
    用于将所有可枚举属性的值从源对象复制到目标对象。它将返回目标对象。

  • 展开语法 Spread

  • Array.prototype.slice()
    返回一个新的数组对象,这一对象是一个由 begin和 end(不包括end)决定的原数组的浅拷贝。原始数组不会被改变。

深拷贝

深拷贝是创建一个新的对象,将一个对象从内存中完整地拷贝出来一份给新对象,开辟一个新的区域存放新对象,且新对象的修改并不会改变原对象,二者实现真正分离。

一、 对象的深拷贝

序列化反序列化法

JSON.parse(JSON.stringify());
缺点:
number,string,array类似这样的能被json表示的数据类型,可以正常拷贝;
而函数、Date等, 这种不能被 json 表示的类型,将不能被正确处理。
undefined、symbol 和函数这三种情况,会直接忽略;
循环引用情况下,会报错;
不能正确处理new Date() 和 正则 ;

  • tips:
    但是!在工作中,这个深拷贝满足需求,最常使用。因为符合业务逻辑中的数据结构。
    在复杂的深拷贝需求时,通常使用lodash库中的cloneDeep()更加稳定。
    lodash 除了cloneDeep()常用之外,
    还有chunk()用来将一维数组按照一定的规则变成二维数组,
    debounce(),throttle(),
    minBy()按照传入的属性找到属性值最小的对象

js实现深拷贝

思路

迭代递归法:

可以拆分成 2 步,浅拷贝 + 递归:
浅拷贝时判断属性值是否是对象,如果是对象就进行递归操作,两个一结合就实现了深拷贝。

考虑数组:

使用 typeof 来兼容数组

循环引用问题:

设置一个哈希表存储已拷贝过的对象,当检测到当前对象已存在于表中时,取出该值并返回即可。
可用数据结构WeakMap,以及它的方法has(),get(),set()来实现。

WeakMap vs Map
如果想要让垃圾回收器回收某一对象,就将对象的引用直接设置为 null
但如果一个对象被多次引用时,
例如作为另一对象的键、值或子元素时,
将该对象引用设置为 null,该对象也是不会被回收的,依然存在。
ES6 推出了: WeakMap
它对于值的引用都是不计入垃圾回收机制的,这是弱引用
弱引用是指当该对象应该被GC回收时不会阻止GC的回收行为

Map 相对于 WeakMap :

Map 的键可以是任意类型,WeakMap 只接受对象作为键(null除外),不接受其他类型的值作为键
Map 的键实际上是跟内存地址绑定的,只要内存地址不一样,就视为两个键; WeakMap 的键是弱引用,键所指向的对象可以被垃圾回收,此时键是无效的
Map 可以被遍历, WeakMap 不能被遍历

拷贝Symbol:

Symbol 在 ES6 下才有,可以利用Object.getOwnPropertySymbols,查找有没有 Symbol 属性,如果查找到则先forEach遍历处理 Symbol 情况,然后再处理正常情况

代码实现

首先需要一个函数,对传入参数进行校验
没有对传入参数进行校验

1
2
3
4
5
6
7

function isObject(obj) {
// typeof null => object
// typeof [] => object
return typeof obj === 'object' && obj != null;
}

深拷贝代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24


function cloneDeep(source, hash = new WeakMap()) {

if (!isObject(source)) return source;
// 查哈希表,如果已存在于哈希表中时,取出该值并返回即可
if (hash.has(source)) return hash.get(source);

let target = Array.isArray(source) ? [] : {};
hash.set(source, target); // 哈希表设值

// Reflect.ownKeys() 获取所有的键值,同时包括 Symbol,对 source 遍历赋值即可
Reflect.ownKeys(source).forEach(key => {
if (isObject(source[key])) {
// 若是对象则再次调用该函数递归。 注意:需要传入哈希表,以便之后的查询、设值
target[key] = cloneDeep(source[key], hash);
} else {
//如果是基本类型,则直接赋值
target[key] = source[key];
}
});
return target;
}

注意问题

  • for…in 会追踪原型链上的属性

  • 需要拷贝不可枚举的属性
    比如需要拷贝类似属性描述符,setters 以及 getters 这样不可枚举的属性。这就需要一个额外的不可枚举的属性集合来存储它们。

  • Object.create() 的第二个参数
    除了对象的原型,Object.create方法还可以接受第二个参数。
    该参数是一个属性描述对象,它所描述的对象属性,会添加到实例对象,作为该对象自身的属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

function cloneDeep(source, hash = new WeakMap()) {
if (!isObject(source)) return source;
// 查表,防止循环拷贝
if (hash.has(source)) return hash.get(source)

let isArray = Array.isArray(source)
// 初始化拷贝对象
let cloneObj = isArray ? [] : {}
// 哈希表设值
hash.set(source, cloneObj)
// 获取源对象所有属性描述符
let allDesc = Object.getOwnPropertyDescriptors(source)
// 获取源对象所有的 Symbol 类型键
let symKeys = Object.getOwnPropertySymbols(source)
// 拷贝 Symbol 类型键对应的属性
if (symKeys.length > 0) {
symKeys.forEach(symKey => {
cloneObj[symKey] = isObject(source[symKey]) ? cloneDeep(source[symKey], hash) : source[symKey]
})
}

// 拷贝不可枚举属性
cloneObj = Object.create(
Object.getPrototypeOf(source),
allDesc
)
// 拷贝可枚举属性(包括原型链上的)
for (let key in source) {
cloneObj[key] = isObject(source[key]) ? cloneDeep(source[key], hash) : source[key];
}

return cloneObj
}

一些特殊类型的数据需要特殊照顾

  • Date和RegExp对象类型
    日期 或 正则对象,直接构造一个新对象返回
    if([Date,RegExp].includes(source.constructor)) return new source.constructor(source);

  • 函数对象
    if(typeof source == 'function') return new Function('return' + source.toString())();

  • Map对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

if(source instanceof Map) {
const result = new Map();
source.foeEach((value,key)=>{
// map中的值若是Object, 需要递归进行深拷贝
if(isObject(value)) {
result.set(key,cloneDeep(value));

} else {
result.set(key,value);
}
})

return result;
}

  • Set对象

if(source instanceof Set) {
    const result = new Set();
    source.forEach((value)=>{
        if(isObject(value)) {
            result.add(cloneDeep(value));
        } else {
            result.add(value);
        }
    });

    return result;
}









查看评论