前语

咱们都知道,JavaScript是一门弱类型脚本语言,在处理数据仿制时涉及到深仿制和浅仿制两种不同的概念。这两种仿制方式在处理方针和数组时有着重要的区别,深仿制和浅仿制的选择关于程序的正确性和功能有着深远的影响。下面我将解说一下深浅仿制,并尝试着去手写实现一个深仿制函数。

下面是一道面试题,在咱们学习完深浅仿制常识之后咱们再倒过来做一遍。当然,假如你现已会了,显然这篇文章对你的帮助也不会很大。

题目: 实现一个深仿制函数 deepClone,该函数能够对传入的方针进行深度仿制,确保原始方针与仿制方针之间没有引证联系。

要求:

  1. 深仿制应该适用于各种数据类型,包含方针、数组、字符串、数字等。
  2. 考虑处理循环引证的状况,防止堕入无限递归。
  3. 你能够选择运用任何合适的方式来实现深仿制,例如递归、JSON.parse 和 JSON.stringify,或其他办法。

什么是浅仿制?

浅仿制是一种仿制方针或数组的办法,但它只仿制了方针的第一层,而不会递归仿制嵌套在其中的方针或数组。简而言之,浅仿制创立了一个新的方针,但该方针的嵌套结构仍然与原始方针共享引证。对仿制后的第二层方针的值进行修正,会影响到被仿制的方针的值。

下面介绍一下JS中一些常见的浅仿制的办法:

1. Object.create(x)

  • Object.create() 办法创立一个新方针,该方针的原型链继承自指定的方针 x
let obj = { name: '小明', details:{ age: 25 } };
let objCopy = Object.create(obj);
objCopy.name = '小红';
objCopy.details.age = 30;
console.log(obj.name); // 输出: 小明,原方针未受影响
console.log(obj.details.age); // 输出: 30,原方针受影响

2.Object.assign({},x)

  • Object.assign() 办法用于将一切可枚举特点的值从一个或多个源方针仿制到方针方针 {}
let obj = { name: '小明', details:{ age: 25 } };
let objCopy = Object.assign({},obj);
objCopy.name = '小红';
objCopy.details.age = 30;
console.log(obj.name); // 输出: 小明,原方针未受影响
console.log(obj.details.age); // 输出: 30,原方针受影响

数组的浅仿制

1.concat

  • concat 办法用于合并两个或多个数组。它创立一个新的数组,其中包含被调用的数组的浅仿制。
let arr = [1, 2, 3, { value: 4 }];
let arrCopy = arr.concat();
arrCopy[2] = 5;
console.log(arr); // 输出: [1, 2, 3, { value: 4 }],原数组未受影响
arrCopy[3].value = 10;
console.log(arr); // 输出: [1, 2, 3, { value: 10 }],原数组受影响

2.slice

  • slice回来一个新数组,其中包含被调用的数组的浅仿制。
let arr = [1, 2, 3, { value: 4 }];
let arrCopy = arr.slice();
arrCopy[2] = 5;
console.log(arr); // 输出: [1, 2, 3, { value: 4 }],原数组未受影响
arrCopy[3].value = 10;
console.log(arr); // 输出: [1, 2, 3, { value: 10 }],原数组受影响

3.数组解构

  • 运用数组解构赋值也能进行浅仿制
let arr = [1, 2, 3, { value: 4 }];
let [...arrCopy] = arr;
arrCopy[2] = 5;
console.log(arr); // 输出: [1, 2, 3, { value: 4 }],原数组未受影响
arrCopy[3].value = 10;
console.log(arr); // 输出: [1, 2, 3, { value: 10 }],原数组受影响

什么是深仿制?

  • 递归创立全新方针,对仿制方针的值进行修正不会影响到原始方针的值。

深仿制是一种创立方针或数组完全独立副本的方式,包含嵌套在其中的方针或数组。深仿制确保原始方针和仿制方针之间不存在引证联系,防止了副作用。

常见的深仿制办法

JOSN.parse(JSON.stringify(obj))

