JS的深浅拷贝

一、数据类型

 数据分为基本数据类型(String, Number, Boolean, Null, Undefined,Symbol)和 引用数据类型(统称为 Object类型,细分的话有:Object Array Date RegExp Function… )。

 基本数据类型的特点:直接存储在栈(stack)内存中

 引用数据类型的特点:真实的数据存放在堆内存中,在栈中存储了指针,该指针指向堆中该实体的起始地址。当解释器寻找引用值时,会首先检索其在栈中的地址,取得地址后从堆中获得实体。

 基本数据类型是不可变的,任何方法都无法改变一个基本数据类型的值,也不可以给基本数据类型添加属性或方法。但是可以为引用数据类型添加属性和方法,也可以删除其属性和方法。

 为什么要分两种保存方式呢? 因为保存在栈内存的必须是大小固定的数据,引用类型的大小不固定,只能保存在堆内存中,但是我们可以把它的地址写在栈内存中以供我们访问。

二、区分浅拷贝与深拷贝

拷贝其实就是对象复制,为了解决对象复制是产生的引用类型问题。

浅拷贝:利用迭代器,循环对象将对象中的所有可枚举属性复制到另一个对象上,但是浅拷贝有一个问题就是只拷贝了对象的一级,其他级如果还是引用类型的话依旧解决不了问题

深拷贝:深拷贝解决了浅拷贝的问题,利用递归的形势遍历对象的每一级,实现起来较为复杂,得判断值是数组还是对象,简单的说就是,在内存中存在两个数据结构完全相同又相互独立的数据,将引用类型的值进行复制,而不是只复制其引用关系,能够实现真正意义上的对象的拷贝。

三、赋值和浅拷贝的区别

当我们把一个对象赋值给一个新的变量时,赋的其实是该对象在栈中的地址,而不是堆中的数据。也就是两个对象指向的是同一个存储空间,无论哪个对象发生改变,其实都是改变的存储空间的内容,因此,两个对象是联动的。

浅拷贝是按位拷贝对象,它会创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值;如果属性是内存地址(引用类型),拷贝的就是内存地址 ,因此如果其中一个对象改变了这个地址,就会影响到另一个对象。即:默认拷贝构造函数只是对对象进行浅拷贝复制(逐个成员依次拷贝),即只复制对象空间而不复制资源。

我们先来看两个例子,对比一下赋值与浅拷贝分别会对原对象带来哪些改变

对象赋值:
var
obj1 = { name:'zhang', msg:{ age:18 } } var obj2 = obj1 obj2.name = 'wang' obj2.msg.age = 20 console.log('obj1',obj1) console.log('obj2',obj2)

浅拷贝:
var obj1 = {
    name:'zhang',
    msg:{
        age:18
    }
}
var obj3 = shallowCopy(obj1)
obj3.name = 'wang'
obj3.msg.age = 20
function shallowCopy(obj) {
    var res = Array.isArray(obj) ? [] : {}
for (var key in obj) {
if (obj.hasOwnProperty(key)) {
res[key] = obj[key]
}
     }
     return res
}
console.log(
'obj1',obj1) console.log('obj3',obj3)

 

 

 上面例子中,obj1是原始数据,obj2是赋值操作得到的,而obj3是浅拷贝得到的。我们可以很清晰的看到对原始数据的影响。

四、浅拷贝的实现方式

1.Object.assign( )、Object.create( )、Object解构

Object.assign() 方法可以把任意多个源对象自身的可枚举属性拷贝给目标对象,然后返回目标对象。但是 Object.assign()进行的是浅拷贝,拷贝的是对象的属性的引用,而不是对象本身。

Object.assign():
var
obj = { a: {name: 'zhang', age: 30} } var newObj = Object.assign({}, obj) newObj.a.name = "wang" console.log(obj.a.name)//wang
Object.create():
var
obj = { a: {name: 'zhang', age: 30} } var newObj = Object.create(obj) newObj.a.age = 25 console.log(obj.a.age)//25
解构:
var
obj = { a: {name: 'zhang',sex:'男'} } var newObj = {...obj} newObj.a.sex = '女' console.log(obj.a.sex)//

注意:当object只有一层的时候,是深拷贝

Object.assign():
var obj = { name: "zhang" }

var newObj = Object.assign({}, obj)

newObj.name = "wang"

console.log(obj.name)//zhang
Object.create():
var obj = { age: 20 }

var newObj = Object.create(obj)

newObj.age = 30

console.log(obj.age)//20
解构:
var obj = { sex: '男' }

