一、两种数据类型

  首先,得理解Javascript中有两种数据类型:基本数据类型引用数据类型。基本数据类型保存在栈内存中,引用数据类型保存在堆内存中。那么,为什么会有这样子的保存数据方式呢?保存在栈内存的必须是大小固定的数据,引用数据类型大小不固定,只能保存在堆内存中。所以在访问的时候,基本数据类型就是按值访问,操作的就是变量保存的值,访问引用类型的时候,只能访问保存在变量中的引用类型的地址来操作实际对象。

1、基本数据类型

  常见的基本数据类型包括:Number, String, Boolean,Null和Undefined,基本数据类型是按值访问的,所以可以直接操作保存在变量中的实际值,比如:

const a = 10;
let b = a;
b = 20;
console.log(a); //10
console.log(b); //20
复制代码



2、引用数据类型

  也就是我们所知道的Object,Function等。引用类型数据在栈内存中保存的实际上是对象在堆内存中的引用地址。通过这个引用地址可以快速查找到保存中堆内存中的对象。比如:

let obj1 = new Object();
let obj2 = obj1;
obj2.name = "Hello"
console.log(obj1.name); // Hello
复制代码



所以在拷贝引用类型的时候,需要考虑到深浅拷贝的问题。

两者区别:

浅拷贝(shallow copy):只复制指向某个对象的指针,而不复制对象本身,新旧对象共享一块内存;
深拷贝(deep copy):复制并创建一个一摸一样的对象,不共享内存,修改新对象,旧对象保持不变。

二、深拷贝和浅拷贝

浅拷贝很简单,直接赋值就行。

let color1 = ['red','green'];
let color2 = color1;
console.log(color2)//['red','green'];
color1.push('black') ;//改变color1的值
console.log(color2)//['red','green', 'black'];
复制代码

因为浅拷贝在复制引用类型时,复制的只是它的指针,两个变量指向同个指针。



包括ES6中提供的两个复制数组的方法,扩展运算符(...)和Object.assign()也都是浅拷贝

var obj = {a: 1, b: 2, c: { a: 3 },d: [4, 5]}
var obj1 = obj
var obj2 = JSON.parse(JSON.stringify(obj))//深拷贝常用方法
var obj3 = {...obj}
var obj4 = Object.assign({},obj)
obj.a = 999
obj.c.a = -999
obj.d[0] = 123
console.log(obj1) //{a: 999, b: 2, c: { a: -999 },d: [123, 5]}
console.log(obj2) //{a: 1, b: 2, c: { a: 3 },d: [4, 5]}
console.log(obj3) //{a: 1, b: 2, c: { a: -999 },d: [123, 5]}
console.log(obj4) //{a: 1, b: 2, c: { a: -999 },d: [123, 5]}

复制代码

再来看看深拷贝,有以下几种方法:

  • 1、直接循环拷贝对象中的每个值
let color1 = ['red','green']; 
let color2 = [];
//复制
for(let i  = 0;i < color1.length;i++){
   color2[i] = color1[i]; 
}
console.log(color2)//['red','green'];
color1.push('black') ;//改变color1的值
console.log(color2)//['red','green']
复制代码

这种方法有一个局限性,那就是因为我们知道要拷贝的对象中的每一项都是基本数据类型,所以可以深拷贝成功,如果对象中的某一项是引用类型呢?所以有了第二种方法。

  • 2、递归
function deepCopy (obj) {
   var result;

   //引用类型分数组和对象分别递归
   if (Object.prototype.toString.call(obj) == '[object Array]') {
       result = []
       for (i = 0; i < obj.length; i++) {
               result[i] = deepCopy(obj[i])
       }
   } else if (Object.prototype.toString.call(obj) == '[object Object]') {
       result = {}
       for (var attr in obj) {
           result[attr] = deepCopy(obj[attr])
       }
   }
   //值类型直接返回
   else {
       return obj
   }
   return result
}
复制代码
  • 3、JSON对象中的parse和stringify
let test={
   a:"ss",
   b:"dd",
   c:[
       {dd:"css",ee:"cdd"},
       {mm:"ff",nn:"ee"}
   ]
};
let test1 = JSON.parse(JSON.stringify(test));//拷贝数组,注意这行的拷贝方法
console.log(test);
console.log(test1); 
test1.c[0].dd="change"; //改变test1的c属性对象的d属性
console.log(test);  //不影响test
console.log(test1);
复制代码

此方法最常见,也最好用。

  • 4、slice、concat 这两个方法在对数组的操作中经常用到,也确实不会对原有数组产生影响。不过对于非一维数据来说,就无法进行深拷贝了。
let a = [0, 1, [1, 2], 3];
let b = a.slice();
a[0] = 1;
a[2][0] = 0;
console.log(a); //[1, 1, [0, 2], 3]
console.log(b); // [0, 1, [0, 2], 3]
复制代码

上面例子可以看到,拷贝的并不彻底,b数组的一级属性确实不受影响,但是二级属性还是没能拷贝成功,第一层的属性确实深拷贝,拥有了独立的内存,但更深的属性却仍然公用了地址,所以才会造成上面的问题。同理,concat方法与slice也存在这样的情况,他们都不是真正的深拷贝,这里需要注意。

结语

说了这么多,应该能够大致理解为什么会出现深拷贝和浅拷贝的问题,以及如果处理这两种情况。这个问题在面试中经常被问到,理解这个问题,不仅仅为了应付面试,更是在实际开发过程中,受用无穷。博主还是小菜鸟的时候就被这个问题所困扰,当后端返回的数据经过前端多层处理之后,变得莫名其妙,面目全非的时候,是时候考虑一下深拷贝和浅拷贝的问题了。