关于JS中的深拷贝与浅拷贝

319 阅读2分钟

我们都知道对象类型在赋值过程中其实是复制了地址,从而导致了改变了一方其它也都被改变的情况。通常在开发中我们不希望出现这样的问题,我们可以通过浅拷贝来解决这个情况。

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

浅拷贝

首先可以通过 Object.assign 来解决这个问题,但 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 };
let c = Object.assign({}, a);

a.jobs.first = 'native';

console.log(b.jobs.first); // native
console.log(c.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);

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

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

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

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

在通常情况下,复杂的数据都是可以序列化的,所以这个函数可以解决大部分问题。

如果需要拷贝的对象含有内置类型并且不包含函数,可以使用 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: 2
  }
};

obj.b.d = obj.b;

// 注意该方法是异步的
// 可以处理 undefined 和循环引用对象
const test = async () => {
  const clone = await structuralClone(obj)
  console.log(clone)
}; 
test();

手动实现简易版深拷贝

当然你可能想自己来实现一个深拷贝,但是其实实现一个深拷贝是很困难的,需要我们考虑好多种边界情况,比如原型链如何处理、DOM 如何处理等等,所以这里实现的深拷贝只是简易版

function deepClone(obj) {
  function isObject(o) {
    return (typeof o === 'object' || typeof o === 'function') && o !== null;
  };

  if(!isObject(obj)) {
    throw new Error('非对象')
  };

  let isArray = Array.isArray(obj);
  let newObj = isArray ? [...obj] : {...obj};
  Reflect.ownKeys(newObj).forEach(key => {
    newObj[key] = isObject(obj[key]) ? deepClone(obj[key]) : obj[key]
  })

  return newObj;

};

let objs = {
  a: [1, 2, 3],
  b: {
    c: 2,
    d: 3
  }
};
let newObj = deepClone(objs);
newObj.b.c = 1;
console.log(objs.b.c);  // 2

---END---