全面聊聊JavaScript的浅拷贝和深拷贝

一、背景

     首先我们可以看下面这段简单的代码:

var obj = {name:'程序猿',sex:'男'};
var arr = ['程序猿','程序媛'];                                                        

var copyobj = obj
copyobj .name = '设计狗'
console.log(obj) // {name: "设计狗", sex: "男"}
console.log(copyobj) //{name: "设计狗", sex: "男"}
console.log(copyobj === obj) //true
// 以上修改copyobj的时候也修改了obj

var copyarr = arr
copyarr [0] = '设计狗'
console.log(arr) // ["设计狗", "程序媛"]
console.log(copyarr) // ["设计狗", "程序媛"]
console.log(copyarr=== arr) //true
// 以上修改copyarr 的时候也修改了arr

var obj2 = {name:'程序猿',msg:{sex:'男',age:'20'}};
var arr2 = ['程序猿','程序媛',['男','女']];

var obj2 = {name:'程序猿',msg:{sex:'男',age:'20'}};
var arr2 = ['程序猿','程序媛',['男','女']];

var copyobj2 = obj2
copyobj2.msg.sex= '人妖'
console.log(obj2) // {name:'程序猿',msg:{sex:'人妖',age:'20'}};
console.log(copyobj2) //{name:'程序猿',msg:{sex:'人妖',age:'20'}};
console.log(copyobj2 === obj2)  //true
// 以上修改copyobj2的时候也修改了obj2

var copyarr2 = arr2
copyarr2[2][0]= '人妖'
console.log(arr2) // ['程序猿','程序媛',['人妖','女']];
console.log(copyarr2) // ['程序猿','程序媛',['人妖','女']];
console.log(copyarr2 === arr2)  //true
// 以上修改copyarr2 的时候也修改了arr2

    显然,有时候我们需要克隆一份数据并修改,但是并不想影响原来的数据,特别是在现在前端使用双向数据绑定的情况下,经常有这样的需求,因此,浅拷贝和深拷贝就产生了。

二、关于栈内存和堆内存

       了解JavaScript的浅拷贝和深拷贝之前,我们先了解下栈内存和堆内存。首先,任何程序的运行都是要占用内存的,而为了减少程序对内存的占用,运行程序的载体(比如浏览器)都会有个垃圾回收机制,这个机制把内存分成了栈内存与堆内存。

        当我们声明了全局基础变量或者是执行一个普通的方法的时候,基础变量会直接占用栈内存,而方法会建立自己的一个局部栈内存来占用全局的栈内存,方法中也可以申明局部基础变量来占用自己的栈内存,当方法执行完毕则会释放所有基础变量占用的内存,当然,闭包等特殊的方法除外。

       当我们声明了一个对象或者数组的时候,对象或者数组会被分成两部分进行内存存储,一部分是对象或数组在栈内存中的地址,或叫指针;另一部分是对象或者数组在堆内存中的值。地址(指针)指向对应的值。这样,当查询引用类型的变量时, 先从栈中读取内存地址, 然后再通过地址找到堆中的值。对于这种,我们把它叫做按引用访问。

        比如下面两个声明在内存中是如图所示的:

        var a = 5;   var b = {name:'hao'};  var c = ['hao','feng']   

        

二、关于JavaScript的数据类型

    JavaScript的数据类型可分为基本类型和引用类型。

     基本类型:存放在栈内存中的简单数据段。数据大小确定,内存空间大小可以分配。
     5种基本数据类型有Undefined、Null、Boolean、Number 和 String,它们是直接按值存放的,所以可以直接访问。
 
     引用类型:存放在堆内存中的对象,变量中实际保存的是一个指针,这个指针指向另一个位置。每个空间大小不一样,要根据情况开进行特定的分配。
     当我们需要访问引用类型(如对象,数组,函数等)的值时,首先从栈中获得该对象的地址指针,然后再从堆内存中取得所需的数据。 
 
