Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JS 中的浅拷贝与深拷贝 #55

Open
z0nka1 opened this issue Feb 14, 2021 · 0 comments
Open

JS 中的浅拷贝与深拷贝 #55

z0nka1 opened this issue Feb 14, 2021 · 0 comments

Comments

@z0nka1
Copy link
Owner

z0nka1 commented Feb 14, 2021

在讲深浅拷贝之前,我们需要了解 JS 中的数据类型。

JS 数据类型

在 JS 中,数据类型分为两大类,一类是基础类型,一类是引用类型。

截至目前,JS 中基础类型有这7种:

  • Number
  • String
  • Boolean
  • Symbol
  • Bigint
  • Undefined
  • Null

引用类型有1种:

  • Object

其中,Object又可以分为以下7种:

  • Object
  • Array
  • Function
  • Date
  • RegExp
  • Math
  • Error

那么基础类型和引用类型有什么不同呢?

基础类型 VS 引用类型

原来,基础类型的变量存储在栈内存中,而引用类型的变量在栈内存中存储的是一个地址,这个地址指向存储在堆内存中的真正的值。(参考JavaScript中变量是如何存储的

当将一个基础类型的变量赋值给另一个变量的时候,新变量得到的是全新的值,跟原变量没有任何关联,修改新变量的值也不会影响到原变量。但是引用类型就不一样了,当把一个引用类型的变量赋值给另一个变量的时候,新变量得到的只是原变量的地址,它们都指向同一块内存,当对任何一个变量作出更改,另一个变量也会被改变。

明确了基础类型和引用类型的区别,下面我们可以开始深浅拷贝的内容了。

什么是浅拷贝?

先对浅拷贝下个定义:基于原对象创建一个新对象,复制原对象的每一个属性,如果属性值是基础类型,则复制得到该值;如果属性值是引用类型,则复制得到该值的地址。当新对象或原对象的其中一个被更改,基础类型的属性值不会互相影响,引用类型的属性值会跟着改变。

实际上,JS 原生已经实现了很多浅拷贝方法:

Array:concat(),slice(),Array.from(),展开运算符(...)
Object:Object.assign()

通过以下例子了解浅拷贝的特性。

数组浅拷贝例子:

const obj = [
  {
    a: 1
  },
  {
    b: 2
  }
];

const copyObj = [].concat(obj);

copyObj[0] = 'newVal';

copyObj[1].b = 'newB';

console.log(copyObj); // ['newVal', { b: 'newB' }]

console.log(obj); // [{ a: 1 }, { b: 'newB' }]

可以看到,在浅拷贝的时候,更改数组第一层的值并不会影响原对象,而更改更深层级的值就影响到了原对象。

再来看下对象(狭义)浅拷贝的例子:

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

const copyObj = Object.assign({}, obj);

copyObj.a = 'newA';

copyObj.b.c = 'newC';

console.log(copyObj);  // { a: 'newA', b: { c: 'newC' } }

console.log(obj);  //  { a: 1, b: { c: 'newC' } }

通过以上代码可以看到,浅拷贝对象的情况下,更改对象基础类型的属性值,没有影响到另一个对象;但是更改引用类型的属性值,另一个对象也被影响到了。这跟数组浅拷贝表现一致。

OK,我们已经了解浅拷贝的特性了,让我们开始对深拷贝的探索。

什么是深拷贝?

照例,我们对深拷贝进行下定义:基于原对象创建一个新对象,复制原对象的每一个属性,不管属性值是基础类型还是引用类型,都完全复制其属性值。对新对象或者原对象的任何更改,都不会影响另一个对象。

那么,了解了什么是深拷贝,是时候开始动手实现了。

键盘敲起来。。。

深拷贝如何实现

深拷贝实现思路主要有两种,一种是通过转为字符串,切断与原对象的联系,非常简单直观;另一种思路是通过递归。

下面我们先看第一种思路。

通过 JSON 序列化实现深拷贝

这种方式就是使用JSON.stringify将对象转为字符串,再用JSON.parse将字符串转回对象。

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

const copyObj = JSON.parse(JSON.stringify(obj));

简单粗暴的方式,总是会有诸多缺点:

  1. Date 类型的值会变成字符串
  2. RegExp 类型的值会变成空对象
  3. 如果属性值为NaN、Infinity 或者 -Infinity,JSON 序列化后会变成 null
  4. 如果属性值为函数、undefined 或者 Symbol,JSON 序列化后键值对会消失
  5. 不能拷贝不可枚举的属性
  6. 无法拷贝对象循环引用(obj[key] = obj)

我们要记住的就是,如果对象中有这些类型的属性值,就不要使用 JSON 序列化这种方式进行深拷贝。

下面我们看下更好一些的方案。

递归实现深拷贝

递归实现深拷贝的思路是,遍历对象的每个自身属性,如果该属性值是基本数据类型,则直接复制到新对象;如果该属性值是引用类型,则使用递归重走以上流程。

function deepClone(obj) {
  let result = Array.isArray(obj) ? [] : {};
  for (const k in obj) {
    if (obj.hasOwnProperty(k)) {
      if (typeof obj[k] === "object" && obj[k] !== null) {
        result[k] = deepClone(obj[k]);
      } else {
        result[k] = obj[k];
      }
    }
  }
  return result;
}

但是这样的实现方式也还是存在一些问题:

  1. 不能拷贝 Symbol 类型的值
  2. 不能拷贝 Date、RegExp、Error、Function 和 Array 类型的值
  3. 循环引用问题没有得到解决

针对以上问题,我们得到最终版深拷贝方法:

const isComplexDataType = obj => (typeof obj === 'object' || typeof obj === 'function') && (obj !== null);

const deepClone = function (obj, hash = new WeakMap()) {
  if (obj.constructor === Date) {
    return new Date(obj);       // 日期对象直接返回一个新的日期对象
  }

  if (obj.constructor === RegExp) {
    return new RegExp(obj);     //正则对象直接返回一个新的正则对象
  }

  //如果循环引用了就用 weakMap 来解决
  if (hash.has(obj)) return hash.get(obj);

  let allDesc = Object.getOwnPropertyDescriptors(obj);

  //遍历传入参数所有键的特性
  let cloneObj = Object.create(Object.getPrototypeOf(obj), allDesc);

  //继承原型链
  hash.set(obj, cloneObj);

  for (let key of Reflect.ownKeys(obj)) { 
    cloneObj[key] = (isComplexDataType(obj[key]) && typeof obj[key] !== 'function') ? deepClone(obj[key], hash) : obj[key]
  }

  return cloneObj;
}

经过这样的完善,绝大部分情况都考虑到了,可以满足各种场景的开发需求了。

@z0nka1 z0nka1 changed the title 如何实现一个深拷贝? JS 中的浅拷贝与深拷贝 Feb 19, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant