深入JavaScript基础之深浅拷贝

9,046 阅读9分钟

最近在学到JavaScript对象的深拷贝和浅拷贝做了一些比较,将实际开发的点和基础点做了些小结,话不多说,开始进入主题吧。


  • 基础认识---基本类型
  • 基础认识---引用类型
  • 浅拷贝的实现-对象&&数组
  • 深拷贝的实现-对象&&数组
  • 深拷贝的实现- ES6扩展运算符实现对象&&数组的深拷贝
  • 深拷贝的实现-递归的方法
  • 深拷贝的实现-JSON.stringify/parse的方法

基础认识:

对于js的对象的深拷贝和浅拷贝,必须先提到的是JavaScript的数据类型, 我们先来理解一些js基本的概念 —— Javascript有五种基本数据类型(也就是简单数据类型),它们分别是:Undefined,Null,Boolean,Number和String,并且基本类型存放在栈内存。还含有一种复杂的数据类型(也叫引用类型)存放在堆内存,就是对象(Object,Array)。 堆内存用于存放由new创建的对象,栈内存存放一些基本的类型的变量和对象的引用变量。

注意Undefined和Null的区别,Undefined类型只有一个值,就是undefined,Null类型也只有一个值,也就是null

Undefined 其实就是已声明未赋值的变量输出的结果

null 其实就是一个不存在的对象的结果。

JS 中的浅拷贝与深拷贝,只是针对复杂数据类型(Object,Array)的复制问题。浅拷贝与深拷贝都可以实现在已有对象上再生出一份的作用。但是对象的实例是存储在堆内存中然后通过一个引用值去操作对象,由此拷贝的时候就存在两种情况了:拷贝引用和拷贝实例,这也是浅拷贝和深拷贝的区别

1. 对于基本数据类型

他们的值在内存中占据着固定大小的空间,并被保存在栈内存中。当一个变量向另一个变量复制基本类型的值,会创建这个值的副本,并且我们不能给基本数据类型的值添加属性

var a = 1;
var b = a;
b.name = 'hanna';
console.log(a); //1
console.log(b.name); //undefined

上面的代码中,a是基本数据类型(Number), b是a的一个副本,它们两者都占有不同位置但相等的内存空间,只是它们的值相等,若改变其中一方,另一方不会随之改变。

2. 对于引用类型

复杂的数据类型即是引用类型,它的值是对象,保存在堆内存中,包含引用类型值的变量实际上包含的不是对象本身,而是一个指向该对象的指针。从一个变量向另一个变量复制引用类型的值,复制的其实是指针地址而已,因此两个变量最终都指向同一个对象

var obj = {
   name:'Hanna Ding',
   age: 22
}
var obj2 = obj;
obj2['c'] = 5;
console.log(obj); //Object {name: "Hanna Ding", age: 22, c: 5}
console.log(obj2); //Object {name: "Hanna Ding", age: 22, c: 5}

image

注:0x123指针地址

我们可以看到obj赋值给obj2后,但我们改变其中一个对象的属性值,两个对象都发生了改变,根本原因就是obj和obj2两个变量都指向同一个指针,赋值时只是复制了指针地址,它们指向同一个引用,所以当我们改变其中一个的值就会影响到另一个变量的值。

一、深拷贝和浅拷贝的区别

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

浅拷贝的实现

浅拷贝的意思就是只复制引用,而未复制真正的值,有时候我们只是想备份数组,但是只是简单让它赋给一个变量,改变其中一个,另外一个就紧跟着改变,但很多时候这不是我们想要的。

(1)对象的浅拷贝: 上面的代码就是对象的浅拷贝的例子

(2)数组的浅拷贝

var arr = [1, 2, 3, '4'];

var arr2 = arr;
arr2[1] = "test"; 
console.log(arr); // [1, "test", 3, "4"]
console.log(arr2); // [1, "test", 3, "4"]

arr[0]="fisrt"
console.log(arr); // ["fisrt", "test", 3, "4"]
console.log(arr2); // ["fisrt", "test", 3, "4"]



上面的代码是最简单的利用 = 赋值操作符实现了一个浅拷贝,可以很清楚的看到,随着 arr2 和 arr 改变,arr 和 arr2 也随着发生了变化

深拷贝的实现

(1)数组的深拷贝 对于数组我们可以使用slice() 和 concat() 方法来解决上面的问题

