javaScript中的深拷贝解决方案

369 阅读3分钟

说深拷贝之前先说一下js中的几种判断类型的方式

  • 判断类型首选Object原型上的toString方法执行时传入当前要判断的数据
    // 不安全可以通过修改或者覆盖原型方法来改变结果
    // value.toString()无法获取类型,因为实例的toString方法是重写了原型的toString方法
    // 想要判断类型需要使用原型的toString方法
    const value = function(){};
    Object.prototype.toString.call(value).slice(8, -1).toLowerCase(); // function
  • 使用value.construtor.name属性来判断
    // 缺点:undefined和null需要单独处理
    const value = "123";
    value.constructor.name.toLowerCase(); // string
  • 使用 typeOf
    // 特点: typeOf 检测函数返回"function" 检测null返回"object"
    const val = 123;
    typeOf value  // number
  • 使用 instanceOf
    // 用来检测value是否在Array的原型链上也可以使用原型上的isPrototypeOf方法
    const value = '123;
    value.instanceOf(String) // true
  • 使用 Array.isArray()
    const arr = [];
    Array.isArray(arr) // true
    // 对于数组类型 可以使用Array.isArray(); 内部已经进行判空处理

深拷贝

  • JSON.parse(JSON.stringify())
    • 循环引用问题
    • 属性为function和undefined会丢失
    • Symbol值会生成undefined
    • 时间对象会生成时间字符串
    • 正则或者错误对象会生成空对象
    • 会丢失对象的构造器
  • 结构化克隆structuredClone(web原生能力)
    • 兼容性问题
    • 解决了JSON.parse(JSON.stringify())大部分问题
    • 存在的问题:丢失对象的原型链,不可拷贝函数,不可拷贝dom节点与Symbol
  • MessageChannel
    • 兼容性问题
    • MessageChannel是一个红任务,好在其执行时机先于定时器
    • 利用MessageChannel序列化进行深拷贝
    • 存在的问题:丢失对象的原型链,不可拷贝函数,不可拷贝dom节点与Symbol
    function deepClone(obj) {
      return new Promise((resolve, reject) => {
        try {
          const { port1, port2 } = new MessageChannel();
          port2.onmessage = function (e) {
            resolve(e.data);
          };
          port1.postMessage(obj);
        } catch (err) {
          reject(err);
        }
      });
    }
    // 测试
    const b = {
      a: undefined,
      b: {
        c: 1,
      },
    };
    console.log(b);
    deepClone(b).then((res) => {
      console.log(res, "res====");
    });
  • 手写深拷贝函数
function isTypeOf(value) {
  return Object.prototype.toString.call(value).slice(8, -1).toLowerCase();
}

function deepCopy(value) {
  if (!value || typeof value !== "object") {
    return value;
  }
  // 解决循环引用问题
  let cache = null;

  if (!deepCopy.cache) {
    deepCopy.cache = new WeakMap();
  }

  cache = deepCopy.cache;

  if (cache.has(value)) {
    return cache.get(value);
  }
  // 拷贝dom节点
  if (value.nodeType && "cloneNode" in value) {
    return value.cloneNode(true);
  }
  // 拷贝日期对象
  if (isTypeOf(value) === "date") {
    return new Date(value.getTime());
  }
  // 拷贝正则表达式
  if (isTypeOf(value) === "regexp") {
    return new RegExp(value);
  }
  // 拷贝错误对象
  if (isTypeOf(value) === "error") {
    return new Error(value.message);
  }
  // 单独处理集合结构
  if (isTypeOf(value) === "set") {
    const set = new Set();
    cache.set(value, true);
    value.forEach((item) => {
      set.add(deepCopy(item));
    });
    return set;
  }
  // 单独处理字典结构
  if (isTypeOf(value) === "map") {
    const map = new Map();
    cache.set(value, true);
    value.forEach((item, key) => {
      map.set(key, deepCopy(item));
    });
    return map;
  }

  const result = Array.isArray(value)
    ? []
    : value.construtor
    ? new value.construtor()
    : {};
  
  cache.set(value, true);
  for (let key in value) {
    result[key] = deepCopy(value[key]);
  }
  return result;
}
  • lodash中的深拷贝函数
    • 兼容性好
    • 容错性较为全面

结语

  • 一般情况下对于不是很复杂的数据结构,可以使用es6的扩展运算符或者一些数组方法来进行拷贝
  • 在项目中可以自己来封装深拷贝方法 避免引入第三方库 可以减少打包体积
  • 深拷贝可以解决对象引用问题 但是占用较大内存也是一个需要认真考虑的事情
  • 在react项目中我们可以使用一些不可变数据结构的库来避免深拷贝带来的内存消耗问题,如immer.js