JavaScript深拷贝和浅拷贝

Posted 子卿

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JavaScript深拷贝和浅拷贝相关的知识,希望对你有一定的参考价值。

深拷贝就是一份一模一样数据,且该数据和之前的数据断开连接,互不影响:

那么为什么会出现数据相互影响呢?这就涉及到javascript中的内存概念:栈 stack”和“堆 heap”,stack一般是静态分配内存,heap上一般是动态分配内存;堆是通过地址的指针传值,即传址;栈是直接传值。

了解了堆栈,我们再说说JavaScript的数据类型,JavaScript分原始类型和引用类型:

一、基本类型:

 基本类型:存放在栈内存中的简单数据段。数据大小确定,内存空间大小可以分配。

ECMAScript 有 5 种原始类型(primitive type),即 Undefined、Null、Boolean、Number 和 String。

对于基本类型数据,它们都是放在栈中,之间不会相互影响:如:

let a = b = \'ziChin\' // a -> "ziChin"; b -> "ziChin"
b = "子卿" // b -> "子卿"
console.log(a) // a -> "ziChin"

栈 “stack”中的数据不会相互影响。那么浅拷贝深拷贝说的什么数据呢?我们来讲讲JavaScript的另一种类型:引用类型:

 

二、引用类型

引用类型:存放在堆内存中的对象,变量中实际保存的是一个指针,这个指针指向另一个位置。每个空间大小不一样,要根据情况开进行特定的分配。
     当我们需要访问引用类型(如对象,数组,函数等)的值时,首先从栈中获得该对象的地址指针,然后再从堆内存中取得所需的数据。
栗子:
let a = [1,2,3]
let b = a // b ->  [1,2,3]

b.push(4) // b -> [ 1, 2, 3, 4 ]
console.log(a) // a -> [ 1, 2, 3, 4 ]

a.push(5) // a -> [ 1, 2, 3, 4, 5 ]
console.log(b) // b ->  [ 1, 2, 3, 4, 5 ]

 栗子"b = a"表示变量a把它的引用地址赋给了b,他们在堆中指向同一个内存地址;所以a或b中值的变化会相互影响,下面我画了一张图可以帮助大家理解:

其实可以这样理解:堆里有一所房间,a能进去,因为它有一把进入房间的钥匙;"b = a"时,我们给b也配了一把同样的钥匙,所以b也能进入房间了;房间还是那个房间,并没有新盖一所哦。怎么能够断掉ab之间的引用关系呢?请看下文:

了解了JavaScript的数据类型,我们进入正题:怎么实现浅拷贝和深拷贝

一、浅拷贝:

前面已经提到,在定义一个对象或数组时,变量存放的往往只是一个地址。当我们使用对象拷贝时,如果属性是对象或数组时,这时候我们传递的也只是一个地址。因此b对象在访问该属性时,会根据地址回溯到a对象指向的堆内存中,即a、b对象发生了关联,两者的属性值会指向同一内存空间。

 实现方法一:开循环

{ // -作用域-

function copy(parent) {
    if (typeof parent !== "object" && parent !== null){ // 基本类型直接返回
      return parent
  }
  let data = parent instanceof Array ? [] : {}
  for (var key in parent) { // arr和obj开循环拷贝赋值
      data[key] = parent[key]
  }
  return data
}
let a = [1,2,3]
let b = copy(a) // b ->  [1,2,3]
a.push(4) // a -> [1,2,3,4]
console.log(b) // b ->  [1,2,3]

} // -作用域-

实现方法二:assign方法

定义:Object.assign() 方法用于将所有可枚举属性的值从一个或多个源对象复制到目标对象。它将返回目标对象。
语法:Object.assign(target, ...sources)
适用于Object,栗子

{ // -作用域 -

let a = {"a": 1}
let b = Object.assign({},a) // b -> {"a": 1}
b.b = 2
console.log(b) // b -> {"a": 1, "b": 2}
console.log(a) // a -> {"a": 1}

} // -作用域 -

实现方法三:[...data]

 三个点运算符是ES6里的内容,适用于Array 和 Object,栗子:

{ // -作用域 -

let a = {"a": 1}
let b = {...a} // b -> {"a": 1}
b.b = 2
console.log(b) // b -> {"a": 1, "b": 2}
console.log(a) // a -> {"a": 1}

} // -作用域 -

