详细说说数组的浅拷贝和深拷贝、对象的浅拷贝和深拷贝

150 篇文章 3 订阅
12 篇文章 1 订阅

一、基本类型和引用类型

1、ECMAScript 中的变量类型分为两类:

① 基本类型

undefined, null, 布尔值(Boolean), 字符串(String), 数值(Number),Symbol

② 引用类型 

统称为Object类型。细分的话有:Object类型,Array类型,Date类型,Function类型等。 

2、不同类型的存储方式:

① 基本数据类型 保存在 栈内存

形式如下:栈内存中分别存储着变量的标识符以及变量的值。

引用类型 保存在 堆内存 

栈内存存储的是变量的标识符以及对象在堆内存中的存储地址。当需要访问引用类型(如对象,数组等)的值时,首先从栈中获得该对象的地址指针,然后再从对应的堆内存中取得所需的数据。

3、不同类型的复制方式:

① 基本类型的复制 

当你在复制基本类型的时候,相当于把值也一并复制给了新的变量。

 ② 引用类型的复制

当你在复制引用类型的时候,实际上只是复制了指向堆内存的地址。即原来的变量与复制的新变量指向了同一个东西。 

二、所谓的深浅拷贝

1、仅仅是复制了引用(地址)。换句话说,复制了之后,原来的变量和新的变量指向同一个东西,彼此之间的操作会互相影响,为 浅拷贝(藕断丝连)

2、而如果是在堆中重新分配内存,拥有不同的地址,但是值是一样的,复制后的对象与原来的对象是完全隔离,互不影响,为 深拷贝

3、深浅拷贝的主要区别就是:复制的是引用(地址)还是复制的是实例。

4、由深拷贝的定义来看,深复制要求如果源对象存在对象属性,那么需要进行递归复制,从而保证复制的对象与源对象完全隔离然而还有一种可以说处在浅复制和深复制的粒度之间,也是jQuery的extend方法在deep参数为false时所谓的“浅复制”,这种复制只进行一个层级的复制

三、实现深拷贝的方法

1、原理 

不是简单的拷贝引用,而是在堆中重新分配内存,并且把源对象实例的所有属性都进行新建复制,以保证深拷贝的对象的引用不包含任何原有对象。

简单数据类型:直接拷贝值;

引用数据类型:在堆内存中开辟一块内存用于存放复制的对象,并把原有的对象类型数据拷贝过来,这两个对象相互独立,属于两个不同的内存地址。修改其中一个,另一个不会发生改变。

2、方法一:JSON对象的 parse 和 stringify          

JSON.parse(JSON.stringify(obj))  目前比较常用的深拷贝方法之一

先将JS对象序列化为JSON字符串,再将JSON字符串反序列化为JS对象。改变对象的格式,使其不指向同一个对象。

原理:

JSON 对象是 ES5 中引入的新的类型(支持的浏览器为 IE8+);

parse 方法可以将 JSON 字符串反序列化成 JS 对象;stringify 方法可以将 JS 对象序列化成JSON 字符串。

借助这两个方法,也可以实现对象的深拷贝。

let bbb = [1,[2,3,4]]
let ccc = JSON.parse(JSON.stringify(bbb))
ccc[1][0] = 222
console.log(bbb, ccc) // [1,[2,3,4]]   [1,[222,3,4]]

优点:

这个方法使用较为简单便捷,可以满足基本的深拷贝需求(80%),而且能够处理 JSON 格式能表示的所有数据类型。

缺点:

拷贝的对象的值中如果有 undefined、函数、symbol 这些类型,JSON.stringify 序列化之后的字符串中这个键值对会消失 

拷贝 Date 引用类型会变成字符串

拷贝 RegExp 引用类型会变成空对象

对象中含有 NaN、Infinity 以及 -lnfinity,JSON 序列化的结果会变成 null

无法拷贝不可枚举的属性、无法拷贝对象的原型链

无法拷贝对象的循环应用,即对象成环(obj[key] = obj)

    let a = [{
      'aa': undefined,
      'bb': [1, 2, 3],
      'cc': null,
      'dd': new Date(),
      'ee': () => {}
    }]
    let b = JSON.parse(JSON.stringify(a))
    console.log(a, b)


    打印结果:
      a的打印结果: 
        aa:undefined
        bb:[1, 2, 3]
        cc:null
        dd:Wed Mar 13 2019 09:46:13 GMT+0800 (中国标准时间)
        ee:ƒ ee()
      
      b的打印结果:
        bb:[1, 2, 3]
        cc:null
        dd:Wed Mar 13 2019 09:46:13 GMT+0800 (中国标准时间)

