作者 | 吴胜斌

来源 | https://www.simbawu.com/article/search/9

在说深拷贝与浅拷贝前,我们先看两个简单的案例:

//案例1
var num1 = 1, num2 = num1;
console.log(num1) //1
console.log(num2) //1
num2 = 2; //修改num2
console.log(num1) //1
console.log(num2) //2
//案例2
var obj1 = {x: 1, y: 2}, obj2 = obj1;
console.log(obj1) //{x: 1, y: 2}
console.log(obj2) //{x: 1, y: 2}
obj2.x = 2; //修改obj2.x
console.log(obj1) //{x: 2, y: 2}
console.log(obj2) //{x: 2, y: 2}

按照常规思维,obj1应该和num1一样,不会因为另外一个值的改变而改变,而这里的obj1 却随着obj2的改变而改变了。同样是变量,为什么表现不一样呢?这就要引入JS中基本类型和引用类型的概念了。

基本类型和引用类型

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

打个比方,基本类型和引用类型在赋值上的区别可以按“连锁店”和“单店”来理解:基本类型赋值等于在一个新的地方安装连锁店的规范标准新开一个分店,新开的店与其他旧店互不相关,各自运营;而引用类型赋值相当于一个店有两把钥匙,交给两个老板同时管理,两个老板的行为都有可能对一间店的运营造成影响。

上面清晰明了的介绍了基本类型和引用类型的定义和区别。目前基本类型有:

Boolean、Null、Undefined、Number、String、Symbol,引用类型有:Object、Array、Function。之所以说“目前”,因为Symbol就是ES6才出来的,之后也可能会有新的类型出来。

再回到前面的案例,案例1中的值为基本类型,案例2中的值为引用类型。案例2中的赋值就是典型的浅拷贝,并且深拷贝与浅拷贝的概念只存在于引用类型。

深拷贝与浅拷贝

既然已经知道了深拷贝与浅拷贝的来由,那么该如何实现深拷贝?我们先分别看看Array和Object自有方法是否支持:

Array

var arr1 = [1, 2], arr2 = arr1.slice();
console.log(arr1); //[1, 2]
console.log(arr2); //[1, 2]
arr2[0] = 3; //修改arr2
console.log(arr1); //[1, 2]
console.log(arr2); //[3, 2]

此时,arr2的修改并没有影响到arr1,看来深拷贝的实现并没有那么难嘛。我们把arr1改成二维数组再来看看:

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

咦,arr2又改变了arr1,看来slice()只能实现一维数组的深拷贝。

具备同等特性的还有:concat、Array.from() 。

Object

1、Object.assign()

var obj1 = {x: 1, y: 2}, obj2 = Object.assign({}, obj1);
console.log(obj1) //{x: 1, y: 2}
console.log(obj2) //{x: 1, y: 2}
obj2.x = 2; //修改obj2.x
console.log(obj1) //{x: 1, y: 2}
console.log(obj2) //{x: 2, y: 2}
var obj1 = {
   x: 1,
   y: {
       m: 1
   }
};
var obj2 = Object.assign({}, obj1);
console.log(obj1) //{x: 1, y: {m: 1}}
console.log(obj2) //{x: 1, y: {m: 1}}
obj2.y.m = 2; //修改obj2.y.m
console.log(obj1) //{x: 1, y: {m: 2}}
console.log(obj2) //{x: 2, y: {m: 2}}

经测试,Object.assign()也只能实现一维对象的深拷贝。

2、JSON.parse(JSON.stringify(obj))

var obj1 = {
   x: 1,
   y: {
       m: 1
   }
};
var obj2 = JSON.parse(JSON.stringify(obj1));
console.log(obj1) //{x: 1, y: {m: 1}}
console.log(obj2) //{x: 1, y: {m: 1}}
obj2.y.m = 2; //修改obj2.y.m
console.log(obj1) //{x: 1, y: {m: 1}}
console.log(obj2) //{x: 2, y: {m: 2}}

JSON.parse(JSON.stringify(obj)) 看起来很不错,不过MDN文档 的描述有句话写的很清楚:

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

我们再来把obj1改造下:

var obj1 = {
   x: 1,
   y: undefined,
   z: function add(z1, z2) {
       return z1 + z2
   },
   a: Symbol("foo")
};
var obj2 = JSON.parse(JSON.stringify(obj1));
console.log(obj1) //{x: 1, y: undefined, z: ƒ, a: Symbol(foo)}
console.log(JSON.stringify(obj1)); //{"x":1}
console.log(obj2) //{x: 1}

发现,在将obj1进行JSON.stringify()序列化的过程中,y、z、a都被忽略了,也就验证了MDN文档的描述。既然这样,那JSON.parse(JSON.stringify(obj))的使用也是有局限性的,不能深拷贝含有undefined、function、symbol值的对象,不过JSON.parse(JSON.stringify(obj))简单粗暴,已经满足90%的使用场景了。

经过验证,我们发现JS 提供的自有方法并不能彻底解决Array、Object的深拷贝问题。只能祭出大杀器:递归

