谈谈深拷贝、浅拷贝

码海清韵
• 阅读 1418
前提: 假设您已经知道为什么在JavaScript中需要深拷贝和浅拷贝了。

举两个例子:


const a = [1, 2, { key: 20 }]
const b = [...a]
b[2].key = 30

console.log(a[2] === b[2])


console.log(a === b) // true

const o = { k1: { kk1: 50} }

const o1 = { ...o }

o1.k1.bb = { bb: 30 }

console.log(o1.k1 === o.k1) // true

在上面数组和对象中分别改变了 b[2]o1.k1,但是最后结果的得到和原来的值保持一致。

在JavaScript中分为2大类(原始值类型和对象类型)7中数据类型(Boolean, Null, Undefined, Number, String, Symbol),原始值类型标识对这个数据的任何操作都会返回一个新的数据,也就是说一旦申明一个原始值类型的数据则该数据不可变。如果申明一个对象类型例如: {}, new Map(), new Set(), new Regex(), new Date() 等等。再进一步来说:

谈谈深拷贝、浅拷贝
(网图,侵删)

不同的类别的数据保存的数据结构,数据申明保存在栈数据结构中,而对象应用则分配在堆中,即用一大块非结构化的内存区域
参考: https://developer.mozilla.org...

我们一般说的深拷贝和浅拷贝主要是对数组和对象来进行的,下面也主要对数组和对象进行实践操作:

浅拷贝

浅拷贝可以当成是单层拷贝,何为单层拷贝,就是复制的对象深度只有一。

数组

Array.concat

如下操作:

const arr = [1, 2, 3]
const newArr = [].concat(arr)
// arr === newArr false

上面这个方式就直接把把arr合并到新的数组中,并把新的数组返回回来,达到拷贝的目的。

Array.slice

数组的复制操作:

const arr = [1, 2, 3]
const newArr = arr.slice()
// arr === newArr false

上面从0 -> arr.length - 1 进行拷贝复制,返回一个新的数组.

Array.from

数组的创建方式,通过给定一个不定参数,然后创建一个数组

const arr = [1, 2, 3]
const newArr = Array.from(arr)
// newArr === arr false

通过原有数组创建新数组,得到拷贝目的。

总结

上面三种方式都可以简单进行数组的浅拷贝,如果数组内嵌套有其他数据呢?这个数据是没有处理过呢,如何做呢?且看下文

Object

Object.assign

对象合并方法:

const o = {a: 1, b: 2}
const no = Object.assign({}, o)
// o  === no1 false

通过一个新对象和原有对象合并,得到新的对象

ES6 扩展运算符(...)

扩展符号如下:

const o = { a: 1, b: 1 }
const o1 = {...o }
// o === o1 false

通过一个新的对象申明,并把原有对象属性通过 ... 复制下来,达到拷贝目的。

深拷贝

上文中都是单层数据拷贝,在内存堆栈来说,就是在栈内重新重新开辟的空间,但是实际上,这个对象对应的二层对象并没有进行任何处理,依旧还是原有只想,浅拷贝实现的示意图如下:
谈谈深拷贝、浅拷贝

红色部分是新进行申明的变量以及新的在堆中的内容,绿色部分总是没有被复制。如何始终让绿色可以被拷贝,被复制呢?下面就说一下这个

普通深拷贝 JSON.parse 和 JSON.stringify

通过v8提供的JSON序列化和反序列的的方法,首先把json转换成字符串,在js中,所有Primitive 值都是不可变的,一旦修改就是新的数据。然后通过反序列的方式,直接将JSON.parse 转换回来了即可。


const a = { ... }

const deepCloneA = JSON.parse(JSON.stringify(a))

JSON 序列化和反序列化局限:

  1. undefined、任意的函数以及 symbol 值,在序列化过程中会被忽略(出现在非数组对象的属性值中时)或者被转换成 null(出现在数组中时)。
  2. 对包含循环引用的对象(对象之间相互引用,形成无限循环)执行此方法,会抛出错误。
  3. 所有以 symbol 为属性键的属性都会被完全忽略掉,即便 replacer 参数中强制指定包含了它们。
  4. Date日期调用了toJSON()将其转换为了string字符串(同Date.toISOString()),因此会被当做字符串处理。
  5. NaN和Infinity格式的数值及null都会被当做null。
  6. 其他类型的对象,包括Map/Set/weakMap/weakSet,仅会序列化可枚举的属性。