3、方法二: jQuery 中的 extend 复制方法

原理:jQuery中的 extend 方法可以用来扩展对象。这个方法可以传入一个参数 deep(true or false),表示是否执行深复制(如果是深复制则会执行递归复制)

false:浅拷贝(只会进行一个层级的复制)即如果源对象中存在对象属性,那么复制的对象上也会引用相同的对象。这不符合深复制的要求,但又比简单的复制引用的复制粒度有了加深。

缺点:这个方法基本的思路就是如果碰到 array 或者 objec t的属性,那么就执行递归复制。这也导致对于 Date、Function等引用类型,jQuery 的 extend 也无法支持

let a=[0,1,[2,3],4],
    b=$.extend(true,[],a);
a[0]=1;
a[2][0]=1;
console.log(a,b);   // [1,1,[1,3],4]   [0,1,[2,3],4]

4、函数库 lodash 的 _.cloneDeep 方法 

var _ = require('lodash');
var obj1 = {
    a: 1,
    b: { f: { g: 1 } },
    c: [1, 2, 3]
};
var obj2 = _.cloneDeep(obj1);
console.log(obj1.b.f === obj2.b.f);// false

5、手写实现深拷贝

原理:利用 递归 来实现深复制,对属性中所有引用类型的值,遍历到是基本类型的值为止

      function deepClone(obj) {
        let objClone = Array.isArray(obj) ? [] : {};

        if (obj instanceof Object === false) return obj;
          for (let key in obj) {
            const value = obj[key];
            // for-in遍历的是原型链,需要用 hasOwnProperty 判断是否是自有属性
            if (obj.hasOwnProperty(key)) {
              if (value && typeof value === "object") {
                // 递归复制
                objClone[key] = deepClone(value);
              } else {
                // 简单复制
                objClone[key] = value;
              }
            }
          }
          return objClone;
        } else {
          return obj;
        }
      }
      console.log('使用自定义的深拷贝')
      let arrA = [1, 2, 3]
      let arrAcopy = deepClone(arrA)
      arrAcopy[0] = 111
      console.log(arrA, arrAcopy) // (3) [1, 2, 3] (3) [111, 2, 3]

      let array2 = [1, [2, 3, 4]]
      let array2Copy = deepClone(array2)
      array2[0] = 111
      console.log(array2, array2Copy) // [111,[2,3,4]]   [1,[2,3,4]]

      let stringA = 11
      let aCopy = deepClone(stringA)
      stringA = 22
      console.log(stringA, aCopy) // 22 11

      let stringB = null
      let bCopy = deepClone(stringB)
      console.log(bCopy) // null

虽然利用递归能实现一个深拷贝,但是同上面的 JSON.stringfy 一样,还是有一些问题没有完全解决,例如:

  • 这个深拷贝函数并不能复制不可枚举的属性以及 Symbol 类型
  • 这种方法只是针对普通的引用类型的值做递归复制,而对于 Array、Date、RegExp、Error、Function 这样的引用类型并不能正确地拷贝;
  • 对象的属性里面成环,即循环引用没有解决
  • 递归的深度太深,会引发栈内存的溢出

 

6、解决递归爆栈的问题

将待拷贝的对象放入栈中,循环直至栈为空。

https://www.yuque.com/12312wo/vvtmcs/zo3hfhzk45b7452s#PnQ3J 

这样我们就解决了递归爆栈的问题,但是循环引用的问题依然存在。例如:当a对象的中的某属性值为a对象,这样就会造成循环引用。

7、解决循环引用的问题 

使用暴力破解的方法来解决。

四、实现浅拷贝的方法

1、原理

复制后的引用都是指向同一个对象的实例,彼此之间的操作会互相影响

一个新的对象对原始对象的属性值进行精确地拷贝:

如果拷贝的是基本数据类型,拷贝的就是基本数据类型的值;

如果拷贝的是引用数据类型,拷贝的就是内存地址。如果其中一个对象的引用内存地址发生改变,另一个对象也会发生变化。

2、方法一:直接赋值

let aa = [1,2,3]
let bb = aa
bb[1] = 22
console.log(aa,bb) // (3) [1, 22, 3] (3) [1, 22, 3]

3、方法二:ES6 拓展运算符 ...

将数组或者对象转为用逗号分隔的参数序列

