JS 深拷贝

33 阅读3分钟
let obj = {
  p1: undefined,
  p2: null,
  p3: new Date(),
  p4: Symbol(),
  p5: Boolean(false),
  p6: String("p6"),
  p7: Number(7),
  p8: NaN,
  p9: Infinity,
  p10: new Map([[10, 10]]),
  p11: new Set([1, 2, 3]),
  p12: new RegExp("p11"),
  p13: function () {
    console.log("p12");
  },
  p14: { p: 1 },
};

let arr = [
  undefined,
  null,
  new Date(),
  Symbol(),
  Boolean(false),
  String("p6"),
  Number(7),
  NaN,
  Infinity,
  new Map([[10, 10]]),
  new Set([1, 2, 3]),
  new RegExp("p11"),
  function () {
    console.log("p12");
  },
  { p: 1 },
];

/**
 * 浅拷贝
 * 数组:可以使用slice,concat 扩展运算符(...)
 * 对象:Object.assign 扩展运算符(...)
 */
const newArr = arr.slice();
newArr[newArr.length - 1].p = 2;
console.log(arr[newArr.length - 1]); // { p: 2 }

const newObj = Object.assign({}, obj);
newObj.p14.p = 2;
console.log(obj.p14); // { p: 2 }

/**
 * 版本1 JSON.parse(JSON.stringify())
 * 适用于 只包含基本数据类型值的对象或数组
 * 结果:有的丢了,有的转成了null或空对象
 * 原因:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#%E6%8F%8F%E8%BF%B0
 * 布尔值、数字、字符串的包装对象在序列化过程中会自动转换成对应的原始值。
 * undefined、任意的函数以及 symbol 值,在序列化过程中会被忽略(出现在非数组对象的属性值中时)或者被转换成 null(出现在数组中时)。
 * NaN 和 Infinity 格式的数值及 null 都会被当做 null。
 */
console.log(JSON.parse(JSON.stringify(obj)), JSON.parse(JSON.stringify(arr)));

/**
 * 版本2
 * 针对常见对象进行特殊处理
 */
function deepCopy(obj, map = new WeakMap()) {
  // 只拷贝对象
  if (typeof obj !== "object" || obj === null) {
    return obj;
  }

  // 处理循环引用
  if (map.has(obj)) {
    return map.get(obj);
  }

  /***
   * https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Date/Date#%E5%8F%82%E6%95%B0
   * 参数是一个现有的 Date 对象时。这实际上是在现有的 Date 对象上复制了一个相同的日期和时间。这等同于 new Date(dateObject.valueOf()),除了不调用 valueOf() 方法。
   * 当一个参数被传递给 Date() 构造函数时,Date 实例被特别处理。所有其他的值都被转换为原始值。如果结果是一个字符串,它将被解析为一个日期字符串。否则,产生的会被进一步强制转换为数值,并被视为时间戳。
   */
  if (obj instanceof Date) {
    return new Date(obj);
  }

  /**
   * https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/RegExp/RegExp#%E5%8F%82%E6%95%B0
   * 从 ES5 开始,参数 可以是 RegExp 对象
   * 如果没有指定flags并且提供了一个正则表达式对象,则该对象的 flags(和 lastIndex 值) 将被复制。
   */
  if (obj instanceof RegExp) {
    return new RegExp(obj);
  }

  if (obj instanceof Set) {
    const newObj = new Set();
    map.set(obj, newObj);
    obj.forEach((value) => newObj.add(deepCopy(value, map)));
    return obj;
  }

  if (obj instanceof Map) {
    const newObj = new Map();
    map.set(obj, newObj);
    obj.forEach((value, key) => newObj.set(key, deepCopy(value, map)));
    return newObj;
  }

  // Array.isArray()
  if (obj instanceof Array) {
    const newObj = [];
    map.set(obj, newObj);
    obj.forEach((vlaue, index) => {
      newObj[index] = deepCopy(vlaue, map);
    });
    return newObj;

    // return obj.map((value) => deepCopy(value));
  }

  /**
   * JSON 对象
   * Object.keys() 静态方法返回一个由给定对象自身的可枚举的《字符串键属性名》组成的数组。Symbol 没有
   * 静态方法 Reflect.ownKeys() 返回一个由目标对象自身的属性键组成的数组,包括 Symbol 键和不可枚举属性
   */
  const newObj = Object.create(Object.getPrototypeOf(obj));
  map.set(obj, newObj);
  Reflect.ownKeys(obj).forEach((key) => (newObj[key] = deepCopy(obj[key], map)));
  return newObj;
}

// 保持原型链:
class CustomClass {}
const original1 = new CustomClass();
const cloned1 = deepCopy(original1);
console.log(cloned1 instanceof CustomClass); // true

// Symbol 属性支持:
const sym = Symbol("key");
const original2 = { [sym]: "value" };
const cloned2 = deepCopy(original2);
console.log(cloned2[sym]); // 'value'

// 测试复杂结构
const original = {
  p1: null,
  p2: undefined,
  array: [1, { nested: "value" }],
  date: new Date(),
  regex: /test/g,
  set: new Set([1, 2, 3, sym]),
  map: new Map([
    ["key", "value"],
    [sym, "symbol"],
  ]),
  fn: function () {
    return "function";
  },
};

const cloned = deepCopy(original);

console.log(cloned.array !== original.array); // true
console.log(cloned.array[1] !== original.array[1]); // true
console.log(cloned.date.getTime() === original.date.getTime()); // true
console.log(cloned.regex.source === original.regex.source); // true
console.log(cloned.set.has(1)); // true
console.log(cloned.set.has(sym)); // true
console.log(cloned.map.get("key")); // 'value'
console.log(cloned.map.get(sym)); // 'symbol'
console.log(cloned.fn()); // 'function'
console.log(cloned.p1); // null
console.log(cloned.p2); // undefined

// 循环引用
const original3 = { name: "循环对象" };
original3.self = original3;
original3.child = { parent: original3 };

const cloned3 = deepCopy(original3);

original3.age = 12;

console.log(cloned3.self === cloned3); // true
console.log(cloned3.age); // undefined