三、通过案例了解JavaScript的浅拷贝和深拷贝
     1、对于基本类型,可直接“深拷贝”(实际上基本类型不会存在深浅拷贝问题),基本类型占据着固定大小的空间,并被保存在栈内存中。当一个变量向另一个变量复制基本类型的值,会创建这个值的副本占用同等的栈内存
      
var a = 5;
var d = a;
console.log(d); // 5
d = 8;
console.log(d); // 8
console.log(a); // 5

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

    2、对于引用类型,可根据一维对象数组跟多维对象数组分为浅拷贝和深拷贝

    浅拷贝,可以简单理解为对整个一维对象进行复制或对多维数组对象中的某一值进行复制

    深拷贝,可以简单理解为对整个数组对象进行复制,无论该数组对象有多少层级

   (1)通过变量复制实现一维数组对象的浅拷贝或多维数组对象的浅拷贝

// 变量复制实现一维数组对象的浅拷贝
var obj = {name:'程序猿',sex:'男'};
var arr = ['程序猿','程序媛'];
var e = obj.name;
console.log(e); // 程序猿
e = '搬砖农';
var f = arr[0];
console.log(f); // 程序猿
f = '设计狗';
console.log(obj); // {name: "程序猿", sex: "男"}
console.log(e); // 搬砖农
console.log(arr); // ["程序猿", "程序媛"]
console.log(f); // 设计狗

   (2)通过es6新增的Object.assign来复制对象

var obj = {name:'程序猿',sex:'男'};
var arr = ['程序猿','程序媛'];
var copyobj =Object.assign({}, obj);
var copyarr=Object.assign({}, arr);

console.log(obj); // {name:'程序猿',sex:'男'}
console.log(copyobj); //{name:'程序猿',sex:'男'}
console.log(obj===copyobj); // false,说明堆内存新克隆了值,在堆内存中,值的长相一致且占用大小一致,但所在位置不一致的值是不相等的(类似双胞胎)
console.log(obj.msg === copyobj.msg) // false
console.log(arr); // ['程序猿','程序媛']; console.log(copyarr); // {0: "程序猿", 1: "程序媛"} // tips由上可以看出Object.assgin用在数组上会将数据对象化,因此需要转化为数组,此时就基本不是整个数组的复制了 var arr2= [] for (var key in copyarr){ arr2.push(copyarr[key]) } console.log(arr2) // ['程序猿','程序媛']; console.log(arr2 === arr) // false 说明堆内存新建立了值并压入了属性值

   (3)通过数组的slice()和concat()方法来复制数组

var arr = ['程序猿','程序媛'];
// var copyarr = arr.concat()
var copyarr = arr.slice()
console.log(copyarr)
console.log(copyarr === arr) // false,说明堆内存新复制了一份值
copyarr[0] = '设计狗' console.log(copyarr) // ["设计狗", "程序媛"] console.log(arr) // ['程序猿','程序媛']

   (4)通过jquey中的$.extend({}, obj); 不做案例

   基本实现浅拷贝的方式有以上几种,到目前为止都是以一维的数组对象进行拷贝,如果用以上方法对多维数组对象进行拷贝,会出现什么情况呢,我们可以看看。

// 变量复制
var obj = {name:'程序猿',msg:{sex:'男',age:'20'}};
var arr = ['程序猿','程序媛',['男','女']];
var e = obj.msg.sex;
console.log(e); //
e = '性别不详';
var f = arr[2][0];
console.log(f); //
f = '人妖';
console.log(obj); // {name:'程序猿',msg:{sex:'男',age:'20'}}
console.log(e); // 性别不详
console.log(arr); // ['程序猿','程序媛',['男','女']]
console.log(f); // 人妖

// 由上可以看出跟变量复制对一维数组的拷贝没有什么区别
// Object.assign
var obj = {name:'程序猿',msg:{sex:'男',age:'20'}};
var arr = ['程序猿','程序媛',['男','女']];
var copyobj =Object.assign({}, obj);
var copyarr=Object.assign({}, arr);

