编程实战

# 对象拷贝

let a = {
    age: 1
}
let b = a
a.age = 2
console.log(b.age) // 2

从上述例子中我们可以发现,如果给一个变量赋值一个对象,那么两者的值会是同一个引用,其中一方改变,另一方也会相应改变。

通常在开发中我们不希望出现这样的问题,我们可以使用浅拷贝来解决这个问题。

# 浅拷贝

首先可以通过 Object.assign 来解决这个问题。

let a = {
    age: 1
}
let b = Object.assign({}, a)
a.age = 2
console.log(b.age) // 1

当然我们也可以通过展开运算符 ... 来解决:

let a = {
    age: 1
}
let b = { ...a }
a.age = 2
console.log(b.age) // 1

通常浅拷贝就能解决大部分问题了,但是当我们遇到如下情况就需要使用到深拷贝了:

let a = {
    age: 1,
    jobs: {
        first: 'FE'
    }
}
let b = {...a}
a.jobs.first = 'native'
console.log(b.jobs.first) // native

浅拷贝只解决了第一层的问题,如果接下去的值中还有对象的话,那么就又回到刚开始的话题了,两者享有相同的引用。要解决这个问题,我们需要引入深拷贝。

# 深拷贝

这个问题通常可以通过 JSON.parse(JSON.stringify(object)) 来解决。

let a = {
    age: 1,
    jobs: {
        first: 'FE'
    }
}
let b = JSON.parse(JSON.stringify(a))
a.jobs.first = 'native'
console.log(b.jobs.first) // FE

但是该方法也是有局限性的:

  • 会忽略 undefined
  • 会忽略 symbol
  • 不能序列化函数
  • 不能解决循环引用的对象
let obj = {
  a: 1,
  b: {
    c: 2,
    d: 3,
  },
}
obj.c = obj.b
obj.e = obj.a
obj.b.c = obj.c
obj.b.d = obj.b
obj.b.e = obj.b.c
let newObj = JSON.parse(JSON.stringify(obj))
console.log(newObj)

如果你有这么一个循环引用对象,你会发现你不能通过该方法深拷贝

深拷贝 error

在遇到函数、 undefined 或者 symbol 的时候,该对象也不能正常的序列化

let a = {
    age: undefined,
    sex: Symbol('male'),
    jobs: function() {},
    name: 'yck'
}
let b = JSON.parse(JSON.stringify(a))
console.log(b) // {name: "yck"}

你会发现在上述情况中,该方法会忽略掉函数和 undefined

但是在通常情况下,复杂数据都是可以序列化的,所以这个函数可以解决大部分问题,并且该函数是内置函数中处理深拷贝性能最快的。当然如果你的数据中含有以上三种情况下,可以使用 lodash 的深拷贝函数 (opens new window)。同时,我们也可以尝试实现一个解决上述问题的深拷贝函数:

// 解法一
function deepClone (target, hash = new WeakMap()) { // 额外开辟一个存储空间 WeakMap 来存储当前对象
    // 如果是 null 就不进行拷贝操作
    if (target === null) return target;

    // 处理日期
    if (target instanceof Date) return new Date(target);

    // 处理正则
    if (target instanceof RegExp) return new RegExp(target);

    // 处理 DOM元素
    if (target instanceof HTMLElement) return target;

    // 处理原始类型和函数:不需要深拷贝,直接返回
    if (typeof target !== 'object') return target;

    // 是引用类型的话就要进行深拷贝
    // 当需要拷贝当前对象时,先去存储空间中找,如果有的话直接返回
    if (hash.get(target)) return hash.get(target);

    // 创建一个新的克隆对象或克隆数组
    const cloneTarget = new target.constructor();
    // 如果存储空间中没有就存进 hash 里
    hash.set(target, cloneTarget);

    // 引入 Reflect.ownKeys,处理 Symbol 作为键名的情况
    Reflect.ownKeys(target).forEach(key => {
        // 递归拷贝每一层
        cloneTarget[key] = deepClone(target[key], hash);
    });

    // 返回克隆的对象
    return cloneTarget;
}

// 解法二
// https://mp.weixin.qq.com/s/lCytacvU-EhlO0pxaBz6vA
function deepClone(obj, hash = new WeakMap()) {
    if (hash.has(obj)) {
        return obj;
    }
    let res = null;
    const reference = [Date, RegExp, Set, WeakSet, Map, WeakMap, Error];

    if (reference.includes(obj?.constructor)) {
        res = new obj.constructor(obj);
    } else if (Array.isArray(obj)) {
        res = [];
        obj.forEach((e, i) => {
            res[i] = deepClone(e);
        });
    } else if (typeof obj === "object" && obj !== null) {
        res = {};
        for (const key in obj) {
            if (Object.hasOwnProperty.call(obj, key)) {
                res[key] = deepClone(obj[key]);
            }
        }
        hash.set(obj, res);
    } else {
        res = obj;
    }
    return res;
}

如果你所需拷贝的对象含有内置类型并且不包含函数,可以使用 MessageChannel

function structuralClone(obj) {
    return new Promise(resolve => {
        const {port1, port2} = new MessageChannel();
        port2.onmessage = ev => resolve(ev.data);
        port1.postMessage(obj);
    });
}

var obj = {a: 1, b: {
    c: b
}};

// 注意该方法是异步的
// 可以处理 undefined 和循环引用对象
(async () => {
    const clone = await structuralClone(obj)
})();
上次更新: 2022/6/15 16:21:29