https://developer.mozilla.org...

局限也是 JSON.stringify的局限。

那么总结一下,如果我们要进行深拷贝,需要考虑的问题是那些呢?

  1. 对象循环拷贝,解决对象内部嵌套对象问题
  2. falsy 的数据,函数,symbol可以被拷贝, date对象能够
  3. 循环引用的解决

解决遗留问题

对象循环拷贝

见如下代码:

function deepCopy(o) {
  if (typeof o !== 'object') return o;

  const object = {};

  for (const key in o) {
    if (o.hasOwnProperty(key)) {
      const element = o[key];

      if (typeof element === 'object' && element !== null) deepCopy(element);
      else object[key] = element;
    }
  }
  return object
}

测试代码:


const o = {
  a: 2,
  b: '2',
  c: { say: 'hello world' },
  d: null,
  e: undefined,
  f: function() {
    console.log('Good')
  },
  symbol: Symbol('hello')
}

console.log(o)
const o1 = deepCopy(o)
console.log(o1);

输出如下:

{ a: 2,
  b: '2',
  c: { say: 'hello world' },
  d: null,
  e: undefined,
  f: [Function: f],
  symbol: Symbol(hello) }
{ a: 2,
  b: '2',
  d: null,
  e: undefined,
  f: [Function: f],
  symbol: Symbol(hello) }
false

在代码里面,我们使用递归,实现了基本数据的复制。上面的情况基本能够解决我们大部分

特殊数据处理

谈谈深拷贝、浅拷贝

在上述的图中,如果我们的数据结构变成这样,结果是怎么样的呢?需要对一些特别的数据进行处理, 例如Date, Map 等。这里以Date和Map为例子,其他类似:
谈谈深拷贝、浅拷贝

最后得到两个值都是空值,所以需要对写类型的数据进行特别处理.

这里增加一种工具类:

const objectTag = '[object Object]';
const arrayTag = '[object Array]';
const dateTag = '[object Date]';
const mapTag = '[object Map]';

const getTag = (o) => Object.prototype.toString.call(o)

开始真正的表演:

function deepCopy(o) {
  if (typeof o !== 'object') return o;

  const object = {};

  for (const key in o) {
    const element = o[key];

    if (element && typeof element === 'object') {
      const tag = getTag(element);
      const Ctor = element.constructor
      switch (tag) {
        case arrayTag:
        case objectTag:
          object[key] = deepCopy(element);
          break
        case dateTag:
          object[key] = new Ctor(+element)
          break
        case mapTag:
          const map = new Ctor
          element.forEach((subValue, key) => {
            map.set(key, deepCopy(subValue))
          })
          object[key] = map
        default:
          break;
      }
    } else object[key] = element;
  }
  return object;
}

运行相同的测试代码,输出如下:

{ a: 2,
  b: '2',
  c: { say: 'hello world' },
  d: null,
  e: undefined,
  f: [Function: f],
  g: Infinity,
  symbol: Symbol(hello),
  dd: 2019-10-22T12:54:57.976Z,
  cc: Map { 'a' => 2 } }
{ a: 2,
  b: '2',
  c: { say: 'hello world' },
  d: null,
  e: undefined,
  f: [Function: f],
  g: Infinity,
  symbol: Symbol(hello),
  dd: 2019-10-22T12:54:57.976Z,
  cc: Map { 'a' => 2 } }
false false

这里就处理好了一些特殊数据的问题。

从上面也可以得到,一个数据的要想支持深拷贝,必须要对对应深拷贝的数据进行处理, 上面也是lodash深拷贝实现思路。

循环引用

特殊数据也处理完成后,若我们有下面数据:
谈谈深拷贝、浅拷贝

直接运行看情况:
谈谈深拷贝、浅拷贝

这里需要对代码进行一些处理,我们需要判断代码是否存在循环引用呢?我们在递归时候,不断把当前父级(currentParent),当然的复制的数据(object), 还有最原始的数据(o)传入,是不是可以通过循环判断是否存在递归, 下面实现一下:

