JS中的对象克隆(完整版看这里)

更多点击这里:

浅拷贝

浅拷贝只新建拷贝对象的第一层,剩下的不变。

const obj = {
  a: 1
  b: {
    c: 2
  }
}

const clonedObj = shallowClone(obj);

console.log(obj === clonedObj) // false
console.log(obj['b'] === clonedObj['b']) // true

Lodash的clone方法

比较灵活,支持多种类型的拷贝

const _ = require('lodash');

const obj = {
  a: 1,
  b: {
    c: 0
  },
  set: new Set('e')
}

const clonedObj = _.clone(obj);
console.log(clonedObj);
console.log(obj.set === clonedObj.set); // true
const _ = require('lodash');

const clonedObj = _.clone({'a': 1});
console.log(clonedObj)

Object.assign

Object.assign() 将一个或者多个源对象中所有可枚举的自有属性复制到目标对象,并返回修改后的目标对象。

const shallowClone = (obj) => {
  return Object.assign({}, obj);
}

const clonedObj = shallowClone({'a': 1});
console.log(clonedObj);

使用起来便捷但是要注意使用方式:

  1. 原型链上的属性和不可枚举的属性不能被复制
  2. 参考:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/assign

ES6的展开运算符

const shallowClone = (obj) => {
  return { ...obj };
}

const clonedObj = shallowClone({'a': 1});
console.log(clonedObj)

Array.prototype.xxx(适用于数组)

  1. Array.prototype.slice
const arr = [1, 2, 3];
const clonedObj = arr.slice(0, arr.length);

console.log(clonedObj);
  1. Array.prototype.concat
const arr = [1, 2, 3];
const clonedObj = [].concat(arr);

console.log(clonedObj);

深拷贝

深拷贝新建拷贝对象的每一层。

const obj = {
  a: 1
  b: {
    c: 2
  }
}

const clonedObj = deepClone(obj);

console.log(obj === clonedObj) // false
console.log(obj['b'] === clonedObj['b']) // false

Lodash库的cloneDeep方法

比较灵活,支持多种类型的拷贝

const _ = require('lodash');

const obj = {
  a: 1,
  b: {
    c: 0
  },
  set: new Set('e')
}

const clonedObj = _.cloneDeep(obj);
console.log(clonedObj);
console.log(obj.set === clonedObj.set); // false

更多点击这里:

使用递归手动实现

实现起来和lodash的cloneDeep方法类似,不过是简单的阉割版

 const isShallowClone = (obj) => {
    if (typeof obj === "function") {
      // function type
      return true;
    } else if (typeof obj !== "object" || obj === undefined) {
      // basic type
      return true;
    } else if (obj === null) {
      // special object type
      return true;
    }
    return false;
  };
  
  const deepClone = (obj) => {
    if (isShallowClone(obj)) {
      return obj;
    }
    let clonedObj = undefined;
    if (obj instanceof Date) {
      return new Date(obj.getTime());
    } else if (obj instanceof RegExp) {
      return new RegExp(obj.source, obj.flags);
    } else if (obj instanceof Map) {
      clonedObj = new Map();
      for (const [k, v] of obj) {
        clonedObj.set(k, deepClone(v));
      }
      return clonedObj;
    } else if (obj instanceof Set) {
      const clonedObj = new Set();
      for (const v of obj) {
        clonedObj.add(deepClone(v));
      }
      return clonedObj;
    } else if (Array.isArray(obj)) {
      clonedObj = [];
    } else {
      clonedObj = {};
    }
    // iterate own properties
    for (const k of Reflect.ownKeys(obj)) {
      clonedObj[k] = deepClone(obj[k]);
    }
    return clonedObj;
  };

用例:

