JavaScript 中对象的多种克隆方式以及注意事项

克隆的意义和常见场景

  • 意义:保证原数据的完整性和独立性
  • 常用场景:复制数据、函数入参、clss 构造函数等等

克隆

浅克隆

  • 只克隆对象的第一层级
  • 如果属性值是原始数据类型,拷贝其值,也就是常说的值拷贝
  • 如果属性值是引用类型,拷贝其内存地址,也就是常说的引用拷贝

常用的浅克隆

  • ES6 拓展运算符
  • Object.assign
  • for in

看个使用扩展运算符实现克隆的例子:

const person = {
  name: 'Jae',
  getName: function() {
    return this.name;
  },
  address: {
    province: '福建'
  }
}
const person2 = {...person};
person2.nae = 'Tom';
person2.getName = function() {
  return 'person2' + this.name
}
person2.address.province = '广东'

console.log("person.name: ", person.name); // Jae, 正常
console.log("person.getName: ", person.getName()); // Jae, 正常
console.log("person.address.province: ", person.address.province); // 广东, 受到影响

数组常用的浅拷贝

  • ES6 拓展运算符
  • slice
  • [].concat

同样看个使用拓展运算符实现数组拷贝的例子:

const arr = [1, 2, 3];

const arr2 = [...arr];
const arr3 = arr.slice(0);
const arr4 = [].concat(arr);

console.log("arr: ", arr); // [1, 2, 3]
console.log("arr2: ", arr2, arr2 == arr); // [1, 2, 3] false, 完美克隆
console.log("arr3: ", arr3, arr3 == arr); // [1, 2, 3] false, 完美克隆
console.log("arr4: ", arr4, arr4 == arr); // [1, 2, 3] false, 完美克隆

深度克隆

  • 克隆对象的每个层级
  • 如果属性值是原始数据类型,拷贝其值,也就是常说的值拷贝
  • 如果属性值是引用类型,递归克隆

下面来看具体的几种深度克隆方法

JSON.stringify() + JSON.parse()

JSON.parse(JSON.stringify(obj));
  • 优点:纯天然,无污染
  • 局限性:
  1. 只能复制普通键的属性,Symbol类型无能为力
  2. 循环引用对象,比如 Window 不能复制
  3. 函数、Date、RegExp、Blob 等类型不能赋值
  4. 性能差
  5. 可能出现溢出栈的情况

看一下代码示例:

const obj = {
  a: 1,
  b: {
    c: 2
  }
}
const date = { date: new Date() };
const reg = { reg: /[0 - 9]/ };
const bolb = { blob: new Blob(['123']) }
const func = { fun() {} }

console.log(JSON.parse(JSON.stringify(obj))); 
// {a:1,b:{c:2}} 完美
console.log(JSON.parse(JSON.stringify(date))); 
// {date: '2022-03-30T07:20:14.135Z'} 变成字符串了
console.log(JSON.parse(JSON.stringify(reg)));
// {reg: {}} 变成空对象了
console.log(JSON.parse(JSON.stringify(blob)));
// {blob: {}} 变成空对象了
console.log(JSON.parse(JSON.stringify(func)));
// {} 变成空对象了
console.log(JSON.parse(JSON.stringify(window)));
// Uncaught TypeError: Converting circular structure to JSON

消息通讯 - BroadcastChannel 等等

实际上是用浏览器内置的各种 API 进行通信,通信的过程中会对传递的参数进行复制,可以利用这个特点进行深度克隆

let chId = 0;
function clone(data) {
  chId ++;
  let cname = `__clone__${chId}`;
  let ch1 = new BroadcastChannel(cname);
  let ch2 = new BroadcastChannel(cname);
  return new Promise(resolve => {
    ch2.addEventListener('message', ev => resolve(ev.data), { once: true });
    ch1.postMessage(data);
  })
}
const obj = {
  a: 1,
  b: {
    c: 2
  },
  d: {
    date: new Date()
  },
  e: {
    reg: /[0 - 9]/
  }
};
clone(obj).then(res => console.log(res)); // 完美克隆 obj, 可以自己试下

const obj2 = { func() {} }
clone(obj2).then(res => console.log(res));
// Uncaught (in promise) DOMException: Failed to execute 'postMessage' on 'BroadcastChannel': func() {} could not be cloned

clone(window).then(res => console.log(res));
// Uncaught (in promise) DOMException: Failed to execute 'postMessage' on 'BroadcastChannel': #<Window> could not be cloned

可以看到,除了函数和 window 无法被克隆,对象都能完美克隆。

局限性:

  • 循环引用对象不能赋值,如 Windows
  • 函数不能赋值
  • 同步变成异步,需要用 Promise 进行包装

手写简单版深度克隆

function isObject(obj) {
    return obj !== null && typeof obj == "object";
}

function isArray(obj) {
    return Array.isArray(obj)
}

function hasOwn(obj, key) {
    return Object.prototype.hasOwnProperty.call(obj, key)
}

function deepClone(obj) {
  if (!isObject(obj)) return obj;
  let data;

  if (isArray(obj)) {
    data = [];
    for (let i = 0; i < obj.length; i++) {
      data[i] = deepClone(obj[i]);
    }
  } else if (isObject(obj)) {
    data = {};
    for (let key in obj) {
      if (hasOwn(obj, key)) {
        data[key] = deepClone(obj[key]);
      }
    }
  }
  return data;
}

可以自己去试一试,这里直接给结论:

  • 只能复制一般的数组和对象
  • 循环引用没有处理
  • 递归(可能爆栈)
  • 特殊类型未处理(函数、Date、RegExp、Blob 等)

循环引用的问题如何解决

从前面的例子中可以看出,循环引用是很难成功复制的,那么怎么解决这个问题呢?