const objectTag = '[object Object]';
const arrayTag = '[object Array]';
const dateTag = '[object Date]';
const mapTag = '[object Map]';

const getTag = (o) => Object.prototype.toString.call(o);

function deepCopy(o, parent = null) {
  if (typeof o !== 'object') return o;

  const object = {};
  let _parent = parent
  while(_parent) {
    if (_parent.originParent === o) {
      return _parent.currentParent
    }
    _parent = _parent.parent
  }

  for (const key in o) {
    const element = o[key];

    if (element && typeof element === 'object') {
      const tag = getTag(element);
      const Ctor = element.constructor;
      switch (tag) {
        case arrayTag:
        case objectTag:
          object[key] = deepCopy(element, { parent, currentParent: object, originParent: o });
          break;
        case dateTag:
          object[key] = new Ctor(+element);
          break;
        case mapTag:
          const map = new Ctor();
          element.forEach((subValue, key) => {
            map.set(key, deepCopy(subValue, { parent, currentParent: object, originParent: o }));
          });
          object[key] = map;
        default:
          break;
      }
    } else object[key] = element;
  }
  return object;
}

我们在入口时候判断,如果是递归的话,就把当前复制的结果给返回即可。查看如下示例:

const o = {
  a: 2,
  b: '2',
  c: { say: 'hello world' },
  c1: { say: 'good idea' },
  d: null,
  e: undefined,
  f: function() {
    console.log('Good');
  },
  g: Infinity,
  symbol: Symbol('hello'),
  dd: new Date(),
  cc: new Map([['a', 2]]),
};
o.ff = o;
o.cc.set('cir', o)
o.c.bb = o.c1

输出结果:

{ a: 2,
  b: '2',
  c: { say: 'hello world', bb: { say: 'good idea' } },
  c1: { say: 'good idea' },
  d: null,
  e: undefined,
  f: [Function: f],
  g: Infinity,
  symbol: Symbol(hello),
  dd: 2019-10-24T05:17:09.550Z,
  cc: Map { 'a' => 2, 'cir' => [Circular] },
  ff: [Circular] }
{ a: 2,
  b: '2',
  c: { say: 'hello world', bb: { say: 'good idea' } },
  c1: { say: 'good idea' },
  d: null,
  e: undefined,
  f: [Function: f],
  g: Infinity,
  symbol: Symbol(hello),
  dd: 2019-10-24T05:17:09.550Z,
  cc: Map { 'a' => 2, 'cir' => [Circular] },
  ff: [Circular] }
false false
false

这样就保证递归的正确性了。

闲谈

或许这里递归方式并不是解决重复引用的最好方法,也有方式采用 WeakMap 方式来解决,每次递归的时候都用WeakMap存下即可。

最后

深浅拷贝涉及JS的数据类型的存储机制,所以对深浅拷贝可以明确区分在JS中 原始类型(Primitive) 或者 对象类型(Object) 存储的区分。

