探究JS中对象的深拷贝和浅拷贝

深拷贝和浅拷贝的区别

在讲深拷贝和浅拷贝的区别之前,回想一下我们平时拷贝一个对象时是怎么操作的?是不是像这样?

var testObj1 = {a: 1, b:2}, testObj2=testObj1;
testObj1.a = 7;
console.log(testObj1);  //{a: 7, b:2}
console.log(testObj2);  //{a: 7, b:2}

发现问题了吗?当testObj1变化时,testObj2相应的属性跟着变化了。这就是属于浅拷贝了,而所谓的深拷贝就是要做到当testObj1变化时testObj2不会跟着变化,这就是浅拷贝与深拷贝的区别。至少在我知道基本类型和引用类型的区别之前我是不知道为什么会这样的,那什么是基本类型和引用类型呢?

基本类型和引用类型

首先通过一个简单的例子看看与上面的例子有什么区别:

var num1 = 1, num2 = num1;
num1 = 7;
console.log(num1);  // 7
console.log(num2);  // 1

很显然,这里的变量num2并没有因为num1的变化而变化。其实,这里的num1和num2就是一种基本类型,而上面的那两个对象变量则属于引用类型。

ECMAScript变量可能包含两种不同数据类型的值:基本类型值和引用类型值。基本类型值指的是那些保存在栈内存中的简单数据段,即这种值完全保存在内存中的一个位置。而引用类型值是指那些保存堆内存中的对象,意思是变量中保存的实际上只是一个指针,这个指针指向内存中的另一个位置,该位置保存对象。

所以,按我的理解,基本类型就是直接存在内存里的一个具体点,相互之间是独立的,而引用类型存储的只是一个指向具体内存地址的指针,当两个对象相等赋值时,他们实际上是指向的同一个内存地址,所以,当一个变了,另一个跟着变也就不奇怪了。打个不恰当的比喻,基本类型之间就像独立的小超市,相互之间的门头都是不一样的,而连锁超市的话都是一样的,而且都会一起变化。

如何实现深拷贝

既然已经知道了深拷贝和浅拷贝的区别以及为什么会出现这种区别,那怎么实现深拷贝呢?我之前在业务开发中有遇到过这个问题,但都是采用先新建一个空对象,再遍历目标对象的属性,将目标对象的属性值一个个赋值给这个新的空对象,从而得到了一个新的对象,这个对象是完全等于之前的对象的,但是不会受它变化而影响,所以算是初步实现了深拷贝的,大体实现如下:

var testObj3 = {a: 1, b:2, c:3, d:4};
var testObj4 = {};
for(var key in testObj3){
    testObj4[key] = testObj3[key];
}
testObj3.a = 7;
console.log(testObj3);  //{a: 7, b:2, c:3, d:4}
console.log(testObj4);  //{a: 1, b:2, c:3, d:4}

咋一看这个方法当时虽然满足了我当时的业务需求,可是还有什么可以改进的地方呢?还有别的实现方式吗?

还有什么别的实现方式吗?

之前我也没细想过这个问题,知道后来偶尔间看到这篇文章,这篇文章提供的其他方式是我之前不知道,算是补上了我之前知识的盲区,在这里便是感谢。总结来说,还有以下几种方式:

  • 1.通过Onject.assign()

    • Object.assign是ES6中引入的一种用于合并对象的方法,具体使用方法可以看文档
    • 这个方法我之前是用的不多的,但最近几个项目中有些使用后算是知道了这个属性,其实当时就应该可以想到可以用于深拷贝的,大体用法如下:
    var obj1 = {x: 1, y:2}, obj2 = Object.assign({}, obj1);
    obj2.x = 7;
    console.log(obj1);  //{a: 7, b:2}
    console.log(obj2);  //{a: 1, b:2}
    • 可以看到,完美地深拷贝了这个对象,新对象并没有受原对象影响,可是,当对象为嵌套对象的时候呢?
    var obj3 = {x:1, y:2, z:{name: 3}}, obj4 = Object.assign({}, obj3);
    obj4.z.name = 7;
    console.log(obj3);  //{x:1, y:2, z:{name: 7}}
    console.log(obj4);  //{x:1, y:2, z:{name: 7}}
    • 事实证明,对于嵌套对象,Object.assign()深拷贝失效了,所以说,Object.assign()只能实现一维对象的深拷贝
  • 2.通过JSON.parse(JSON.stringify(obj))
    • 这个方法也是在前一段时间朋友问我深拷贝的方法时偶尔查到的,之前是没用的,感觉还蛮好用的:
    var obj5 = {x: 1, y:2}, obj6 = JSON.parse(JSON.stringify(obj5));
    obj6.x = 7;
    console.log(obj5);  //{x: 1, y:2}
    console.log(obj6);  //{x: 7, y:2}
    • 可是这个方法能解决多维对象的深拷贝问题吗?
    var obj9 = {x:1, y:2, z:{name: 3}}, obj10 = Object.assign({}, obj3);
    obj10.z.name = 7;
    console.log(obj9);  //{x:1, y:2, z:{name: 3}}
    console.log(obj10);  //{x:1, y:2, z:{name: 7}}
    • 从代码上来看,这个方法完美地实现了多维对象的深拷贝,可是这样小小改动一下就会发现有问题了:
    var obj9 = {x:1, y:2, z:{name: 3, func: function(){}}}, obj10 = Object.assign({}, obj3);
    obj10.z.name = 7;
    console.log(obj9);  //{x:1, y:2, z:{name: 3, func: function(){}}}}
    console.log(obj10);  //{x:1, y:2, z:{name: 7}}
    • 是的,当多维对象里有函数时,并没有复制,这是为什么,这个方法的MDN上有解释:

      undefined、任意的函数以及 symbol 值,在序列化过程中会被忽略(出现在非数组对象的属性值中时)或者被转换成 null(出现在数组中时)。

    • 所以,JSON.parse(JSON.stringify(obj))这种方法虽然可以拷贝多维对象,但不能深拷贝含有undefined、function、symbol值的对象。
