Skip to content

JS 基础:深浅拷贝

深浅拷贝的区分

深浅拷贝,只针对复杂数据类型来说的。

浅拷贝 (ShallowCopy)

是一个对象的逐位副本。创建一个新对象,该对象具有原始对象中的精确副本。如果对象的任何字段是对其他对象的引用,则只复制引用地址,即只复制内存地址,而不复制对象本身,新旧对象还是共享同一块堆内存。改变其中一个对象,另一个也会受影响。如果有修改,会失去原始数据。

深拷贝 (DeepCopy)

复制出一个全新的对象实例,新对象跟原对象不共享内存,两者操作互不影响。

深拷贝是一个很复杂的问题,边缘 case 太多,比如环引原生 DOM/BOM对象RegExpDate包装类Number,String,Boolean函数原型链不可枚举的属性set/map/weakset/weakmapSymbol等,很多深拷贝如函数RegExp等在实际运用中都没有多大意义,如果在开发中遇到需要深拷贝的地方,首先考虑代码设计问题,它面对的问题往往可以用更优雅的方式解决。

浅拷贝方法

直接赋值

javascript
var o1 = { a: 1, b: 2 };
var o2 = o1;
var a = 1;
var b = a;
o1.a = 2;
a = 2;
console.log(o2.a); // 2
console.log(b === a); // false

Array.concat()

javascript
var o1 = [1, [2], 3];
var o2 = o1.concat(); // 这里会返回一个o1对象的浅拷贝对象
console.log(o2); //  [1, [2], 3]
console.log(o1 === o2); // false

Array.slice()

javascript
var o1 = [1, [2], 3];
var o2 = o1.slice(0);
console.log(o2); // [1, [2], 3]
console.log(o1 === o2); // false

Object.assign()

Object.assign() 方法用于将所有可枚举的自有属性的值从一个或多个源对象复制到目标对象。

javascript
var o1 = { a: 1, b: { c: 2, d: 3 } };
var o2 = Object.assign({}, o1);
console.log(o1); // { a : 1, b : { c : 2, d : 3} }
console.log(o2); // { a : 1, b : { c : 2, d : 3} }
console.log(o2 === o1); // false    说明实现了浅拷贝

深拷贝方法

手动拷贝

将每个引用对象都通过复制值来实现深拷贝。

javascript
var o1 = { a: 1, b: 2 };
var o2 = { a: o1.a, b: o1.b };
console.log(o2 === o1); // false
o1.a = 2;
console.log(o1); // {a: 2, b: 2}
console.log(o2); // {a: 1, b: 2}

该方法只适合简单的对象,并且没有引用的属性,适用范围很窄。

JSON.parse(JSON.stringify()) (常用)

javascript
var o1 = { a: 1, b: { c: 2 } };
var o2 = JSON.parse(JSON.stringify(o1));
console.log(o1 === o2); // false
console.log(o1.b === o2.b); // false
o1.b.c = 22;
o1.a = 11;
console.log(o1); //   { a : 11, b : { c : 22} }
console.log(o2); //   { a : 1, b : { c : 2} }
  • 该方法只能深拷贝对象和数组,内部递归实现,毕竟 JSON 的两个方法本身就只是用来转换 js 内的对象为 JSON 格式
  • Set 类型、Map 类型以及 Buffer 类型会被转换成 {}
  • undefined、任意的函数以及 symbol 值,在序列化过程中会被忽略(出现在非数组对象的属性值中时)或者被转换成 null(出现在数组中时)
  • 对包含循环引用的对象(对象之间相互引用,形成无限循环)执行此方法,会抛出错误
  • 所有以 symbol 为属性键的属性都会被完全忽略掉,即便 replacer 参数中强制指定包含了它们
  • 不可枚举的属性会被忽略

迭代递归法(常用)

javascript
function isObject(x) {
  return Object.prototype.toString.call(x) === '[object Object]';
}

function deepClone(obj) {
  if (!isObject(obj)) {
    throw new Error('obj 不是一个对象!');
  }

  let isArray = Array.isArray(obj);
  let cloneObj = isArray ? [] : {};
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      cloneObj[key] = isObject(obj[key]) ? deepClone(obj[key]) : obj[key];
    }
  }
  return cloneObj;
}

