「前端基础」深入研究Javascript深拷贝函数

168 阅读4分钟

前言

无论是面试中还是实际工作中,遇到深浅拷贝的概率都挺大的。
由于浅拷贝比较简单,本文中就不提了,主要聊一聊深拷贝。
首先列举出深拷贝常见的几种方式:

  • 迭代法
  • 序列化和反序列化
  • Object.create()

1. 简单对象的深拷贝

对于数据类型简单的对象,如:

const obj = {
    a: {
        b: 1,
        c: 2
    },
    d: [3, 4]
}

1.1 for in实现

我们用简单的for in递归就可以实现此对象的深拷贝

function deepClone(payload) {
    // payload 不是对象则不执行
    if (typeof payload !== 'object') return;
    
    const result = Array.isArray(payload) ? [] : {};
    
    for (let key in payload) {
        result[key] = typeof payload[key] === 'object' ? deepClone(payload[key]) : payload[key];
    }
    return result;
}
let obj2 = deepClone(obj);
obj.a.b = 2;
obj.d.push(10);

console.log(obj2.a.b);  // 1
console.log(obj2.d);  // [3, 4]

这样 obj2 就是拷贝出的一个新对象而不受 obj 中值发生改变的影响。

1.2 Reflect实现

function deepClone2(obj) {
    // obj 不是对象则不执行
    if (typeof obj !== 'object') return;
    
    let res = Array.isArray(obj) ? [...obj] : { ...obj }
    Reflect.ownKeys(res).forEach( key => {
        res[key] = typeof obj[key] === 'object' ? deepClone2(obj[key]) : obj[key];
    })
    return res;
}
let obj2 = deepClone2(obj);
obj.a.b = 2;
obj.d.push(10);

console.log(obj2.a.b);  // 1
console.log(obj2.d);  // [3, 4]

这个方法使用了Reflect.ownKeys方法遍历后递归拷贝,我们发现结果和for in的方法得到的结果一样,那两者的区别是什么我们后面再说。

1.3 JSON.parse实现

function deepClone3(payload) {
    return JSON.parse(JSON.stringify(payload))
}

使用这种方法的问题在于以下几点

  • 会忽略 undefined
  • 会忽略 symbol
  • 不能序列化函数
  • 不能解决循环引用的对象
  • 会抛弃对象的constructor

也就是这种方式的深拷贝,只能处理那些能用json表示的对象,而且拷贝后,不管这个对象原来的构造函数是什么,都会变成Object类型。

以上就是对于简单对象深拷贝的实现,上述三种方法可以满足一些简单场景

2. 复杂对象的深拷贝

对于对象中只存在数组、Object类型的数据,可以使用上述简单的方式处理,但是如果对象属性中有方法或者其他类型的对象,应该考虑更多边界情况

// 实现如下对象的深拷贝
{
    a: {
        name: '我是一个对象',
        id: 1
    },
    arr: [0, 1, 2],
    fn: () => {
        console.log('我是一个函数')
    },
    date: new Date(),
    reg: new RegExp('/我是一个正则/ig'),
    err: new Error('我是一个错误'),
    [Symbol()]: 'symbol'
}

对于这样的一个对象,如果运用上面的三种方法,我们发现用前两种方法,得到的结果分别是这样的

// for in 方法
{
  a: { name: '我是一个对象', id: 1 },
  arr: [ 0, 1, 2 ],
  fn: [Function: fn],
  date: {},
  reg: {},
  err: {}
}
// Reflect
{
  a: { name: '我是一个对象', id: 1 },
  arr: [ 0, 1, 2 ],
  fn: [Function: fn],
  date: {},
  reg: { lastIndex: 0 },
  err: {
    stack: 'Error: 我是一个错误...',
    message: '我是一个错误'
  },
  [Symbol()]: 'symbol'
}

可以看到两者的区别在于Reflect可以遍历到Symbol,并且对RegexError有值拷贝效果。而用序列化的方法拷贝的话,结果是数组和Object以外的其他对象都会失真,date只剩一个字符串了。

因此需要基于Reflect并处理各边界条件。

2.1 解决function类型

首先对于function类型的值,我们可以先把它转换为字符串,再使用eval方法。
改进后的代码:

function deepClone3(payload) {
    if (typeof payload === 'function') return eval(payload.toString());

    // obj 不是对象则直接返回
    if (typeof payload !== 'object') return payload;
    
    let res = Array.isArray(payload) ? [] : {};
    
    Reflect.ownKeys(payload).forEach( key => {
        res[key] = deepClone3(payload[key]);
    })
    return res;
}

2.2 解决各构造函数

根据原数据可能出现的数据结构有: BooleanDateNumberStringRegExp及其他自定义类。
对于这些数据采用获取构造函数,在进行实例化的方式实现。

const constructor = payload.constructor;
switch(constructor) {
    case String:
    case Number:
    case Boolean:
    case Date:
    case RegExp: return new constructor(payload);
    default: res = new constructor();
}

至此,对于本章开始提到的数据可以实现完整的深拷贝过程。但是,当数据中出现循环引用的值,如obj.o = obj,还需要做最后一步处理。

2.3 解决循环引用的问题

针对循环引用的情况,递归过程容易出现栈溢出,因此采用WeakMap数据结构存入key,当WeakMap内已存在当前key则直接返回数据,避免陷入死循环。

完整实现深拷贝的代码如下:

function deepClone3(payload, hash = new WeakMap()) {
    console.log(payload);
    // 解决循环引用问题
    if (hash.has(payload)) return hash.get(payload);

    // 解决function类型
    if (typeof payload === 'function') return eval(payload.toString());

    // 基本数据类型直接返回
    if (typeof payload !== 'object') return payload;
    
    let res;
    const constructor = payload.constructor;
    switch(constructor) {
        case String:
        case Number:
        case Boolean:
        case Date:
        case RegExp: return new constructor(payload);
        default: 
            res = new constructor();
            // 存入weakMap
            hash.set(payload, res);
    }
    
    Reflect.ownKeys(payload).forEach( key => {
        res[key] = deepClone3(payload[key], hash);
    })
    return res;
}

3. 总结

本文根据现有数据类型,基于Reflect增加各情况判断,基本完整的实现了深拷贝函数。可能还有些没有考虑到的情况,如果小伙伴们有其他测试用例会导致此函数报错的,欢迎及时指正。