如有问题,欢迎交流。
源码地址: https://github.com/zsirfs/content-scripts/blob/master/deep-copy.js
点赞
收藏
评论区
推荐文章
翼
4年前
ES6的解构赋值是深拷贝or浅拷贝?
面试时候有面试官问到ES6的解构赋值是深拷贝还是浅拷贝?,这里做一个总结.ES6的解构赋值,大家应该都清楚,就是可以快速取出数组或者对象中的值;我们先来看一个使用案例:更多的解构赋值知识可以查看:https://es6.ruanyifeng.com/docs/destructuring那么,ES6的解构赋值到底是深拷贝还是浅拷贝呢?我们先来看一下深拷贝和浅
Wesley13 Wesley13
4年前
java 复制Map对象(深拷贝与浅拷贝)
java复制Map对象(深拷贝与浅拷贝)CreationTime2018年6月4日10点00分Author:Marydon1.深拷贝与浅拷贝  浅拷贝:只复制对象的引用,两个引用仍然指向同一个对象
晴空闲云 晴空闲云
4年前
也谈JavaScript浅拷贝和深拷贝
网上关于这个话题,讨论有很多了,根据各路情况我自己整理了一下,最后还是能接近完美的实现深拷贝,欢迎大家讨论。javascript中的对象是引用类型,在复制对象的时候就要考虑是用浅拷贝还是用深拷贝。直接赋值对象是引用类型,如果直接赋值给另外一个对象,那么只是赋值一个引用,实际上两个变量指向的同一个数据对象,如果其中一个对象的属性变更,那么另外一个也会变更。示
菜园前端 菜园前端
2年前
带你了解JS对象的浅拷贝和深拷贝
以下主要介绍了正常情况下的拷贝、浅拷贝、深拷贝三种方式的区别。正常拷贝:复制一个对象,它们的内存地址是相同的浅拷贝:拷贝对象的第一层属性深拷贝:拷贝对象多层的属性正常拷贝假设我们要复制一个对象,如果不对其进行深拷贝,那么改变其中一个对象后,另外一个对象也会
Souleigh ✨ Souleigh ✨
5年前
实现深拷贝的多种方式
实现深拷贝的多种方式简单来说,深拷贝主要是将另一个对象的属性值拷贝过来之后,另一个对象的属性值并不受到影响,因为此时它自己在堆中开辟了自己的内存区域,不受外界干扰。浅拷贝主要拷贝的是对象的引用值,当改变对象的值,另一个对象的值也会发生变化。1.简单深拷贝(一层浅拷贝)①for循环拷贝//只复制第一层的浅拷贝javascriptfunc
Stella981 Stella981
4年前
React之浅拷贝与深拷贝
 最近发现的一个bug让我从react框架角度重新复习了一遍浅拷贝与深拷贝。浅拷贝,就是两个变量都是指向一个地址,改变了一个变量,那另一个变量也随之改变。这就是浅拷贝带来的副作用,两个变量会相互影响到,因为它们指向同一个地址。深拷贝,就是互相独立,指向的是不同的地址,一个变量改变了,另一个变量不会被影响到。react角度:父组件传给
Wesley13 Wesley13
4年前
Java深拷贝和浅拷贝
1.浅复制与深复制概念⑴浅拷贝(浅克隆)   复制出来的对象的所有变量都含有与原来的对象相同的值,而所有的对其他对象的引用仍然指向原来的对象。⑵深拷贝(深克隆)   复制出来的所有变量都含有与原来的对象相同的值,那些引用其他对象的变量将指向复制出来的新对象,而不再是原有的那些被引用的对象。换言之,深复制
Stella981 Stella981
4年前
JavaScript的深拷贝和浅拷贝
一、数据类型数据分为基本数据类型(String,Number,Boolean,Null,Undefined,Symbol)和对象数据类型。、1.基本数据类型的特点:直接存储在栈(stack)中的数据2.引用数据类型的特点:存储的是该对象在栈中引用,真实的数据放在堆内存里。引用数据类型在栈中存储了指针,该指针指向堆中该实
Wesley13 Wesley13
4年前
Java 浅拷贝和深拷贝
!(https://oscimg.oschina.net/oscnet/b2f493d478242c24dc57d59ce17ceebb54f.jpg)前言Java中的对象拷贝(ObjectCopy)指的是将一个对象的所有属性(成员变量)拷贝到另一个有着相同类类型的对象中去。举例说明:比如,对象A和对象B都属于类S,具有
Stella981 Stella981
4年前
JavaScript基础心法——深拷贝和浅拷贝
!(https://oscimg.oschina.net/oscnet/c131215a5aaaeb7909d7398688df6ea6dcd.png)浅拷贝和深拷贝都是对于JS中的引用类型而言的,浅拷贝就只是复制对象的引用,如果拷贝后的对象发生变化,原对象也会发生变化。只有深拷贝才是真正地对对象的拷贝。前言说到深浅拷贝,必须先
linbojue linbojue
1个月前
Javascript学习(十二) --JS进阶(深浅拷贝,异常处理,处理this,性能优化,综合案例)
目录一.深浅拷贝​编辑1.浅拷贝2.深拷贝1.通过递归实现深拷贝2.lodash/cloneDeep3.通过JSON.stringify()实现二.异常处理1.throw抛异常2.try/catch捕获异常3.debugger三.处理this1.this指
码海清韵
码海清韵
Lv1
君不见高堂明镜悲白发,朝如青丝暮成雪。
文章
2
粉丝
0
获赞
0