function deepCopy(obj) {
   // 创建一个新对象
   let result = {}
   let keys = Object.keys(obj),
       key = null,
       temp = null;
   for (let 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;
}
var obj1 = {
   x: {
       m: 1
   },
   y: undefined,
   z: function add(z1, z2) {
       return z1 + z2
   },
   a: Symbol("foo")
};
var obj2 = deepCopy(obj1);
obj2.x.m = 2;
console.log(obj1); //{x: {m: 1}, y: undefined, z: ƒ, a: Symbol(foo)}
console.log(obj2); //{x: {m: 2}, y: undefined, z: ƒ, a: Symbol(foo)}

可以看到,递归完美的解决了前面遗留的所有问题,我们也可以用第三方库:jquery的$.extend和lodash的_.cloneDeep来解决深拷贝。上面虽然是用Object验证,但对于Array也同样适用,因为Array也是特殊的Object。

到这里,深拷贝问题基本可以告一段落了。但是,还有一个非常特殊的场景:

循环引用拷贝

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

此时如果调用刚才的deepCopy函数的话,会陷入一个循环的递归过程,从而导致爆栈。jquery的$.extend也没有解决。解决这个问题也非常简单,只需要判断一个对象的字段是否引用了这个对象或这个对象的任意父级即可,修改一下代码:

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;
}
var obj1 = {
   x: 1,
   y: 2
};
obj1.z = obj1;
var obj2 = deepCopy(obj1);
console.log(obj1); //太长了去浏览器试一下吧~
console.log(obj2); //太长了去浏览器试一下吧~

至此,已完成一个支持循环引用的深拷贝函数。当然,也可以使用lodash的_.cloneDeep。

低门槛彻底理解JavaScript中的深拷贝和浅拷贝的更多相关文章

  1. 理解JavaScript中的深拷贝和浅拷贝

    , num2 = num1;console.log(num1) //1console.log(num2) //1num2 = 2; //修改num2console.log(num1) //1conso ...

  2. javascript中的深拷贝与浅拷贝

    javascript中的深拷贝与浅拷贝 基础概念 在了解深拷贝与浅拷贝的时候需要先了解一些基础知识 核心知识点之 堆与栈 栈(stack)为自动分配的内存空间,它由系统自动释放: 堆(heap)则是动 ...

  3. JavaScript中的深拷贝和浅拷贝!【有错误】还未修改!请逛其他园子!

    JavaScript中的深拷贝和浅拷贝! 浅拷贝 1.浅拷贝只是拷贝一层,更深层次对象级别的只拷贝引用.{也就是拷贝的是地址!简而言之就是在新的对象中修改深层次的值也会影响原来的对象!} // 2.深 ...

  4. 深入剖析javaScript中的深拷贝和浅拷贝

    如何区分深拷贝与浅拷贝,简单来说,假设B复制了A,当修改A时,看B是否会发生变化,如果B也跟着变了,说明这是浅拷贝,如果B没变,那就是深拷贝:我们先看两个简单的案例: //案例1(深拷贝) var a ...

  5. Javascript中的深拷贝和浅拷贝

    var obj = { a:1, arr: [1,2] }; var obj1 = obj; //浅复制 var obj2 = deepCopy(obj); //深复制 javascript中创建对象 ...

  6. 深入理解JavaScript中的属性和特性

    深入理解JavaScript中的属性和特性 JavaScript中属性和特性是完全不同的两个概念,这里我将根据自己所学,来深入理解JavaScript中的属性和特性. 主要内容如下: 理解JavaSc ...

  7. 【干货理解】理解javascript中实现MVC的原理

    理解javascript中的MVC MVC模式是软件工程中一种软件架构模式,一般把软件模式分为三部分,模型(Model)+视图(View)+控制器(Controller); 模型:模型用于封装与应用程 ...

  8. 理解JavaScript中的原型继承(2)

    两年前在我学习JavaScript的时候我就写过两篇关于原型继承的博客: 理解JavaScript中原型继承 JavaScript中的原型继承 这两篇博客讲的都是原型的使用,其中一篇还有我学习时的错误 ...

  9. 深入理解JavaScript中创建对象模式的演变(原型)

    深入理解JavaScript中创建对象模式的演变(原型) 创建对象的模式多种多样,但是各种模式又有怎样的利弊呢?有没有一种最为完美的模式呢?下面我将就以下几个方面来分析创建对象的几种模式: Objec ...

随机推荐

  1. C++ KMP文本匹配

    代码如下: 环境为VC #include <iostream> #include <algorithm> #include <string> #include &l ...

  2. c# 如何获取系统管理员权限(UAC) 及判断当前是否是管理员权限

    环境说明: VS2012,windows 7  亲自验证过win7 和xp ,XP直接不弹框,因为XP没有UAC控制机制 步骤1: 右键项目-->属性-->安全性-->选中[启用Cl ...

  3. webpack使用的补充

    1.分离生产环境和开发环境的wepack.config.js 我们可以将生产环境和开发环境中的配置分离为两个不同的文件,并且还维护一个共同的配置文件 common,可以通过 webpack.merge ...

  4. 【Java】maven打包源码、依赖插件

    <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactI ...

  5. ajax 封装(集中 认证、错误、请求loading处理)

    一.为什么要对 ajax 进行封装:    (在使用antd pro 开发项目时,里面默认是把请求进行了封装的,放在 utils/request.js 中.使用起来非常方便   https://pro ...

  6. window安装nexus和配置

    原文 http://www.cnblogs.com/fxmemory/p/7133672.html 一. 认识mav仓库 1.1 maven仓库的作用   回想之前不用maven的时候,我们用ecli ...

  7. 杂项:JFB-权限设置

    ylbtech-杂项:JFB-权限设置 1. 家政经理返回顶部 1. if (UserContext.GetTeamId() == (int)UserType.Manager) { condition ...

  8. GET和POST区别及缓存问题

    2.就是get和post区别的缓存问题. 首先要了解什么是缓存. HTTP缓存的基本目的就是使应用执行的更快,更易扩展,但是HTTP缓存通常只适用于idempotent request(可以理解为查询 ...

  9. js另存为、打印、属性、加入收藏、关闭等代码

    js打开代码 <input name=Button onClick=document.all.WebBrowser.ExecWB(1,1) type=button value=打开> &l ...

  10. CSS 6种完全居中最佳实践(整理)

    2016年4月28日 1.最佳法: .Absolute-Center { width: 50%; height: 50%; overflow: auto; margin: auto; position ...