该方法存在的问题:

  • 适合一般 对象数组 的拷贝
  • 层级太深时容易爆栈
  • 未考虑 func,date,reg,err,Map,Set,Symbol,原型链,不可枚举case
  • 未考虑循环引用

循环法

javascript
// 保持引用关系
function cloneForce(x) {
  // =============
  const uniqueList = []; // 用来去重
  // =============

  let root = {};

  // 循环数组
  const loopList = [
    {
      parent: root,
      key: undefined,
      data: x,
    },
  ];

  while (loopList.length) {
    // 广度优先
    const node = loopList.pop();
    const parent = node.parent;
    const key = node.key;
    const data = node.data;

    // 初始化赋值目标,key为undefined则拷贝到父元素,否则拷贝到子元素
    let res = parent;
    if (typeof key !== 'undefined') {
      res = parent[key] = {};
    }

    // =============
    // 数据已经存在
    let uniqueData = find(uniqueList, data);
    if (uniqueData) {
      parent[key] = uniqueData.target;
      continue; // 中断本次循环
    }

    // 数据不存在
    // 保存源数据,在拷贝数据中对应的引用
    uniqueList.push({
      source: data,
      target: res,
    });
    // =============

    for (let k in data) {
      if (data.hasOwnProperty(k)) {
        if (typeof data[k] === 'object') {
          // 下一次循环
          loopList.push({
            parent: res,
            key: k,
            data: data[k],
          });
        } else {
          res[k] = data[k];
        }
      }
    }
  }

  return root;
}

function find(arr, item) {
  for (let i = 0; i < arr.length; i++) {
    if (arr[i].source === item) {
      return arr[i];
    }
  }

  return null;
}
  • 该方法未采用递归,采用循环的方式遍历,不会爆栈。
  • 解决了循环引用的问题。
  • 未考虑 func,date,reg,err,Map,Set,Symbol,原型链,不可枚举case

结构化克隆方法

结构化算法嗦支持的拷贝类型见文档

postMessage()

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

特点:异步、兼容性 ok

Notification API

javascript
function structuralClone(obj) {
  return new Notification('', {data: obj, silent: true}).data;
}

const obj = /* ... */;
const clone = structuralClone(obj);

特点:简洁、兼容性不佳(safari 全系列不支持)

History API

javascript
function structuralClone(obj) {
  const oldState = history.state;
  history.replaceState(obj, document.title);
  const copy = history.state;
  history.replaceState(oldState, document.title);
  return copy;
}

const obj = /* ... */;
const clone = structuralClone(obj);

特点:SafarireplaceState 调用的限制数量为 30 秒内 100 次。

深拷贝中其他类型的拷贝

拷贝 Symbol

javascript
//接 迭代递归法
let symKeys = Object.getOwnPropertySymbols(obj);
if (symKeys.length > 0) {
  symKeys.forEach((symKey) => {
    cloneObj[symKey] = isObject(obj[symKey]) ? deepClone(obj[symKey]) : obj[symKey];
  });
}

拷贝原型上的属性

javascript
let cloneObj = Object.create(Object.getPrototypeOf(obj));

拷贝不可枚举的属性

javascript
let cloneObj = Object.create(Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors());

拷贝 Map,Set

拷贝原始值和包装类

等号直接赋值

valueOf()

拷贝 Date 对象

javascript
new Date(new Date().valueOf());

拷贝正则

javascript
const reFlags = /\w*$/;
function cloneRegExp(regexp) {
  // 返回当前匹配的文本
  const result = new regexp.constructor(regexp.source, reFlags.exec(regexp));
  // 下一次匹配的起始索引
  result.lastIndex = regexp.lastIndex;
  return result;
}

拷贝函数,Err

javascript
cloneObj = eval(obj[k].toString());
//或者
cloneObj = obj[k].bind();

拷贝 Map,WeakMap,Set,WeakSet

javascript
//采用遍历for...of 或者 forEach
//如果是Map,WeakMap
let s = new Set([1, 2, 3]);
let newS = new Set();
s.forEach((item, index) => {
  newS.add(item);
});

//如果是Set,WeakSet
let m = new Map([
  [1, 'x'],
  [2, 'y'],
  [3, 'z'],
]);
let newM = new Map();
m.forEach((value, key) => {
  newM.set(key, value);
});

更多参考 lodash

链接