let obj = { name: '小明', value: { a: 1, b: 2 } };
let deepCopy = JSON.parse(JSON.stringify(obj));
deepCopy.name = '小红';  // 修正第一层内容
console.log(obj);  // 输出:{ name: '小明', value: { a: 1, b: 2 } } 原始方针obj的值没有受影响
deepCopy.value.a =  3;  // 修正第二层内容
console.log(obj);  // 输出:{ name: '小明', value: { a: 1, b: 2 } } 原始方针obj的值没有受影响

上述办法存在以下缺陷:

1.无法仿制 undefined、function、Symbol、bigint:

  • JSON.stringifyJSON.parse 在序列化和反序列化时会丢掉 undefined、function、Symbol,以及不能正确处理 bigint 类型。这意味着深仿制后的方针或许丧失这些类型的信息。

比如:

let obj = { a: undefined, b: function() {}, c: Symbol('symbol'), d: BigInt(123) };
let deepCopy = JSON.parse(JSON.stringify(obj));
console.log(deepCopy); // 输出: { a: null, b: null, c: null, d: 123 }

2.无法处理循环引证:

  • 假如方针存在循环引证(即方针的特点之间构成一个闭环),JSON.stringify 会抛出反常,由于 JSON 格局无法表明循环引证。

比如:

let obj = { a: 1 };
obj.self = obj; // 构成循环引证
// 以下代码会抛出反常
// let deepCopy = JSON.parse(JSON.stringify(obj));

面试官:请手写一个深仿制函数

手写一个完美的深仿制

通过上面的剖析之后,咱们都知道要实现一个完美的深仿制就必须处理上述常用办法-JOSN.parse(JSON.stringify(obj))带来的问题。

function deepClone(obj, clonedObjects = new WeakMap()) {
  if (obj === null || typeof obj !== 'object') {
    return obj;
  }
  // 查看是否现已仿制过该方针,防止循环引证导致无限递归
  if (clonedObjects.has(obj)) {
    return clonedObjects.get(obj);
  }
  let clone;
  // 处理数组
  if (Array.isArray(obj)) {
    clone = [];
    clonedObjects.set(obj, clone);
    for (let i = 0; i < obj.length; i  ) {
      clone[i] = deepClone(obj[i], clonedObjects);
    }
  }
  // 处理方针
  else if (obj instanceof Object) {
    clone = {};
    clonedObjects.set(obj, clone);
    for (let key in obj) {
      if (obj.hasOwnProperty(key)) {
        clone[key] = deepClone(obj[key], clonedObjects);
      }
    }
  }
  return clone;
}

上述办法是如何处理JOSN.parse(JSON.stringify(obj))带来的问题的呢?这里咱们得感谢WeakMap()。

1.运用WeakMap处理循环引证

  • WeakMap 是一个弱映射表,它允许你在没有内存走漏危险的状况下将方针作为键存储在其中。在 deepClone 中,clonedObjects 是一个 WeakMap,用于跟踪现已仿制过的方针。
为什么不运用Map,而是WeakMap?

运用 Map 代替 WeakMap 会导致潜在的内存走漏问题,由于 Map 对键的引证是强引证。而 WeakMap 运用的是弱引证,能够防止由于该引证而阻挠废物收回器对方针的收回。 在深仿制的场景中,当你运用 Map 时,或许会导致整个仿制的方针及其子方针都无法被废物收回,由于 Map 对键的引证是强引证。而运用 WeakMap 时,当不再存在对原方针的引证时,对应的键值对就能够被废物收回,不会造成内存走漏。

2.关于 undefined、function、Symbol、bigint 的处理:

if (obj === null || typeof obj !== 'object') {
  return obj;
}

解释:

在进入递归部分之前,首先进行了根底的判别,假如 obj 是 null 或者不是方针类型,就直接回来 obj。这样,关于 undefined、function、Symbol、bigint 这些特殊类型,就会直接仿制。

检验是否处理了

1.循环引证

面试官:请手写一个深仿制函数

2.关于 undefined、function、Symbol、bigint 的处理

面试官:请手写一个深仿制函数