注意:(slice() 和 concat()对数组的深拷贝是有局限性的。

**slice **

var arr = ['a', 'b', 'c'];
var arrCopy = arr.slice(0);
arrCopy[0] = 'test'
console.log(arr); // ["a", "b", "c"]
console.log(arrCopy); // ["test", "b", "c"]

concat

var arr = ['a', 'b', 'c'];
var arrCopy = arr.concat();
arrCopy[0] = 'test'
console.log(arr); // ["a", "b", "c"]
console.log(arrCopy); // ["test", "b", "c"]

针对上面说的slice() 和 concat()对局限性,我们可以继续看下面的例子:

var arr1 = [{"name":"Roubin"},{"name":"RouSe"}];//原数组
var arr2 = [].concat(arr1);//拷贝数组
arr1[1].name="Tom";
console.log(arr1);//[{"name":"Roubin"},{"name":"Tom"}]
console.log(arr2);//[{"name":"Roubin"},{"name":"Tom"}]

可以发现使用.concat()和浅复制的结果一样,这是为什么呢,那slice()会出现同样的结果吗?继续看写看例子

var arr1 = [{"name":"weifeng"},{"name":"boy"}];//原数组
var arr2 = arr1.slice(0);//拷贝数组
arr1[1].name="girl";
console.log(arr1);// [{"name":"weifeng"},{"name":"girl"}]
console.log(arr2);//[{"name":"weifeng"},{"name":"girl"}
var a1=[["1","2","3"],"2","3"];
var a2=a1.slice(0);
a1[0][0]=0; //改变a1第一个元素中的第一个元素
console.log(a1);  //[["0","2","3"],"2","3"]
console.log(a2);   //[["0","2","3"],"2","3"]

哟,也是出现同样的结果呀,原来由于上面数组的内部属性值是引用对象(Object,Array),slice和concat对对象数组的拷贝,整个拷贝还是浅拷贝,拷贝之后数组各个值的指针还是指向相同的存储地址.

因此,slice和concat这两个方法,仅适用于对不包含引用对象的一维数组的深拷贝

注(补充点):

  • arrayObj.slice(start, [end]) 该方法返回一个 Array 对象,其中包含了 arrayObj 的指定部分。不会改变原数组
  • arrayObj.concat() 方法用于连接两个或多个数组。该方法不会改变现有的数组,而仅仅会返回被连接数组的一个副本。 其实也就是下面实现的方式,但还是用上面的方法来实现比较简单高效些
function deepCopy(arr1, arr2) {
   for (var i = 0; i < arr1.length; ++i) {
       arr2[i] = arr1[i];
   }
}

ES6扩展运算符实现数组的深拷贝

var arr = [1,2,3,4,5]
var [ ...arr2 ] = arr
arr[2] = 5
console.log(arr)  //[1,2,5,4,5]
console.log(arr2)  //[1,2,3,4,5]

(2)对象的深拷贝 对象的深拷贝实现原理: 定义一个新的对象,遍历源对象的属性 并 赋给新对象的属性 主要是两种:

  • 利用递归来实现每一层都重新创建对象并赋值
  • 利用 JSON 对象中的 parse 和 stringify
  • ES6扩展运算符实现对象的深拷贝
var obj = {
  name: 'FungLeo',
  sex: 'man',
  old: '18'
}
var { ...obj2 } = obj
obj.old = '22'
console.log(obj)   ///{ name: 'FungLeo', sex: 'man', old: '22'}
console.log(obj2)  ///{ name: 'FungLeo', sex: 'man', old: '18'}
var obj = {
   name:'xiao ming',
   age: 22
}

var obj2 = new Object();
obj2.name = obj.name;
obj2.age = obj.age

obj.name = 'xiaoDing';
console.log(obj); //Object {name: "xiaoDing", age: 22}
console.log(obj2); //Object {name: "xiao ming", age: 22}

obj2是在堆中开辟的一个新内存块,将obj1的属性赋值给obj2时,obj2是同直接访问对应的内存地址。

递归的方法

递归的思想就很简单了,就是对每一层的数据都实现一次 创建对象->对象赋值 的操作,简单粗暴上代码:

function deepClone(source){
  const targetObj = source.constructor === Array ? [] : {}; // 判断复制的目标是数组还是对象
  for(let keys in source){ // 遍历目标
    if(source.hasOwnProperty(keys)){
      if(source[keys] && typeof source[keys] === 'object'){ // 如果值是对象,就递归一下
        targetObj[keys] = source[keys].constructor === Array ? [] : {};
        targetObj[keys] = deepClone(source[keys]);
      }else{ // 如果不是,就直接赋值
        targetObj[keys] = source[keys];
      }
    } 
  }
  return targetObj;
}

那我们来试试个例子:

var obj = {
    name: 'Hanna',
    age: 22
}
var objCopy = deepClone(obj)
obj.name = 'ding';
console.log(obj);//Object {name: "ding", age: 22}
console.log(objCopy);//Object {name: "Hanna", age: 22}

对象与Json相互转换

我们先看这两种方法:SON.stringify/parse的方法

The JSON.stringify() method converts a JavaScript value to a JSON string.

**JSON.stringify **是将一个 JavaScript 值转成一个 JSON 字符串。

The JSON.parse() method parses a JSON string, constructing the JavaScript value or object described by the string.

**JSON.parse **是将一个 JSON 字符串转成一个 JavaScript 值或对象。

JavaScript 值和 JSON 字符串的相互转换。

来一步步看下面的封装层例子:

function  deepClone(origin){
    var clone={};
    try{
        clone= JSON.parse(JSON.stringify(origin));
    }
    catch(e){
        
    }
    return clone;

}

未封装和封装的进行比较:

const originArray = [1,2,3,4,5];
const cloneArray = JSON.parse(JSON.stringify(originArray));
console.log(cloneArray === originArray); // false
const originObj = {a:'a',b:'b',c:[1,2,3],d:{dd:'dd'}};
const cloneObj = JSON.parse(JSON.stringify(originObj));
console.log(cloneObj === originObj); // false
 
cloneObj.a = 'aa';
cloneObj.c = [1,1,1];
cloneObj.d.dd = 'tt';
 
console.log(cloneObj); 
console.log(originObj);
/****************封装层**************/
function  deepClone(origin){
    var  clone={};
    try{
       clone= JSON.parse(JSON.stringify(origin));
    }
    catch(e){
        
    }
    return clone;

}
const originObj = {a:'a',b:'b',c:[1,2,3],d:{dd:'dd'}};
const cloneObj = deepClone(originObj);
console.log(cloneObj === originObj); // false
 //改变值
cloneObj.a = 'aa';
cloneObj.c = [4,5,6];
cloneObj.d.dd = 'tt';

console.log(cloneObj); // {a:'aa',b:'b',c:[1,1,1],d:{dd:'tt'}};
console.log(originObj);// {a:'a',b:'b',c:[1,2,3],d:{dd:'dd'}};

虽然上面的深拷贝很方便(请使用封装函数进行项目开发以便于维护),但是,只适合一些简单的情景(Number, String, Boolean, Array, Object),扁平对象,那些能够被 json 直接表示的数据结构。function对象,RegExp对象是无法通过这种方式深拷贝。

注意

var  clone={};
    try{
       clone= JSON.parse(JSON.stringify(origin));
    }
    
    笔者在写着段代码犯了一个错误:使用const 连续声明了 导致代码运行出现错误,原因在于在同一代码出现连续的const声明,则会产生暂时性死区。
    [详细请看阮一峰的ES6](http://es6.ruanyifeng.com/#docs/let#const-命令)
    const clone={};
    try{
      const clone= JSON.parse(JSON.stringify(origin));
    }
    

举例:

const originObj = {
 name:'pipi',
 sayHello:function(){
 console.log('Hello pipi');
 }
}
console.log(originObj); // {name: "pipi", sayHello: ƒ}
const cloneObj = deepClone(originObj);
console.log(cloneObj); // {name: "pipip"}

发现在 cloneObj 中,有属性丢失了。。。是什么原因? 在 MDN 上找到了原因:

If undefined, a function, or a symbol is encountered during conversion it is either omitted (when it is found in an object) or censored to null (when it is found in an array). JSON.stringify can also just return undefined when passing in "pure" values like JSON.stringify(function(){}) or JSON.stringify(undefined).

undefined、function、symbol 会在转换过程中被忽略。。。 明白了吧,就是说如果对象中含有一个函数时(很常见),就不能用这个方法进行深拷贝。