语法:let cloneObj={...obj}

let obj1 = {a:1,b:{c:1}}
let obj2 = {...obj1};
obj1.a = 2;
console.log(obj1); //{a:2,b:{c:1}}
console.log(obj2); //{a:1,b:{c:1}}
obj1.b.c = 2;
console.log(obj1); //{a:2,b:{c:2}}
console.log(obj2); //{a:1,b:{c:2}}
    let arr = [1,2,3,4]
    let list = [...arr]
    list[0] = '111'
    console.log(arr,list)

// arr的打印结果:
// [1, 2, 3, 4]

// list的打印结果:
// ["111", 2, 3, 4]

ES6的拓展运算符,也叫解构赋值,也可以实现数据的拷贝,但是只遍历一层

针对一维对象和数组可以看做是深拷贝,多维的就是浅拷贝 

    let arr = [{
      name: '小草莓',
      hobby: ['a', 'b', 'c']
    }]
    let list = [...arr]
    list[0].hobby = ['a', 'b', 'c', 'd']
    list[0].name = 'new 小草莓'
    console.log(arr)
    console.log(list)

// arr的打印结果:
// [{
//    name: 'new 小草莓',
//    hobby: ['a', 'b', 'c', 'd']
// }]

// list的打印结果
// [{
//    name: 'new 小草莓',
//    hobby: ['a', 'b', 'c', 'd']
// }]

缺点:

扩展运算符和 object.assign 有同样的缺陷,也就是实现的浅拷贝的功能差不多,但是如果属性都是基本类型的值,使用扩展运算符进行浅拷贝会更加方便。

  • 它不会拷贝对象的继承属性;
  • 它不会拷贝对象的不可枚举的属性;
  • 可以拷贝 Symbol 类型的属性。
let obj={
  name:"lala",
  [Symbol('key1')]:"yyy"
}
Object.defineProperty(obj,'notShow',{
  value:"不可枚举属性",
  enumerable:false
})
let obj2={...obj}
console.log(obj2) //{name: 'lala', Symbol(key1): 'yyy'} 没有不可枚举属性

4、方法三:js的 slice() 方法

arrayObj.slice(start, [end])

原理:数组中的 slice() 方法返回指定截取的一段,原数组不改变。当 start 为0素,end 不填则默认复制整个数组。

    let c = [1,2,3,4,5]
    let d = c.slice(0)
    d[2] = 333
    console.log(c,d) // (5) [1, 2, 3, 4, 5] (5) [1, 2, 333, 4, 5]
let arr = [1,2,3,4];
console.log(arr.slice()); // [1,2,3,4]
console.log(arr.slice() === arr); //false

 缺点:Array 的 slice 和 concat 方法都会返回一个新的数组实例,但是这两个方法对于数组中的对象元素却没有执行深复制,而只是复制了引用了,因此这两个方法并不是真正的深复制 

      let cc = [1,[2,3,4]]
      let ee = cc.slice(0)
      ee[1][0] = 222
      console.log(cc,ee) // [1,[222,3,4]] [1,[222,3,4]]

5、方法四:js的 concat() 方法

arrayObject.concat(arrayX,arrayX,......,arrayX)

原理:数组中的 concat() 方法用于合并两个或多个数组,返回合并之后的数组,原数组不改变。

    let e = [1,2,3,4]
    let f = e.concat()
    f[1] = 222
    console.log(e,f)  // (4) [1, 2, 3, 4] (4) [1, 222, 3, 4]
let arr = [1,2,3,4];
console.log(arr.concat()); // [1,2,3,4]
console.log(arr.concat() === arr); //false

缺点:Array的 slice 和 concat 方法都会返回一个新的数组实例,但是这两个方法对于数组中的对象元素却没有执行深复制,而只是复制了引用了,因此这两个方法并不是真正的深复制

      let g = [1,[2,3,4]]
      let h = g.concat()
      h[1][0] = 222
      console.log(g,h) // [1,[222,3,4]]  [1,[222,3,4]]

6、方法五:Object.assign(target, …sources)

原理:把任意多个的源对象自身的可枚举属性拷贝给目标对象,然后返回目标对象

语法:Object.assign(target, ...sources)。

该方法的第一个参数是拷贝的目标对象,后面的参数是拷贝的来源对象(也可以是多个来源)。

用途:实现浅拷贝,也可以实现一维对象的深拷贝。  