var newObj = {...obj}

newObj.sex = '女'

console.log(obj.sex)//

2.Array.prototype.concat( )、Array.prototype.slice( )

修改新对象会改到原对象:

concat():
var
arr1 = [1, 2, { name: 'zhang' }]
var arr2 = arr1.concat()
arr2[
2].name = 'wang'
console.log(arr1[2].name)//wang
slice():
var
arr1 = [1, 2, { name: 'zhang' }] var arr2 = arr1.slice() arr2[2].name = 'wang' console.log(arr1[2].name)//wang

Array的slice和concat方法不修改原数组,只会返回一个浅复制了原数组中的元素的一个新数组。

原数组的元素会按照下述规则拷贝:

  • 如果该元素是个对象引用(不是实际的对象),slice 会拷贝这个对象引用到新的数组里。两个对象引用都引用了同一个对象。如果被引用的对象发生改变,则新的和原来的数组中的这个元素也会发生改变。
  • 对于字符串、数字及布尔值来说(不是 String、Number 或者 Boolean 对象),slice 会拷贝这些值到新的数组里。在一个数组里修改这些字符串或数字或是布尔值,将不会影响另一个数组。(若拷贝数组是纯数据(不含对象),可以通过concat() 和 slice() 来实现深拷贝)

五、深拷贝的实现方式

1.通过 JSON 反序列化来实现:JSON.parse(JSON.stringify( ))

原理: 用JSON.stringify将对象转成JSON字符串,再用JSON.parse()把字符串解析成对象,一去一来,新的对象产生了,而且此对象会开辟新的栈,实现深拷贝。

var arr1 = [1, 2, { name: 'zhang' }]

var arr2 = JSON.parse(JSON.stringify(arr1))

arr2[2].name = 'wang'

console.log(arr1[2].name)//zhang

缺点:因为JSON.stringify() 方法是将一个JavaScript值(数组或对象)转换为一个 JSON字符串,所以这种方法只能实现数组和对象的深拷贝,而不能处理函数和其他数据类型

2.手写递归克隆方法

递归方法实现深度克隆原理:遍历对象、数组直到里边都是基本数据类型,然后再去复制,就是深度拷贝

通过JSON 反序列化进行深拷贝除了不支持数组和对象以外的数据类型,也不能处理内部循环引用的问题,先来看一下什么是循环引用:

 

var obj = {a: '123'}
obj.b = obj

 

为了解决循还引用,我们建立一个集合[],每次遍历对象进行比较,如果[]中已存在,则证明出现了循环引用或者相同引用,我们直接返回该对象已复制的引用即可

 

class DeepClone {
  constructor() {
    this.cacheList = []  
  }
  clone(target) {
      if(typeof target !== 'object'){
        return target
      }
      const cache = this.findCache(target)
      if (cache) {
          return cache // 如果找到缓存,证明存在循环引用或属性引用了相同对象,直接返回引用就可以了
      } else {
            if(target instanceof Array){
                var result = []
            }else{
                var result = {}
            }
            this.cacheList.push([target, result])// 把源对象和新对象放进缓存列表
            for (let key in target) {
                if (target.hasOwnProperty(key)) { // 不拷贝原型上的属性,太浪费内存
                    if(typeof target[key] !== 'object'){
                        result[key] = target[key]  // 不是引用类型,则复制值
                    }else{
                        result[key] = this.clone(target[key]) //递归遍历复制对象 
                    }
                }
            }
            return result
        }
  }
  findCache(source) {
    for (let i = 0; i < this.cacheList.length; i++) {
      if (this.cacheList[i][0] === source) {
        return this.cacheList[i][1]; // 如果有环,返回对应的新对象
      }
    }
    return undefined;
  }
}

 

var obj1 = {
        name:'zhang',
        msg:{
            age:18
        }
    }
var deepClone = new DeepClone()
var obj2 = deepClone.clone(obj1)
obj2.name = 'wang' 
obj2.msg.age = 20
console.log('obj1',obj1)
console.log('obj2',obj2)

 

3.函数库lodash

该函数库提供了_.cloneDeep用来做深拷贝

var _ = require('lodash')
var obj1 = {a: { a: 10 }}
obj2 = _.cloneDeep(obj1)
obj2.a.a = 20
console.log(obj1.a.a)//10

 

总结:简单需求用 JSON 反序列化,复杂需求用递归克隆或lodash函数库。

 

posted @ 2020-12-16 16:39  奥利奥ALA  阅读(149)  评论(0)    收藏  举报