如何彻底理解深浅拷贝?

218 阅读7分钟

最近刷掘金看到axuebin大佬的 2020年前端面试复习必读文章,让我受益匪浅。所以决定开始写一些基础文档,提升一下自己,顺便回顾一下原生JS.

基本数据类型

javascript 中的数据类型分为基础数据类型和引用数据类型

基础数据类型

指的是那些保存在栈内存中的简单数据段,这种值完全保存在内存中的一个位置,包含 String、Number、Boolean、Null、Undefined、Symbol他们是直接存放的,所以可以直接访问

引用数据类型

指的是那些保存在堆内存中的对象,所以引用类型的值保存的是一个指针,这个指针指向存储在堆中的一个对象。除了上面的 6 种基本数据类型外,剩下的就是引用类型了,统称为 Object 类型。细分的话,有: Object 类型、Array 类型、Date 类型、RegExp 类型、Function 类型等。当我们需要访问这三种引用类型的值时,首先得从栈中获得该对象的地址指针,然后再从堆内存中取得所需的数据。

JS中的堆内存和栈内存

在js引擎中对变量的存储主要有两种位置,堆内存和栈内存。

和java中对内存的处理类似,栈内存主要用于存储各种基本类型的变量,包括Boolean、Number、String、Undefined、Null,**以及对象变量的指针,这时候栈内存给人的感觉就像一个线性排列的空间,每个小单元大小基本相等。

而堆内存主要负责像对象Object这种变量类型的存储,如下图

为什么基础数据类型存在栈中,而引用数据类型存在堆中呢?

  1. 堆比栈大,栈比对速度快。
  2. 基础数据类型比较稳定,而且相对来说占用的内存小。
  3. 引用数据类型大小是动态的,而且是无限的。
  4. 堆内存是无序存储,可以根据引用直接获取。

赋值

将一个数值或者对象赋值给另一个变了的过程

基本数据类型

对于基本数据类型进行复制,系统会自动为新的变量在内存中分配地址,赋值之后2个变量互不影响;

var a = 'haha';
var b= a;
console.log(b) //haha
a = 'heihei'
console.log(a,b) //heihei haha

引用数据类型 系统也会自动为新的变量在栈内存中分配一个值,但这个值仅仅是一个地址。也就是说,复制出来的变量和原有的变量具有相同的地址值,指向堆内存中的同一个对象,所以互相之间有影响!

var a = {
    name: 'haha',
    age: '23',
    sex: 'boy'
}
var b = a
console.log(b)
a.name = 'heihei';
console.log(a)
console.log(b)

浅拷贝(Shallow Copy)

什么是浅拷贝?

创建一个新的对象,这个对象有着原始对象属性值的一份精确拷贝。如果原始数据是基本类型,拷贝的就是基本类型的值,如果原始数据是引用类型,拷贝的就是原始数据的内存地址,所以说其中一个对象改变了这个地址,就会影响另一个对象。

上图中,SourceObject 是原对象,其中包含基本类型属性 field1 和引用类型属性 refObj。浅拷贝之后基本类型数据 field2 和 filed1 是不同属性,互不影响。但引用类型 refObj 仍然是同一个,改变之后会对另一个对象产生影响。

简单来说可以理解为浅拷贝只解决了第一层的问题,拷贝第一层的基本类型值,以及第一层的引用类型地址。

对象的浅拷贝

Object.assign

方法用于将所有可枚举的属性值从一个或者多个源对象赋值到目标对象,然后返回目标对象。

var a = {
    name: 'haha',
    age: '23',
    sex: 'boy',
    option: {
        address: '陕西省西安市'
    }
}
var b = Object.assign({},a)
a.name = 'heihei';
a.option.address= '山西省太原市'
console.log(a)
console.log(b)

修改a的name属性,b没有变化,是因为a的name属性值是一个基础数据类型,复制后2个变量互补影响,修改a的address属性,a和b的address属性都引起了改变,是因为option的属性值是一个引用数据类型,复制后同指向同一个地址,所以一个修改后,另一个也会跟着变化。

由此可见:如果对象的属性值为简单类型(如:string、number...),使用Object.assign({},srcObj),得到新的对象时深拷贝,如果属性值为对象或者其他引用类型,那么对于这个对象而言是浅拷贝。

使用扩展运算符...(spread)