object.assign 是 ES6 中 Object 的一个方法,该方法可以用于 JS 对象的合并等多个用途,其中一个用途就是可以进行浅拷贝。 

      console.log('对象浅拷贝')

      let arr = [1,2,3,4]
      let obj1 = { a: 1, b: 2, c: 3 }
      let obj2 = { a: 11, b: 22, c: 33, d:arr, e: 44 }
      let obj3 = Object.assign({}, obj1, obj2)

      obj1.a = 'aaa'
      obj2.d[0] = 'bbb'

      obj2.e = 'eee'

      console.log(obj1, obj2, obj3)
      // {a: "aaa", b: 2, c: 3}
      // {a: 11, b: 22, c: 33, d: ['bb',2,3,4], e: 'eee'}
      // {a: 11, b: 22, c: 33, d: ['bb',2,3,4], e: 44}

注意:

  • 如果目标对象和源对象有同名属性,或者多个源对象有同名属性,则后面的属性会覆盖前面的属性。
  • 如果该函数只有一个参数,当参数为对象时,直接返回该对象;当参数不是对象时,会先将参数转为对象然后返回。
  • 因为 nullundefined 不能转化为对象,所以第一个参数不能为 nullundefined,会报错。
  • 它不会拷贝对象的继承属性,不会拷贝对象的不可枚举的属性,可以拷贝 Symbol 类型的属性。
let obj={
  name:"lala",
  [Symbol('key1')]:"yyy"
}
Object.defineProperty(obj,'notShow',{
  value:"不可枚举属性",
  enumerable:false
})
let obj2=Object.assign({},obj)
console.log(obj)
console.log(obj2) //{name: 'lala', Symbol(key1): 'yyy'} 没有不可枚举属性

 

7、手写实现浅拷贝

实现一个浅拷贝的大致思路分为两点:

  • 对基础类型做一个最基本的一个拷贝;
  • 对引用类型开辟一个新的存储,并且拷贝第一层对象属性。
      function shallowCopy(val) {
        if (!val || typeof val !== "object") {
          return val;
        }
        let newVal = Array.isArray(val) ? [] : {};
        for (let key in val) {
          if (val.hasOwnProperty(key)) {
            // for-in遍历的是原型链
            // 需要用hasOwnProperty判断是否是自有属性
            newVal[key] = val[key];
          }
        }
        return newVal;
      }

      let arr1 = [1, 2];
      const arr2 = shallowCopy(arr1);
      arr1[1] = 22;
      console.log(arr1, arr2); // [1, 22]   [1, 2]

      let arr11 = [1, [1, 2]];
      const arr22 = shallowCopy(arr11);
      arr11[1][0] = 11;
      console.log(arr11, arr22); // [1, [11, 2]]   [1, [11, 2]]

五、自己实现一个拷贝方法,通过传入deep参数来表示是否执行深拷贝

      // util作为判断变量具体类型的辅助模块
      var util = (function() {
        var class2type = {};

        [('Null',
          'Undefined',
          'Number',
          'Boolean',
          'String',
          'Object',
          'Function',
          'Array',
          'RegExp',
          'Date')
        ].forEach(function(item) {
          class2type['[object ' + item + ']'] = item.toLowerCase()
        })

        function isType(obj, type) {
          return getType(obj) === type
        }

        function getType(obj) {
          return class2type[Object.prototype.toString.call(obj)] || 'object'
        }

        return {
          isType: isType,
          getType: getType
        }
      })()


      function copy(obj, deep) {
        // 如果obj不是对象,那么直接返回值就可以了
        if (obj === null || typeof obj !== 'object') {
          return obj
        } 

        // 定义需要的局部变量,根据obj的类型来调整target的类型
        var i,
          target = util.isType(obj, 'array') ? [] : {},
          value,
          valueType

        for (i in obj) {
          value = obj[i]
          valueType = util.getType(value) 
           // 只有在明确执行深复制,并且当前的value是数组或对象的情况下才执行递归复制
          if (deep && (valueType === 'array' || valueType === 'object')) {
            target[i] = copy(value)
          } else {
            target[i] = value
          }
        }
        return target
      }

更过例子可参考:js 深克隆(考虑到类型检查,递归爆栈,相同引用,Date和Function等特殊类型克隆,原型克隆)_js克隆对象中的functiion的问题_一袋米要扛几楼_的博客-CSDN博客

 

六、手写实现深拷贝改进版

https://www.yuque.com/12312wo/vvtmcs/hnbcpp6iuku6g2qb

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值