浅拷贝与深拷贝
我们可以先看看一个常遇到的一个小问题
let a = { age:1 } let b = a a.age = 2 console.log(b.age) //2
从上面的例子中我们看到了,如果给一个变量赋值一个对象,那么两者的值会是同一个引用,其中一方变化,另一方也会有相应的改变。
通常我们开发中不希望出现这样的问题,我们可以使用浅拷贝来解决这个问题。
此篇文章中也会简单阐述到栈堆,基本数据类型与引用数据类型,因为这些概念能更好的让你理解深拷贝与浅拷贝。
我们来举个浅拷贝例子:
let a=[0,1,2,3,4], b=a; console.log(a===b); a[0]=1; console.log(a,b);
嗯?明明b复制了a,为啥修改数组a,数组b也跟着变了,这里我不禁陷入了沉思。
那么这里,就得引入基本数据类型与引用数据类型的概念了。
面试常问,基本数据类型有哪些,number,string,boolean,null,undefined,symbol以及未来ES10新增的BigInt(任意精度整数)七类。
引用数据类型(Object类)有常规名值对的无序对象{a:1},数组[1,2,3],以及函数等。
而这两类数据存储分别是这样的:
a.基本类型--名值存储在栈内存中,例如let a=1;
当你b=a复制时,栈内存会新开辟一个内存,例如这样:
所以当你此时修改a=2,对b并不会造成影响,因为此时的b已自食其力,翅膀硬了,不受a的影响了。当然,let a=1,b=a;虽然b不受a影响,但这也算不上深拷贝,因为深拷贝本身只针对较为复杂的object类型数据。
b.引用数据类型--名存在栈内存中,值存在于堆内存中,但是栈内存会提供一个引用的地址指向堆内存中的值,我们以上面浅拷贝的例子画个图:
当b=a进行拷贝时,其实复制的是a的引用地址,而并非堆里面的值。
而当我们a[0]=1时进行数组修改时,由于a与b指向的是同一个地址,所以自然b也受了影响,这就是所谓的浅拷贝了。
那,要是在堆内存中也开辟一个新的内存专门为b存放值,就像基本类型那样,岂不就达到深拷贝的效果了
一、数据类型
数据分为基本数据类型(String, Number, Boolean, Null, Undefined,Symbol)和对象数据类型。
- 基本数据类型的特点:直接存储在栈(stack)中的数据
- 引用数据类型的特点:存储的是该对象在栈中引用,真实的数据存放在堆内存里
引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址。当解释器寻找引用值时,会首先检索其在栈中的地址,取得地址后从堆中获得实体。
二、浅拷贝与深拷贝
深拷贝和浅拷贝是只针对Object和Array这样的引用数据类型的。
深拷贝和浅拷贝的示意图大致如下:
浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存。
但深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象。
三、赋值和浅拷贝的区别
-
当我们把一个对象赋值给一个新的变量时,赋的其实是该对象的在栈中的地址,而不是堆中的数据。也就是两个对象指向的是同一个存储空间,无论哪个对象发生改变,其实都是改变的存储空间的内容,因此,两个对象是联动的。
-
浅拷贝是按位拷贝对象,它会创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值;如果属性是内存地址(引用类型),拷贝的就是内存地址 ,因此如果其中一个对象改变了这个地址,就会影响到另一个对象。即默认拷贝构造函数只是对对象进行浅拷贝复制(逐个成员依次拷贝),即只复制对象空间而不复制资源。
我们先来看两个例子,对比赋值与浅拷贝会对原对象带来哪些改变?
// 对象赋值 var obj1 = { 'name' : 'zhangsan', 'age' : '18', 'language' : [1,[2,3],[4,5]], }; var obj2 = obj1; obj2.name = "lisi"; obj2.language[1] = ["二","三"]; console.log('obj1',obj1) console.log('obj2',obj2)
// 浅拷贝 var obj1 = { 'name' : 'zhangsan', 'age' : '18', 'language' : [1,[2,3],[4,5]], }; var obj3 = shallowCopy(obj1); obj3.name = "lisi"; obj3.language[1] = ["二","三"]; function shallowCopy(src) { var dst = {}; for (var prop in src) { if (src.hasOwnProperty(prop)) { dst[prop] = src[prop]; } } return dst; } console.log('obj1',obj1) console.log('obj3',obj3)
上面例子中,obj1是原始数据,obj2是赋值操作得到,而obj3浅拷贝得到。我们可以很清晰看到对原始数据的影响,具体请看下表:
补充:
ES6的展开运算符(...)
let obj={name:'程序员',age:{child:12}} let copy={...obj} copy.name="zhujianxiong" copy.age.child="23" console.log('obj',obj) console.log('copy',copy)
四、浅拷贝的实现方式
1.Object.assign()
Object.assign()
方法用于将所有可枚举属性的值从一个或多个源对象复制到目标对象。它将返回目标对象。
第一个参数target决定了你的对象被拷贝到哪个目标对象上面,如果你不想对原始对象产生影响,就定义一个空对象{}作为target,单单这样还不够,sources只设置原始对象obj的话,表示对原始对象的“浅拷贝”
Object.assign() 方法可以把任意多个的源对象自身的可枚举属性拷贝给目标对象,然后返回目标对象。但是 Object.assign()进行的是浅拷贝,拷贝的是对象的属性的引用,而不是对象本身。
var obj = { a: {a: "kobe", b: 39} }; var initalObj = Object.assign({}, obj); initalObj.a.a = "wade"; console.log(obj.a.a); //wade
浅拷贝一个对象
var arr=[1,13,24,11,11,14,1,2] var res=Object.assign([],arr); res[2]=100; console.log(arr) //[1, 13, 24, 11, 11, 14, 1, 2]
console.log(res) //[1, 13, 100, 11, 11, 14, 1, 2]
浅拷贝一个数组
注意:当object只有一层的时候,是深拷贝
let obj = { username: 'kobe' }; let obj2 = Object.assign({},obj); obj2.username = 'wade'; console.log(obj);//{username: "kobe"}
2.Array.prototype.concat()
let arr = [1, 3, { username: 'kobe' }]; let arr2=arr.concat(); arr2[2].username = 'wade'; console.log(arr);
3.Array.prototype.slice()
let arr = [1, 3, { username: ' kobe' }]; let arr3 = arr.slice(); arr3[2].username = 'wade' console.log(arr);
同样修改新对象会改到原对象:
关于Array的slice和concat方法的补充说明:Array的slice和concat方法不修改原数组,只会返回一个浅复制了原数组中的元素的一个新数组。
原数组的元素会按照下述规则拷贝:
- 如果该元素是个对象引用(不是实际的对象),slice 会拷贝这个对象引用到新的数组里。两个对象引用都引用了同一个对象。如果被引用的对象发生改变,则新的和原来的数组中的这个元素也会发生改变。
- 对于字符串、数字及布尔值来说(不是 String、Number 或者 Boolean 对象),slice 会拷贝这些值到新的数组里。在别的数组里修改这些字符串或数字或是布尔值,将不会影响另一个数组。
可能这段话晦涩难懂,我们举个例子,将上面的例子小作修改:
let arr = [1, 3, { username: ' kobe' }]; let arr3 = arr.slice(); arr3[1] = 2 console.log(arr,arr3);
五、深拷贝的实现方式
1.JSON.parse(JSON.stringify())
let arr = [1, 3, { username: ' kobe' }]; let arr4 = JSON.parse(JSON.stringify(arr)); arr4[2].username = 'duncan'; console.log(arr, arr4)
原理: 用JSON.stringify将对象转成JSON字符串,再用JSON.parse()把字符串解析成对象,一去一来,新的对象产生了,而且对象会开辟新的栈,实现深拷贝。
这种方法虽然可以实现数组或对象深拷贝,但不能处理函数
let arr = [1, 3, { username: ' kobe' },function(){}]; let arr4 = JSON.parse(JSON.stringify(arr)); arr4[2].username = 'duncan'; console.log(arr, arr4)
这是因为JSON.stringify() 方法是将一个JavaScript值(对象或者数组)转换为一个 JSON字符串,不能接受函数
2.手写递归方法
递归方法实现深度克隆原理:遍历对象、数组直到里边都是基本数据类型,然后再去复制,就是深度拷贝
//定义检测数据类型的功能函数 function checkedType(target) { return Object.prototype.toString.call(target).slice(8, -1) } //实现深度克隆---对象/数组 function clone(target) { //判断拷贝的数据类型 //初始化变量result 成为最终克隆的数据 let result, targetType = checkedType(target) if (targetType === 'object') { result = {} } else if (targetType === 'Array') { result = [] } else { return target } //遍历目标数据 for (let i in target) { //获取遍历数据结构的每一项值。 let value = target[i] //判断目标结构里的每一值是否存在对象/数组 if (checkedType(value) === 'Object' || checkedType(value) === 'Array') { //对象/数组里嵌套了对象/数组 //继续遍历获取到value值 result[i] = clone(value) } else { //获取到value值是基本的数据类型或者是函数。 result[i] = value; } } return result }
3.函数库lodash
该函数库也有提供_.cloneDeep用来做 Deep Copy
var _ = require('lodash'); var obj1 = { a: 1, b: { f: { g: 1 } }, c: [1, 2, 3] }; var obj2 = _.cloneDeep(obj1); console.log(obj1.b.f === obj2.b.f); // false
补充:数组拷贝问题
javascript数组在使用时,时常会遇到数组备份的情况,之后对数组做些修改,再同原数组进行比对,查看数组的变化,这里就涉及到一个数组拷贝的问题。
浅拷贝只复制一层对象的属性;深拷贝递归复制了所有层级。
数组的拷贝,通常可以使用一个新的数组,指向现有数组
var arr = [el1, el2, el3...]; var arr2 = arr; //这种写法,待arr2做改变时,我们查看arr会同步做修改 var arr = ['liuche', 'zhouyafu', 'huoqubing', 'weiqing']; var arr2 = arr; arr2.push('liguang'); alert(arr); // 'liuche', 'zhouyafu', 'huoqubing', 'weiqing', 'liguang' alert(arr2); // 'liuche', 'zhouyafu', 'huoqubing', 'weiqing', 'liguang'
这里我们看到,待修改数组arr2时,arr同时做了改变,这显然不是我们想要的结果。
示例中,这种直接将数组引用复制的方式就是浅拷贝。
那我们想要对数组进行备份的话,该如何操作呢,可以借助于Array对象的slice()方法和concat()方法。
slice方法
slice() 方法可从已有的数组中返回选定的元素。
arrayObject.slice(start,end)
其中:
start,必需。规定从何处开始选取。如果是负数,那么它规定从数组尾部开始算起的位置,从0开始。也就是说,-1 指最后一个元素,-2 指倒数第二个元素,以此类推。
end,可选。规定从何处结束选取。该参数是数组片断结束处的数组下标。如果没有指定该参数,那么切分的数组包含从 start 到数组结束的所有元素。如果这个参数是负数,那么它规定的是从数组尾部开始算起的元素。
slice方法返回一个新的数组,包含从 start 到 end (不包括该元素)的 arrayObject 中的元素。
若start为0,end 缺省,则相当于截取了整个数组的元素值,即我们这里要说的数组拷贝。
var arr = ['liuche', 'zhouyafu', 'huoqubing', 'weiqing']; var arr2 = arr.slice(0); arr2.push('liguang'); alert(arr); // 'liuche', 'zhouyafu', 'huoqubing', 'weiqing' alert(arr2); // 'liuche', 'zhouyafu', 'huoqubing', 'weiqing', 'liguang' //但是,如果遇到多维数组,slice方法并不奏效 //实例1 var arr = [['liuche', 'zhouyafu', 'weiqing'], ['chengajiao', 'weizifu', 'liupiao']]; var arr2 = arr.slice(0); arr2.push('liguang'); alert(arr); // liuche,zhouyafu,weiqing,chengajiao,weizifu,liupiao alert(arr2); // liuche,zhouyafu,weiqing,chengajiao,weizifu,liupiao,liguang //实例2 var arr = [['liuche', 'zhouyafu', 'weiqing'], ['chengajiao', 'weizifu', 'liupiao']]; var arr2 = arr.slice(0); arr2[0][3] = 'liguang'; alert(arr); // liuche,zhouyafu,weiqing,liguang,chengajiao,weizifu,liupiao alert(arr2); // liuche,zhouyafu,weiqing,liguang,chengajiao,weizifu,liupiao
上面两个示例中,实例1,arr2修改后,arr并未跟着一起修改,因为arr2的修改,是给arr2新增了arr[3]元素;但是实例2中,arr2修改后,arr跟着一起修改了,原因在于arr中arr[0] 元素是个数组对象,并非单的数值。
concat方法
concat() 方法用于连接两个或多个数组。
该方法不会改变现有的数组,而仅仅会返回被连接数组的一个副本。
arrayObject.concat(arrayX,arrayX,......,arrayX)
其中
arrayX,必需。该参数可以是具体的值,也可以是数组对象。可以是任意多个。
concat方法返回一个新的数组。该数组是通过把所有 arrayX 参数添加到 arrayObject 中生成的。如果要进行 concat() 操作的参数是数组,那么添加的是数组中的元素,而不是数组。
使用concat() 方法时,若arrayX为空,则相当于原数组与一个空数组拼接,即返回原数组,做到原数组的拷贝。
那使用concat方法再来看看刚才的示例
var arr = [['liuche', 'zhouyafu', 'weiqing'], ['chengajiao', 'weizifu', 'liupiao']]; var arr2 = arr.concat(); arr2[0][3] = 'liguang'; alert(arr); // liuche,zhouyafu,weiqing,liguang,chengajiao,weizifu,liupiao alert(arr2); // liuche,zhouyafu,weiqing,liguang,chengajiao,weizifu,liupiao
结果同上相同,并未得到我们想要的结果
扩展运算符[...x]
使用扩展运算符可以返回一个新数组
实在无解之际,咨询了下一个专职做前端的同事,了解到一个万能的JS拷贝方法,无论是数组还是对象均可以实现深拷贝
JSON.parse(JSON.stringify(arr));
这个方法其实比较简单,先把所有的对象属性解析为简单数值,再将数值拼接解析为JS对象。
看之前的例子
var arr = [['liuche', 'zhouyafu', 'weiqing'], ['chengajiao', 'weizifu', 'liupiao']]; var arr2 = JSON.parse(JSON.stringify(arr)); arr2[0][3] = 'liguang'; alert(arr); // liuche,zhouyafu,weiqing,chengajiao,weizifu,liupiao alert(arr2); // liuche,zhouyafu,weiqing,liguang,chengajiao,weizifu,liupiao
解决上面的问题。
经过上面的例子和分析,可以看出来,简单数组的拷贝可以通过slice方法和concat方法来实现,对于多维数组的实现,必须通过JSON.parse(JSON.stringify(obj))方法来实现。初次学习,有不当地方还望大家批评斧正。谢谢。
文章首发地址为我的GitHub博客,敬请关注!