这里使用 ES6 中的 WeakMap 来解决。

function isObject(obj) {
    return obj !== null && typeof obj == "object";
}

function isArray(obj) {
    return Array.isArray(obj)
}

function hasOwn(obj, key) {
    return Object.prototype.hasOwnProperty.call(obj, key)
}

function deepClone(obj) {
    const wmap = new WeakMap();
    wmap.set(obj, 1);
    function deepCloneInner() {
        if (!isObject(obj)) return obj;
        const data = isArray(obj) ? [] : {};
        for (let key in obj) {
            const val = obj[key];
            if (hasOwn(obj, key)) {
                // 原始数据类型
                if (!isObject(val)) {
                    data[key] = val
                    continue;
                }
                if (wmap.has(val)) {
                    continue;
                }
                wmap.set(val, 1);
                data[key] = deepCloneInner(val);
            }
        }
        return data;
    }
    return deepCloneInner(obj);
}

// 循环引用
let obj = {
  name: 'obj'
}
obj['obj2'] = obj;
console.log(deepClone(obj)); // {name: 'obj'} 完美复制

爆栈问题如何解决

问题是由于使用递归引起的,这里将递归用循环代替来实现:

  function hasOwnProp(obj, property) {
    return Object.prototype.hasOwnProperty.call(obj, property)
  }

  function getType(obj) {
    return Object.prototype.toString.call(obj).slice(8, -1).toLowerCase();
  }

  function isObject(obj) {
    return obj !== null && typeof obj == "object";
  }

  function isArray(obj) {
    return Array.isArray(obj)
  }

  function isCloneObject(obj) {
    return isObject(obj) || isArray(obj)
  }

  // 循环
  function cloneDeep(x) {
    // 先设置默认值
    let root = x;

    if (isArray(x)) {
      root = [];
    } else if (isObject(x)) {
      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] = isArray(data) ? [] : {};
      }

      if (isArray(data)) {
        for (let i = 0; i < data.length; i++) {
          // 避免一层死循环 a.b = a
          if (data[i] === data) {
            res[i] = res;
          } else if (isCloneObject(data[i])) { // 需要深度复制的属性值
            // 下一次循环, 入栈
            loopList.push({
              parent: res,
              key: i,
              data: data[i],
            });
          } else {
            res[i] = data[i];
          }
        }
      } else if (isObject(data)) {
        for (let k in data) {
          if (hasOwnProp(data, k)) {
            // 避免一层死循环 a.b = a
            if (data[k] === data) {
              res[k] = res;
            } else if (isCloneObject(data[k])) { // 需要深度复制的属性值
              // 下一次循环
              loopList.push({
                parent: res,
                key: k,
                data: data[k],
              });
            } else {
              res[k] = data[k];
            }
          }
        }
      }
    }
    return root;
  }

  function createData(deep) {
    var data = {};
    var temp = data;
    for (var i = 0; i < deep; i++) {
      temp = temp['data'] = {};
      temp[i + 1] = i + 1
    }
    return data;
  }

  const data = createData(10000);
  console.log(cloneDeep(data)); // 完美复制

特殊类型如何处理

通过构造函数识别来处理,直接上代码:

  function hasOwnProp(obj, property) {
    return Object.prototype.hasOwnProperty.call(obj, property)
  }

  function getType(obj) {
    return Object.prototype.toString.call(obj).slice(8, -1).toLowerCase();
  }

  function isObject(obj) {
    return obj !== null && typeof obj == "object";
  }

  function isArray(obj) {
    return Array.isArray(obj)
  }

  function isCloneObject(obj) {
    return isObject(obj) || isArray(obj)
  }

  function cloneDeep(x) {
    //使用WeakMap
    let uniqueData = new WeakMap();
    let root = x;

    if (isArray(x)) {
      root = [];
    } else if (isObject(x)) {
      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 source = node.data;

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

      // 复杂数据需要缓存操作
      if (isCloneObject(source)) {
        // 命中缓存,直接返回缓存数据
        let uniqueTarget = uniqueData.get(source);
        if (uniqueTarget) {
          parent[key] = uniqueTarget;
          continue; // 中断本次循环
        }

        // 未命中缓存,保存到缓存
        uniqueData.set(source, target);
      }

      if (isArray(source)) {
        for (let i = 0; i < source.length; i++) {
          if (isCloneObject(source[i])) {
            // 下一次循环
            loopList.push({
              parent: target,
              key: i,
              data: source[i],
            });
          } else {
            target[i] = source[i];
          }
        }
      } else if (isObject(source)) {
        for (let k in source) {
          if (hasOwnProp(source, k)) {
            if (isCloneObject(source[k])) {
              // 下一次循环
              loopList.push({
                parent: target,
                key: k,
                data: source[k],
              });
            } else {
              target[k] = source[k];
            }
          }
        }
      }
    }

    uniqueData = null;
    return root;
  }

  var obj = {
    p1: "p1",
    p2: ["p22", {
      p23: undefined,
      p24: 666
    }],
    null: null,
    p4: new RegExp(),
    p3: undefined,
    func: function () {
      console.log("func");
      return 1
    },
    Symbol: Symbol(2),
    bigint: BigInt(100),
  };
  obj.loop = obj;

  console.log(cloneDeep(obj)); // 完美复制

几种深度克隆方式的小结

深度克隆循环引用递归爆栈特殊类型
JSON×××
消息通讯××(大部分)
手写

浅克隆 VS 深度克隆

方式方法优点缺点备注
浅克隆性能高数据可能不完全独立一层属性全是值类型,等于深度克隆
深度克隆不影响原对象性能低,时间和空间消耗更大保持数据独立,让函数无副作用等

参考资料:https://segmentfault.com/a/1190000016672263

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

大杯美式不加糖

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值