Open maicFir opened 2 years ago
js 的浅拷贝与深拷贝在业务中时常有用到,关于浅拷贝与深拷贝的剖析文章层出不穷,本文是笔者对于深拷贝与浅拷贝的理解,一起来夯实 js 语言基础知识的理解吧。
浅拷贝
深拷贝
正文开始...
在阅读文章之前,本文主要从以下几个方面去探讨
为什么会有浅拷贝与深拷贝
浅拷贝是什么,深拷贝又是什么
浅拷贝与深拷贝有何区别
写一个例子佐证以上所有的观点
我们知道在js中基础数据类型是存放在栈内存中的,而引用数据类型是存放在栈地址引用的一个堆内存中。为什么两种数据会存放方式不同?这是一个值的思考的问题?我的猜想,引用数据类型是复杂的数据结构,本质上也是存放栈地址的引用,只是这个地址指向了另外一个堆内存空间,如果他们都是放在一起的话,就不太好区分你是基础数据类型,还是引用数据类型了。
js
首先它们都是拷贝,一个是浅,一个是深,我们先说结论,浅拷贝是基础数据类型的拷贝,只会拷贝一层,如果遇到拷贝的数据是引用数据,那么浅拷贝的数据与原有数据是同一份引用。
而深拷贝是遇到引用数据类型会创建一个新的对象,遍历原有对象,对新对象进行动态赋值,修改新对象引用不影响原有对象的属性值
我们用一个图来解释上面两段比较长的话
基础数据类型直接存放在栈地址内存中,而引用数据类型是存放在栈内存地址的引用中,这个引用实际上指向的区域是一块堆内存空间
在了解浅拷贝与深拷贝之前,我们先来了解下值拷贝
值拷贝
当我对原有基础数据类型与引用数据类型进行赋值时
赋值
用下面代码示例上图
var userName = 'Maic'; var age = 18; var userInfo = { name: 'Maic', age: 18, fav: { play1: 'ping pang', play2: 'basket ball' } }; var cacheUserName = userName; var cacheAge = age; // 对象值拷贝 var cacheUserInfo = userInfo; cacheUserName = 'Tom'; cacheAge = 20; cacheUserInfo.name = 'jake'; cacheUserInfo.age = 10; cacheUserInfo.fav.play1 = 'swim'; console.log(userName, age, userInfo, '------', cacheUserName, cacheAge, cacheUserInfo);
然后运行node index.js 从执行结果上来看
node index.js
Maic 18 { name: 'jake', age: 10, fav: { play1: 'swim', play2: 'basket ball' } } ------ Tom 20 { name: 'jake', age: 10, fav: { play1: 'swim', play2: 'basket ball' } }
因此可以得出结论
基础数据类型的赋值,是值的拷贝,会重新开辟一个栈空间,新拷贝的值修改不会影响原有数据类型
引用数据类型的赋值,原有引用数据与新赋值的数据指向的是同一份地址,修改引用数据的属性会影响原来的
以上是两种数据类型值的拷贝,貌似与浅拷贝还有离得有点远
于是我们看下对象扩展的浅拷贝
... // 对象浅拷贝 var cacheUserInfo = { ...userInfo } // 与下面等价 // var cacheUserInfo = Object.assign({}, userInfo); // 修改值拷贝后值 cacheUserName = 'Tom'; cacheAge = 20; cacheUserInfo.name = 'jake'; cacheUserInfo.age = 10; cacheUserInfo.fav.play1 = 'swim'; console.log(userName, age, userInfo, '------', cacheUserName, cacheAge, cacheUserInfo);
我使用了es6对象扩展对原有对象进行拷贝,那么此时结果是怎么样
es6
Maic 18 { name: 'Maic', age: 18, fav: { play1: 'swim', play2: 'basket ball' } } ------ Tom 20 { name: 'jake', age: 10, fav: { play1: 'swim', play2: 'basket ball' } }
不知道注意到没有,在引用数据类型的第一级如果这个属性是基础数据类型,那么修改并不会影响原有的值,如果属性是引用数据类型,那么这层结构会是一个值拷贝,修改新赋值属性,会影响到原有的对象属性
我们看下图理解下
因此我们可以得出结论,浅拷贝如果遇到基础数据类型,那么修改新值,不会影响原有的值,但是如果数据是引用数据类型,那么新修改的值会影响原有的值,因为新修改的与原修改的是同一份引用。
所以浅拷贝只会拷贝一层,如果数据是引用数据类型,实际上会直接引用同一份数据。
浅
深拷贝顾名思义,从表现层来看就是,就是为了修改新数据而不影响原有数据而产生的。
我们举个栗子
var userInfo = { name: 'Maic', age: 18, fav: { play1: 'ping pang', play2: 'basket ball' } };
当我需要修改userInfo.fav.play1的值,而不想影响原有userInfo对象的值,那么此时你就会想到深拷贝,那怎么深拷贝呢。
userInfo.fav.play1
userInfo
利用JSON.stringify(data)拷贝对象
JSON.stringify(data)
... const newUseInfo = JSON.parse(JSON.stringify(userInfo)); newUseInfo.fav.play1 = 'hello'; console.log(userInfo, '----', newUseInfo);
结果:
{ name: 'Maic', age: 18, fav: { play1: 'ping pang', play2: 'basket ball' } } ---------- { name: 'Maic', age: 18, fav: { play1: 'hello', play2: 'basket ball' } }
但是我们得考虑到JSON.stringify这种有种缺陷,必须是json对象,有其他比如方法这种会被自动过滤处理。而且如果json对象格式错误,就会抛出异常,所以我们看下另外一种方案。
JSON.stringify
json
方法
使用代理对象思想,将原有对象拷贝一份,然后再赋值
var userInfo = { name: 'Maic', age: 18, fav: { play1: 'ping pang', play2: 'basket ball' }, fav2: [ { a: 1, b: 2 }, { a: 3, b: 4 } ] }; const isType = (val) => { return (type) => Object.prototype.toString.call(val) === `[object ${type}]`; }; function deepMerge(target = {}) { const ret = {}; for (let key in target) { if (target.hasOwnProperty(key)) { if (isType(target[key])('Object')) { ret[key] = deepMerge(target[key]); } else { ret[key] = target[key]; } } } return ret; } const cacheObj = deepMerge(userInfo); cacheObj.fav.play1 = '111'; cacheObj.fav2[0].a = '666'; console.log(userInfo, '-----', cacheObj);
最终结果是
{ name: 'Maic', age: 18, fav: { play1: 'ping pang', play2: 'basket ball' }, fav2: [ { a: '666', b: 2 }, { a: 3, b: 4 } ] } ------- { name: 'Maic', age: 18, fav: { play1: '111', play2: 'basket ball' }, fav2: [ { a: '666', b: 2 }, { a: 3, b: 4 } ] }
但是如果数据中有数组,貌似数组的这种情况还是同一份值,那是因为直接赋值了
... function deepMerge(target = {}) { const ret = {}; for (let key in target) { if (target.hasOwnProperty(key)) { if (isType(target[key])('Object')) { ret[key] = deepMerge(target[key]) } else { // 是因为这里直接赋值了操作 ret[key] = target[key]; } } } return ret; }
于是需要多加一个条件,需要对数组进行判断
const isType = (val) => { return (type) => Object.prototype.toString.call(val) === `[object ${type}]`; }; function deepMerge(target) { const ret = Array.isArray(target) ? [] : {}; for (let key in target) { if (target.hasOwnProperty(key)) { if (isType(target[key])('Object')) { ret[key] = deepMerge(target[key]); } else if (isType(target[key])('Array')) { // 判断数组,并再次递归,用数组concat方法添加该数据 ret[key] = [].concat([...deepMerge(target[key])]); } else { ret[key] = target[key]; } } } return ret; } const cacheObj = deepMerge(userInfo); cacheObj.fav.play1 = '111'; cacheObj.fav2[0].a = '666'; console.log(userInfo, '----', cacheObj);
此时结果
{ name: 'Maic', age: 18, fav: { play1: 'ping pang', play2: 'basket ball' }, fav2: [ { a: 1, b: 2 }, { a: 3, b: 4 } ], fav3: [ 1, 2, 3 ] } ------- { name: 'Maic', age: 18, fav: { play1: '111', play2: 'basket ball' }, fav2: [ { a: '666', b: 2 }, { a: 3, b: 4 } ], fav3: [ 1, 2, 3 ] }
以上用一个图来进一步理解下
真是人才,深拷贝原来是这样的
通过以上例子,我们已经知道
浅拷贝如果拷贝对象内部的数据是基础数据类型,那么直接拷贝,新对象修改值,不会影响原有的值,如果拷贝的对象是一个引用数据类型,那么会是一个值的引用,此时新拷贝对象修改其值会影响原有的值。浅拷贝只会拷贝一层,拷贝的内部引用数据类型是同一份。
深拷贝本质上就是无论原对象值是基础数据类型,还是引用数据类型,我新拷贝的对象修改对象内部的值,并不会影响原有对象的值
另外还要有一点值拷贝,也是赋值,基础数据类型赋值,新修改的数据不会影响原有的数据,但是如果是引用数据类型,那么新拷贝的值修改会影响原有数据
值拷贝(直接赋值操作),主要区分基础数据类型与引用数据类型,如果是基础数据类型,那么新值修改不会影响原有的值,但是如果引用数据类型,那么新修改的值会影响原有数据类型
浅拷贝,如果拷贝的对象内部属性是引用数据类型,那么像es6中的对象扩展符或者Object.assign都是浅拷贝操作,新拷贝的基础数据类型修改不会影响原有值,但是如果拷贝的是引用数据类型,那么新拷贝的值与原有值是同一份引用,新值修改会影响原有的值
Object.assign
深拷贝,一句话,新拷贝的对象修改值不会影响原有值
本文示例code example
js 的
浅拷贝
与深拷贝
在业务中时常有用到,关于浅拷贝
与深拷贝
的剖析文章层出不穷,本文是笔者对于深拷贝与浅拷贝的理解,一起来夯实 js 语言基础知识的理解吧。正文开始...
在阅读文章之前,本文主要从以下几个方面去探讨
为什么会有浅拷贝与深拷贝
浅拷贝是什么,深拷贝又是什么
浅拷贝与深拷贝有何区别
写一个例子佐证以上所有的观点
为什么会有浅拷贝与深拷贝
我们知道在
js
中基础数据类型是存放在栈内存中的,而引用数据类型是存放在栈地址引用的一个堆内存中。为什么两种数据会存放方式不同?这是一个值的思考的问题?我的猜想,引用数据类型是复杂的数据结构,本质上也是存放栈地址的引用,只是这个地址指向了另外一个堆内存空间,如果他们都是放在一起的话,就不太好区分你是基础数据类型,还是引用数据类型了。首先它们都是拷贝,一个是浅,一个是深,我们先说结论,浅拷贝是基础数据类型的拷贝,只会拷贝一层,如果遇到拷贝的数据是引用数据,那么浅拷贝的数据与原有数据是同一份引用。
而深拷贝是遇到引用数据类型会创建一个新的对象,遍历原有对象,对新对象进行动态赋值,修改新对象引用不影响原有对象的属性值
我们用一个图来解释上面两段比较长的话
基础数据类型直接存放在栈地址内存中,而引用数据类型是存放在栈内存地址的引用中,这个引用实际上指向的区域是一块堆内存空间
在了解
浅拷贝
与深拷贝
之前,我们先来了解下值拷贝
值拷贝
当我对原有基础数据类型与引用数据类型进行
赋值
时用下面代码示例上图
然后运行
node index.js
从执行结果上来看因此可以得出结论
基础数据类型的赋值,是值的拷贝,会重新开辟一个栈空间,新拷贝的值修改不会影响原有数据类型
引用数据类型的赋值,原有引用数据与新赋值的数据指向的是同一份地址,修改引用数据的属性会影响原来的
以上是两种数据类型值的拷贝,貌似与浅拷贝还有离得有点远
浅拷贝
于是我们看下对象扩展的
浅拷贝
我使用了
es6
对象扩展对原有对象进行拷贝,那么此时结果是怎么样不知道注意到没有,在引用数据类型的第一级如果这个属性是基础数据类型,那么修改并不会影响原有的值,如果属性是引用数据类型,那么这层结构会是一个值拷贝,修改新赋值属性,会影响到原有的对象属性
我们看下图理解下
因此我们可以得出结论,浅拷贝如果遇到基础数据类型,那么修改新值,不会影响原有的值,但是如果数据是引用数据类型,那么新修改的值会影响原有的值,因为新修改的与原修改的是同一份引用。
所以
浅
拷贝只会拷贝一层,如果数据是引用数据类型,实际上会直接引用同一份数据。深拷贝
深拷贝顾名思义,从表现层来看就是,就是为了修改新数据而不影响原有数据而产生的。
我们举个栗子
当我需要修改
userInfo.fav.play1
的值,而不想影响原有userInfo
对象的值,那么此时你就会想到深拷贝,那怎么深拷贝呢。利用
JSON.stringify(data)
拷贝对象结果:
但是我们得考虑到
JSON.stringify
这种有种缺陷,必须是json
对象,有其他比如方法
这种会被自动过滤处理。而且如果json
对象格式错误,就会抛出异常,所以我们看下另外一种方案。使用代理对象思想,将原有对象拷贝一份,然后再赋值
最终结果是
但是如果数据中有数组,貌似数组的这种情况还是同一份值,那是因为直接赋值了
于是需要多加一个条件,需要对数组进行判断
此时结果
以上用一个图来进一步理解下
真是人才,深拷贝原来是这样的
深拷贝与浅拷贝的区别
通过以上例子,我们已经知道
浅拷贝
如果拷贝对象内部的数据是基础数据类型,那么直接拷贝,新对象修改值,不会影响原有的值,如果拷贝的对象是一个引用数据类型,那么会是一个值的引用,此时新拷贝对象修改其值会影响原有的值。浅拷贝
只会拷贝一层,拷贝的内部引用数据类型是同一份。深拷贝
本质上就是无论原对象值是基础数据类型,还是引用数据类型,我新拷贝的对象修改对象内部的值,并不会影响原有对象的值另外还要有一点
值拷贝
,也是赋值,基础数据类型赋值,新修改的数据不会影响原有的数据,但是如果是引用数据类型,那么新拷贝的值修改会影响原有数据总结
值拷贝(直接赋值操作),主要区分基础数据类型与引用数据类型,如果是基础数据类型,那么新值修改不会影响原有的值,但是如果引用数据类型,那么新修改的值会影响原有数据类型
浅拷贝,如果拷贝的对象内部属性是引用数据类型,那么像
es6
中的对象扩展符或者Object.assign
都是浅拷贝操作,新拷贝的基础数据类型修改不会影响原有值,但是如果拷贝的是引用数据类型,那么新拷贝的值与原有值是同一份引用,新值修改会影响原有的值深拷贝,一句话,新拷贝的对象修改值不会影响原有值
本文示例code example