实现方法四:concat

适用于数组,栗子:

{ // -作用域-

let a = [1,2,3]
let b = a.concat() // b ->  [1,2,3]
a.push(4) // a -> [1,2,3,4]
console.log(b) // b ->  [1,2,3]

} // -作用域-

关于浅拷贝方法,我们先列举以四个栗子,可能还有其他的实现方法,等以后想到,会陆续补充。。。好了我们接着来看深拷贝:

二、深拷贝

对于浅拷贝,有个隐患:假如源对象的属性值是一个指向对象的引用,它也只拷贝那个引用值。

或许以上并不是我们在实际编码中想要的结果,我们不希望a和b对象之间产生关联,那么这时候可以用到深拷贝。既然属性值类型是数组和对象时只会传址,那么我们就用递归来解决这个问题,把a对象中所有属于对象的属性类型都遍历赋给b对象即可。测试代码如下:

通用方法一:递归

{ // -作用域-

function copy(parent) {
    if (typeof parent !== "object" && parent !== null){ // 基本类型直接返回
      return parent
  }
  let data = parent instanceof Array ? [] : {}
  for (var key in parent) { // arr和obj开循环拷贝赋值
      data[key] = copy(parent[key])
  }
  return data
}
let a = {"a": {"aa": 1}, "b": [1,2,3], "c": 3}
let b = {}
b = copy(a) // b -> {"a": {"aa": 1}, "b": [1,2,3], "c": 3}
b.a.aa = 2
b.b.push(4)
console.log(b) // b -> {"a": {"aa": 2}, "b": [1,2,3,4], "c": 3}
console.log(a) // a -> {"a": {"aa": 2}, "b": [1,2,3,4], "c": 3}

} // -作用域-

let a = {"a": {"aa": 1}, "b": [1,2,3], "c": 3} 赋给b的递归过程:

循环第一项a["a"]时,data[key] 是对象{"aa": 1},data["a"] = copy函数自调copy(parent[a])的返回值。

即copy( {"aa": 1}),此时进入下一层:当前的parent是1基本类型,data["aa"] = 1,返回 {"aa": 1}并赋给上一层。
我们回到上一层后:data["a"] =  {"aa": 1}

同理,循环第二项a["b"]时:data["b"] = [1,2,3]

循环第三项a["c"]时,直接得到data["c"] = 3,递归完成。

可能有点绕,大家多写多用就习惯了。

方法二:JSON.stringify

{ // -作用域-

let a = {"a": {"aa": 1}, "b": [1,2,3], "c": 3}
let b = {}
b = JSON.parse(JSON.stringify(a)) // b -> {"a": {"aa": 1}, "b": [1,2,3], "c": 3}
b.a.aa = 2
b.b.push(4)
console.log(b) // b -> {"a": {"aa": 2}, "b": [1,2,3,4], "c": 3}
console.log(a) // a -> {"a": {"aa": 2}, "b": [1,2,3,4], "c": 3}

} // -作用域-

这个就好理解了,JSON.stringify(a) 得到一个字符串,字符串是基本类型切断了原引用关系;然后我们解析JSON字符串就好。

这里有个小问题,就是如果a属性是函数时,JSON.stringify()不转的,请看到代码:

{ // -作用域-

let a = {"a": {"aa": 1}, "b": [1,2,3], "c": 3, d: function(){}}
let b = {}
b = JSON.parse(JSON.stringify(a))
console.log(b) // b -> {"a": {"aa": 2}, "b": [1,2,3,4], "c": 3}
console.log(a) // a -> {"a": {"aa": 2}, "b": [1,2,3,4], "c": 3, d: ƒ}

} // -作用域-

a对象中属性值d内容为函数,在a赋值给b的过程中

d: ƒ

飞走咯,嘿嘿(*^▽^*)

 

 

 

 

 

以上是关于JavaScript深拷贝和浅拷贝的主要内容,如果未能解决你的问题,请参考以下文章

JavaScript的深拷贝和浅拷贝

深入剖析javaScript中的深拷贝和浅拷贝

JavaScript 的 深拷贝和浅拷贝

javascript实现引用数据类型的深拷贝和浅拷贝详解

JavaScript深拷贝和浅拷贝

javascript深拷贝和浅拷贝以及实现方法(推荐)