console.log(obj); // {name:'程序猿',msg:{sex:'男',age:'20'}}
console.log(copyobj); //{name:'程序猿',msg:{sex:'男',age:'20'}}
console.log(obj.name===copyobj.name) // true,克隆了但是跟变量复制的性质一致,为true
console.log(obj.msg===copyobj.msg); // true,说明堆内存没有新克隆值
console.log(arr); //  ['程序猿','程序媛',['男','女']]
console.log(copyarr);  // {0: "程序猿", 1: "程序媛",2:[['男','女']]}

var obj2 = {msg:{sex:'男',age:'20'},name:'程序猿'};
var copyobj2 =Object.assign({}, obj2);
console.log(copyobj2.msg === obj2.msg); // true,说明堆内存没有新克隆值
copyobj2.msg.sex = '性别不详'
console.log(copyobj2) //  {msg:{sex:'性别不详',age:'20'},name:'程序猿'}
console.log(obj2) //  {msg:{sex:'性别不详',age:'20'},name:'程序猿'};
// 数组的slice()与concat()
var arr = ['程序猿','程序媛',['男','女']];
// var copyarr = arr.concat()
var copyarr = arr.slice()
console.log(copyarr === arr) // false,说明外围最大的数组新复制了一份值
console.log(copyarr[2] === arr[2]) // true,说明堆内存没有新复制了一份值
copyarr[0] ='设计狗'
console.log(copyarr) //  ['设计狗','程序媛',['男','女']];
console.log(arr) //  ['程序猿','程序媛',['男','女']];

copyarr[2][0] = '人妖'
console.log(copyarr) //  ['设计狗','程序媛',['人妖','女']];
console.log(arr) //  ['程序猿','程序媛',['人妖','女']];

// 由上可以看出数组的slice()与concat()实现不了对多维数组的克隆

    以上的操作如下图所示:

     3、对于多维的数组对象,我们可以通过以下几种方式进行深拷贝

     (1)使用JSON.parse(JSON.stringify(obj)),JOSN对象中的stringify把一个js对象序列化为一个JSON字符串,parse可以把JSON字符串反序列化为一个js对象,通过这两个方法,也可以实现对象的深复制。

// JSON.parse(JSON.stringify(obj))
var obj = {name:'程序猿',msg:{sex:'男',age:'20'}};
var arr = ['程序猿','程序媛',['男','女']];

var copyobj = JSON.parse(JSON.stringify(obj));
var copyarr = JSON.parse(JSON.stringify(arr));

console.log(copyobj) // {name:'程序猿',msg:{sex:'男',age:'20'}}
console.log(copyobj === obj) // false
console.log(copyobj.msg === obj.msg) // false

copyobj.msg.sex = '人妖'
console.log(copyobj) // {name:'程序猿',msg:{sex:'人妖',age:'20'}}
console.log(obj) // {name:'程序猿',msg:{sex:'男',age:'20'}}

console.log(copyarr) // ['程序猿','程序媛',['男','女']]
console.log(copyarr === arr) // false
console.log(copyarr[2] === arr[2])  // false

copyarr[2][0] =  '人妖'
console.log(copyarr) // ['程序猿','程序媛',['人妖','女']]
console.log(arr) // ['程序猿','程序媛',['男','女']]

   但是,这种方法是有缺陷的,如果数组对象中包含undefined、任意的函数以及 symbol 值,在序列化过程中会被忽略(出现在非数组对象的属性值中时)或者被转换成 null(出现在数组中时)。

var obj = {
   name:'程序猿',
   msg:{sex:'男',age:'20'},
   fn:function(){console.log('我是IT人')},
   other: undefined
  };

var arr = ['程序猿','程序媛',['男','女'],function(){console.log('我是IT人')},undefined];