const obj1 = {
  // =========== 1.基础数据类型 ===========
  num: 0, // number
  str: "", // string
  bool: true, // boolean
  unf: undefined, // undefined
  nul: null, // null
  sym: Symbol("sym"), // symbol
  bign: BigInt(1n), // bigint

  // =========== 2.Object类型 ===========
  // 普通对象
  obj: {
    name: "我是一个对象",
    id: 1,
  },
  // 数组
  arr: [0, 1, { arr2: [-1] }],
  // 函数
  func: function () {
    console.log("我是一个函数");
  },
  // // 日期
  date: new Date(0),
  // // 正则
  reg: new RegExp("/我是一个正则/ig"),
  // // Map
  map: new Map().set("mapKey", 1),
  map2: new Map().set(
  "k1",
  new Map().set("k2", [
    1,
    2,
    [3, 4],
    () => {
      console.log("good");
    },
  ]),
  // // Set
  set: new Set().add("set"),
  // // =========== 3.其他 =====x======
  [Symbol("1")]: { [Symbol("ok")]: "nice" }, // Symbol作为key
};

const res = deepClone(obj1);
console.log(res);

上面的代码不能解决循环引用问题(会导致栈溢出)

const a = {name: "a"};
const b = {name: "b"};
a.b = b;
b.a = a; // 相互引用,代码会不断从a -> b, b-> a,导致不断消耗栈空间最终栈溢出
const res = deepClone(a);
console.log(res);
RangeError: Maximum call stack size exceeded
    at deepClone
    at deepClone
    at deepClone
    at deepClone
    .....

使用标记法解决循环依赖:

const deepClone = (obj) => {
  const isShallowClone = (obj) => {
    if (typeof obj === "function") {
      // function type
      return true;
    } else if (typeof obj !== "object" || obj === undefined) {
      // basic type(number, string, boolean...)
      return true;
    } else if (obj === null) {
      // special object type
      return true;
    }
    return false;
  };

  // 1. 使用(新-旧)键值来判断发生并解决循环引用导致的stackoverflow问题
  // 2. 使用WeakMap避免闭包导致的内存泄漏
  const cache = new WeakMap();

  const _deepClone = (obj) => {
    if (isShallowClone(obj)) {
      return obj;
    }
    if (cache.get(obj)) {
      return cache.get(obj);
    }
    let clonedObj = undefined;
    if (obj instanceof Date) {
      return new Date(obj.getTime());
    } else if (obj instanceof RegExp) {
      return new RegExp(obj.source, obj.flags);
    } else if (obj instanceof Map) {
      clonedObj = new Map();
      for (const [k, v] of obj) {
        cache.set(obj, clonedObj);
        clonedObj.set(k, _deepClone(v));
      }
      return clonedObj;
    } else if (obj instanceof Set) {
      const clonedObj = new Set();
      for (const v of obj) {
        cache.set(obj, clonedObj);
        clonedObj.add(_deepClone(v));
      }
      return clonedObj;
    } else if (Array.isArray(obj)) {
      clonedObj = [];
    } else {
      clonedObj = {};
    }
    // iterate own properties
    for (const k of Reflect.ownKeys(obj)) {
      cache.set(obj, clonedObj);
      clonedObj[k] = _deepClone(obj[k]);
    }
    return clonedObj;
  };

  return _deepClone(obj);
};

用例:

const obj = {
  // =========== 1.基础数据类型 ===========
  num: 0, // number
  str: "", // string
  bool: true, // boolean
  unf: undefined, // undefined
  nul: null, // null
  sym: Symbol("sym"), // symbol
  bign: BigInt(1n), // bigint

  // =========== 2.Object类型 ===========
  // 普通对象
  obj: {
    name: "我是一个对象",
    id: 1,
  },
  // 数组
  arr: [0, 1, 2],
  // 函数
  func: function () {
    console.log("我是一个函数");
  },
  // 日期
  date: new Date(0),
  // 正则
  reg: new RegExp("/我是一个正则/ig"),
  // Map
  map: new Map().set("mapKey", 1),
  // Set
  set: new Set().add("set"),
  // =========== 3.其他 ===========
  [Symbol("1")]: 1, // Symbol作为key
};

obj["loop"] = obj;

const res = deepClone(obj);
console.log(res);

这种递归代码扩展性比较好

  1. 像Promise这类的类型就可以自定义拷贝方式
  2. 对于是否拷贝原型链上的属性以及是否继承原型链都可以定制

更多点击这里:

message channel

message channel一般用于ifram窗口的通信。

这种方法遇到function没法拷贝,会报错

const obj = {
    a: 1,
    b: 2,
    f: () => {}
}

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

const res = await deepClone_messagechannel(obj);

console.log(res)
Uncaught DOMException: Failed to execute 'postMessage' on 'MessagePort': () => {} could not be cloned.

序列化

function类型以及其他某些类型也不能拷贝

deepClone_stringfy = (obj) => {
  const str = JSON.stringify(obj);
  return JSON.parse(str);
};

structuredClone方法

结构化克隆算法用于复制复杂 JavaScript 对象的算法。通过来自 Worker 的 postMessage() 或使用 IndexedDB 存储对象时在内部使用。它通过递归输入对象来构建克隆,同时保持先前访问过的引用的映射,以避免无限遍历循环

  1. Function 对象是不能被结构化克隆算法复制的;如果你尝试这样子去做,这会导致抛出 DATA_CLONE_ERR 的异常。
    企图去克隆 DOM 节点同样会抛出 DATA_CLONE_ERR 异常。
  2. 对象的某些特定参数也不会被保留
  3. RegExp 对象的 lastIndex 字段不会被保留
  4. 属性描述符,setters 以及 getters(以及其他类似元数据的功能)同样不会被复制。例如,如果一个对象用属性描述符标记为 read-only,它将会被复制为 read-write,因为这是默认的情况下。
  5. 原形链上的属性也不会被追踪以及复制。

refer: https://developer.mozilla.org/zh-CN/docs/Web/API/Web_Workers_API/Structured_clone_algorithm

用例:

const obj = { a: 1 };
const res = structuredClone(a);
console.log(res);

可以看出这种方式还是有很多限制的, 内部也是使用了递归的方式实现的

对比这几种

用例:

const deepNestedObject = {
  level1: {
    level2: {
      level3: {
        level4: {
          level5: {
            level6: {
              level7: {
                level8: {
                  level9: {
                    level10: {
                      arrayOfNumbers: [1, 2, 3, 4],
                      anotherObject: {
                        key1: "value1",
                        key2: "value2",
                      },
                    },
                  },
                },
              },
            },
          },
        },
      },
      deep: [
        {
          key1: "value1",
          key2: "value2",
          key3: [
            {
              nestedKey1: "nestedValue1",
              nestedKey2: "nestedValue2",
            },
            {
              nestedKey1: "nestedValue3",
              nestedKey2: "nestedValue4",
            },
          ],
        },
        {
          key1: "value3",
          key2: "value4",
          key3: [
            {
              nestedKey1: "nestedValue5",
              nestedKey2: "nestedValue6",
            },
            {
              nestedKey1: "nestedValue7",
              nestedKey2: "nestedValue8",
            },
          ],
        },
      ],
    },
  },
};

const _ = require("lodash");

const tikTok = (func) => {
  const s = new Date().getTime();
  for (let i = 0; i < 1000000; i++) {
    func();
  }
  console.log(new Date().getTime() - s);
};

const tikTok2 = async () => {
    const s = new Date().getTime();
    for (let i = 0; i < 1000000; i++) {
      await deepClone_messagechannel(deepNestedObject);
    }
    console.log(new Date().getTime() - s);
};


tikTok(() => deepClone(deepNestedObject)); // 自定义deepClone
tikTok(() => _.cloneDeep(deepNestedObject)); // lodash的cloneDeep
tikTok(() => deepClone_stringfy(deepNestedObject)); // JSON.stringfy那种序列化形式的
tikTok(() => structuredClone(deepNestedObject)); // structuredClone方法
await tikTok2(); // message channel

在浏览器统一运行输出结果:

6216
8603
5145
11929
50126

速度上:序列化 > 自定义deepClone > lodash > structuredClone > message channel

使用便利性上:lodash、自定义deepClone > structuredClone、序列化 > message channel

更多点击这里:

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值