可以改进的地方
  • 最开始那种通过for.in遍历所有属性的方式是会将原型链上的属性也遍历出来的,一般来说是不要的,所以最好使用obj.hasOwnProperty(key)来进行筛选判断,当然具体情况要根据业务需求来。
  • 后面的几种方法都没有能完美地实现多维数组的深拷贝,所以还得祭出大杀器:递归。简单来说,就是先新建一个新对象,然后通过Object.keys()遍历原对象的所有属性列表,再遍历这个列表,如果有子集也是对象再递归一次,最终得到了深拷贝的对象:
function deepCopy(obj) {
    let result = {};
    let keys = Object.keys(obj), key=null, tem=null;
    for(var i=0; i<keys.length; i++) {
        key = keys[i];
        temp = obj[key];
        if (temp && typeof temp === ‘object‘) {
            result[key] = deepCopy(temp);
        }
        else{
            result[key] = temp;
        }
    }
    return result
}
console.log(deepCopy({x:1, y:3}))
var obj7 = {x:1, y:3, z:{name: 7, func: function(){}}};
var obj8 = deepCopy(obj7);
obj8.z.name = 8;
console.log(obj8);  // {x:1, y:3, z:{name: 8, func: function(){}}}
console.log(obj7)   // {x:1, y:3, z:{name: 7, func: function(){}}}

这样,就得到了一个可以兼容多维对象,并且可以实现对function,null,symbol值等特殊值的深拷贝。同时,我们也可以利用第三方库实现深拷贝,如jquery的$.extend和lodash的_.cloneDeep。

针对一种特殊情况的处理

本来上面的代码已经几乎完美地实现了深拷贝,但是,有一种特殊情况,就是当拷贝的对象本来就是一个循环引用的对象时,再去递归,那就无穷无尽会爆栈了。像这样的:

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

我参考的处理方案就是加一层股票率判断,虽然我觉得这种情况根本就没有什么现实存在的意义,但就当一种临界值的处理吧:

function deepCopy(obj, parent) {
    let result = {};
    let keys = Object.keys(obj), key=null, tem=null, parent_=parent;
    if (parent_) {
        if (parent_.originalParent === obj) {
            return parent_.curentParent;
        }
        parent_ = parent_.parent;
    }
    for(var i=0; i<keys.length; i++) {
        key = keys[i];
        temp = obj[key];
        if (temp && typeof temp === ‘object‘) {
            result[key] = deepCopy(temp, {
                originalParent: obj,
                curentParent: result,
                parent: parent
            });
        }
        else{
            result[key] = temp;
        }
    }
    return result
}
obj1.z = obj1;
var obj2 = deepCopy(obj1);
console.log(obj2); //很多东西

这样,一个深拷贝函数就完成了,对于数组的话是同样适用的,毕竟数组也是特殊的对象嘛,当然实际项目中使用第三方库可能还会更加方便一点,很多时候造轮子并不是为了在项目中使用而是为了了解轮子的构造,这个很重要,之后还会继续造轮子的,这是个深入过膝的过程,加油!

参考文章:

原文地址:https://www.cnblogs.com/wancheng7/p/9266026.html

时间: 2024-08-08 10:09:25

探究JS中对象的深拷贝和浅拷贝的相关文章

js 中引用类型 的深拷贝 和 浅拷贝的区别