var copyobj = JSON.parse(JSON.stringify(obj));
var copyarr = JSON.parse(JSON.stringify(arr));

console.log(copyobj) // {name:'程序猿',msg:{sex:'男',age:'20'}};
console.log(copyarr) // ['程序猿','程序媛',['男','女'],null,null]

   (2)使用递归,其中涉及使用到Object.keys(),返回当前对象的属性的集合(第三方jquery的$.extend原理)

function deepCopy(obj) {
    // 创建一个新对象
    let result = {}
    // 获取对象的属性的集合
    let keys = Object.keys(obj),
        key = null,  // 声明的result对象的key
        temp = null; // 用于判断循环所得的当前对象的属性值是否为对象
   // 循环获取到的属性集合
    for (let i = 0; i < keys.length; i++) {
       // 将循环所得的key值赋给result的key值
        key = keys[i];
        //// 判断循环所得的当前对象的属性值是否为对象,是的话递归方法    
        temp = obj[key];
        // 如果字段的值也是一个对象则递归操作
        if (temp && typeof temp === 'object') {
            result[key] = deepCopy(temp);
        } else {
        // 否则直接赋值给新对象
            result[key] = temp;
        }
    }
    return result;
}
var obj = {
   name:'程序猿',
   msg:{sex:'男',age:'20'},
   fn:function(){console.log('我是IT人')},
   other: undefined
  };

var arr = ['程序猿','程序媛',['男','女'],function(){console.log('我是IT人')},undefined];

var copyobj = deepCopy(obj);     
console.log(copyobj);  // {name:'程序猿',msg{sex:'男',age:'20'},fn:(),other: undefined}

console.log(copyobj === obj ) // false

copyobj.msg.sex = '人妖';
console.log(copyobj) //{name:'程序猿',msg:{sex:'妖',age:'20'},fn:(),other: undefined}

console.log(obj) // {name:'程序猿',msg:{sex:'男',age:'20'},fn:(),other: undefined}

copyobj.fn(); // 我是IT人

var copyarr = deepCopy(arr );     
console.log(copyarr);  // {0: "程序猿", 1: "程序媛", 2: 
{0: "男", 1: "女"}, 3: ƒ(), 4: undefined}

console.log(copyarr[3]) // ƒ (){console.log('我是IT人')}

   当然,这种递归的方法也不是万能的,只是比前面的转化更为完善,如果递归的是一个被引用的对象,那么会导致死循环递归导致爆栈,比如:

var obj1 = {
    x: 1, 
    y: 2
};
obj1.z = obj1;
var obj2 = deepCopy(obj1);

   解决方法在此不做解释,前辈给了这么种解决方法,供我们参考使用:判断一个对象的字段是否引用了这个对象或这个对象的任意父级即可,改造下前面的deepCopy方法

function deepCopy(obj, parent = null) {
    // 创建一个新对象
    let result = {};
    let keys = Object.keys(obj),
        key = null,
        temp= null,
        _parent = parent;
    // 该字段有父级则需要追溯该字段的父级
    while (_parent) {
        // 如果该字段引用了它的父级则为循环引用
        if (_parent.originalParent === obj) {
            // 循环引用直接返回同级的新对象
            return _parent.currentParent;
        }
        _parent = _parent.parent;
    }
    for (let i = 0; i < keys.length; i++) {
        key = keys[i];
        temp= obj[key];
        // 如果字段的值也是一个对象
        if (temp && typeof temp=== 'object') {
            // 递归执行深拷贝 将同级的待拷贝对象与新对象传递给 parent 方便追溯循环引用
            result[key] = DeepCopy(temp, {
                originalParent: obj,
                currentParent: result,
                parent: parent
            });
        } else {
            result[key] = temp;
        }
    }
    return result;
}

四、总结

  对于数组对象的拷贝,一般情况下常用JSON.parse(JSON.stringify(obj))即可

原文地址:https://www.cnblogs.com/ahao68/p/9046344.html