var a = {
    name: 'haha',
    age: '23',
    sex: 'boy',
    option: {
        address: '陕西省西安市'
    }
}
var b = {...a}
a.name = 'heihei';
a.option.address= '山西省太原市'
console.log(a)
console.log(b)

可以看出效果和使用Object.assign()是一样的。

数组的浅拷贝

array.slice(start, end)

start: 可选。规定从何处开始选取。如果是负数,那么它规定从数组尾部开始算起的位置。也就是说,-1 指最后一个元素,-2 指倒数第二个元素,以此类推。

end: 可选。规定从何处结束选取。该参数是数组片断结束处的数组下标。如果没有指定该参数,那么切分的数组包含从 start 到数组结束的所有元素。如果这个参数是负数,那么它规定的是从数组尾部开始算起的元素。

let a = [0, '1', [2,3]]
let b = a.slice(1); //从下标为1的地方开始选取
console.log(b)
a[1] = '66'
a[2][1] = '666'
console.log(a)
console.log(b)

可以看出,改变 a[1] 之后 b[0] 的值并没有发生变化,但改变 a[2][0] 之后,相应的 b[1][0] 的值也发生变化。说明 slice() 方法是浅拷贝。

Array.prototype.concat()

var arr = ['11', '22', '33'];
var arrCopy = arr.concat();
arr[0] = '666'
console.log(arr)  //["666", "22", "33"]
console.log(arrCopy)  //["11", "22", "33"]

综上, Array的slice和concat方法并不是真正的深拷贝,对于Array的第一层的元素是深拷贝,而Array的第二层 slice和concat方法是复制引用。所以,Array的slice和concat方法都是浅拷贝。

深拷贝

什么是深拷贝?

深拷贝会拷贝所有的属性,并拷贝属性指向的动态分配的内存。当对象和它所引用的对象一起拷贝时即发生深拷贝。深拷贝相比于浅拷贝速度较慢并且花销较大。拷贝前后两个对象互不影响。

JSON.stringify 和 JSON.parse: 用 JSON.stringify 把对象转换成字符串,再用 JSON.parse 把字符串转换成新的对象。

//通过js的内置对象JSON来进行数组对象的深拷贝
function deepClone(obj) {
  let _obj = JSON.stringify(obj);
  let objClone = JSON.parse(_obj);
  return objClone;
}
var a = {
    name: '张三',
    age: '23',
    sex: 'boy',
    option: {
        address: '陕西省西安市'
    }
}
var b = JSON.parse(JSON.stringify(a));
a.name = '李四';
a.option.address = '山西省太原市'
console.log(a)
console.log(b)

可以转成 JSON 格式的对象才能使用这种方法,如果对象中包含 function 或 RegExp 这些就不能用这种方法了。

通过jQuery的extend方法实现深拷贝:

let $ = require('jquery');
let obj1 = {
   a: 1,
   b: {
     f: {
       g: 1
     }
   },
   c: [1, 2, 3]
};
let obj2 = $.extend(true, {}, obj1);

2. 数组深拷贝

let a = [0, "1", [2, 3]];
let b = JSON.parse(JSON.stringify( a.slice(1) ));
console.log(b);
// ["1", [2, 3]]

a[1] = "99"; a[2][0] = 4; console.log(a); // [0, "99", [4, 3]] console.log(b); // ["1", [2, 3]]

3. lodash.cloneDeep()实现深拷贝

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

4. 使用递归的方式实现深拷贝

function _deepClone(source) {
  let target;
  if (typeof source === 'object') {
    target = Array.isArray(source) ? [] : {}
    for (let key in source) {
      if (source.hasOwnProperty(key)) {
        if (typeof source[key] !== 'object') {
          target[key] = source[key]
        } else {
          target[key] = _deepClone(source[key])
        }
      }
    }
  } else {
    target = source
  }
  return target
}

总结

如果对象的属性值为简单类型(如string, number),通过Object.assign({},srcObj);得到的新对象为深拷贝;如果属性值为对象或其它引用类型,那对于这个对象而言其实是浅拷贝的。 当对象中只有一级属性,没有二级属性的时候,Objec*t.assign({},srcObj)``和扩展运算符(...)为深拷贝,但是对象中有对象的时候,此方法,在二级属性以后就是浅拷贝。