一.曾经在读JQ源码的时候,对深拷贝算是有了一点的理解.我们在项目中是不是经常会遇到这样的问题呢? 后台返回一个数组对象(引用类型).次数在页面渲染中需要对部分数据进行处理 比如:银行卡62345092534 (这么长) 但在页面显示的时候, 只显示中国银行(3118)但是传给后台的时候.又要传623445242整个号码,我们也许会把var oldData = res.data; 但是我们发现两个数据都变了? 这是为什么呢? 其实就是一个深浅拷贝的问题. 二.浅拷贝 比如数组,对象,这样的引用类

PHP中对象的深拷贝与浅拷贝

先说一下深拷贝和浅拷贝通俗理解 深拷贝:赋值时值完全复制,完全的copy,对其中一个作出改变,不会影响另一个 浅拷贝:赋值时,引用赋值,相当于取了一个别名.对其中一个修改,会影响另一个 PHP中, = 赋值时,普通对象是深拷贝,但对对象来说,是浅拷贝.也就是说,对象的赋值是引用赋值.(对象作为参数传递时,也是引用传递,无论函数定义时参数前面是否有&符号) <?php //普通对象赋值,深拷贝,完全值复制 $m = 1; $n = $m; $n = 2; echo $m;//值复制,对新对象的

java中对象的深拷贝和浅拷贝

根据对象成员变量的拷贝程度(基本数据类型.引用类型),可将拷贝分为浅拷贝和深拷贝. 一.浅拷贝 package javaKeyTechnology; class PerSon{ private String name; private int age; PerSon(String name,int age){ this.name = name; this.age = age; } public void setName(String name){ //私有数据的更改器 this.name = n

js中对象的浅拷贝和深拷贝的区别

js中对象的浅拷贝和深拷贝的区别 浅度拷贝:复制一层对象的属性,并不包括对象里面的为引用类型的数据,当改变拷贝的对象里面的引用类型时,源对象也会改变. 深度拷贝:重新开辟一个内存空间,需要递归拷贝对象里的引用,直到子属性都为基本类型.两个对象对应两个不同的地址,修改一个对象的属性,不会改变另一个对象的属性. 数据的类型: 一般数据(值传递):字符,数值,布尔,undefined 拷贝(复制)时,传递的是值,修改新数据,不会影响老数据 复杂数据(引用传递):对象 拷贝(复制)时,传递的是内存地址的

js中对象的复制,浅复制(浅拷贝)和深复制(深拷贝)

在js中,我们经常复制一个对象,复制数据,那么就会有人问了,怎么复制,今天鹏哥就带来js中的复制方法. JS中对象分为基本类型和复合(引用)类型,基本类型存放在栈内存,复合(引用)类型存放在堆内存. 堆内存用于存放由new创建的对象,栈内存存放一些基本类型的变量和对象的引用变量. 至于堆内存和栈内存的区别介绍,你们可以百度看看. 下面开始讲解复制: 这种只是简单的变量,内存小,我们直接复制不会发生引用. var a=123; var b=a; a=123456; alert(a); //1234

[转]JS中对象与字符串的互相转换

原文地址:http://www.cnblogs.com/luminji/p/3617160.html 在使用 JSON2.JS 文件的 JSON.parse(data) 方法时候,碰到了问题: throw new SyntaxError('JSON.parse'); 查询资料,大概意思如下: JSON.parse方法在遇到不可解析的字符串时,会抛出SyntaxError异常. 即:JSON.parse(text, reviver),This method parses a JSON text t

原生 JS 中对象相关 API 合集

https://juejin.im/entry/58f8a705a0bb9f0065a4cb20 原文链接:https://microzz.com/2017/04/20/jsobject/ 原生 JavaScript 中对象相关 API 合集 - 对象篇.现在 jQuery 已经没有那么有优势了,原生 JS 赶紧学起来... -- 由microzz分享 Microzz [email protected] 主页 文章 标签 GitHub 关于我 掘金专栏 SegmentFault Vue音乐播放器

js引用类型赋值,深拷贝与浅拷贝

JS中引用类型使用等号“=” 赋值,相当于把原来对象的地址拷贝一份给新的对象,这样原来旧的对象与新的对象就指向同一个地址,改变其中一个对象就会影响另外那个对象,也就是所谓的浅拷贝.例如: var arr = ["One","Two","Three"]; var arrto = arr; arrto[1] = "test"; document.writeln("数组的原始值:" + arr + "&

C#对象的深拷贝与浅拷贝

转载自:http://blog.163.com/hr_msn/blog/static/21549405120132250396584/ 深拷贝是指源对象与拷贝对象互相独立,其中任何一个对象的改动都不会对另外一个对象造成影响.举个例子,一个人名叫张三,后来用他克隆(假设法律允许)了另外一个人,叫李四,不管是张三缺胳膊少腿还是李四缺胳膊少腿都不会影响另外一个人.比较典型的就是Value(值)对象,如预定义类型Int32,Double,以及结构(struct),枚举(Enum)等. 考虑以下写法 in