了解拷贝背后的过程,避免不必要的错误,Js专题系列之深浅拷贝,我们一起加油~
当我们在操作数据之前,可能会遇到这样的情况:
当我们遇到类似需要场景时,首先想到的就是拷贝它,殊不知拷贝也大有学问哦~
下面简单的例子,你是否觉得熟悉?
var str = 'How are you';
var newStr = str;
newStr = 10
console.log(str); // How are you
console.log(newStr); // 10
大家都能想到,字符串是基本类型,它的值保存在栈中,在对它进行拷贝时,其实是为新变量开辟了新的空间。 str
和newStr
就好比两个一模一样的房间,布局一致却毫无关联。
var data = [1, 2, 3, 4, 5];
var newData = data;
newData[0] = 100;
console.log(data[0]); // 100
console.log(newData[0]); // 100
类似的代码段,但这次我们使用数组这个引用类型举例,你会发现修改赋值后的数据,原始数据也跟着改变了,这显然不满足我们的需要。本篇文章就来聊一聊引用数据拷贝的学问。
如果大家对Js的数据类型存在着疑问,不妨看看《JavaScript中的基本数据类型》
拷贝的划分都是针对引用类型来讨论的,浅拷贝——顾名思义,浅拷贝就是“浅层拷贝”,实际上只做了表面功夫:
var arr = [1, 2, 3, 4];
var newArr = arr;
console.log(arr, newArr); // [1,2,3,4] [1,2,3,4]
newArr[0] = 100;
console.log(arr, newArr) // [100,2,3,4] [100,2,3,4]
不发生事情(操作)还好,一旦对新数组进行了操作,两个变量中所保存的数据都会发生改变。
发生这类情况的原因也是因为引用类型
的基本特性:
数组中的slice和concat都会返回一个新数组,我们一起来试一下:
var arr = [1,2,3,4];
var res = arr.slice();
// 或者
res = arr.concat()
res[0] = 100;
console.log(arr); // [1,2,3,4]
这个问题这么快就解决了?虽然对这一层数据进行了这样的的处理后,确实解决了问题,但!
var arr = [ {
age: 23 }, [1,2,3,4] ];
var newArr = arr.concat();
arr[0].age = 18;
arr[1][0] = 100;
console.log(arr) // [ {age: 18}, [100,2,3,4] ]
console.log(newArr) // [ {age: 18}, [100,2,3,4] ]
果然事情没有那么简单,这也是因为数据类型的不同。
S 不允许我们直接操作内存中的地址,也就是说不能操作对象的内存空间,所以,我们对对象的操作都只是在操作它的引用而已。
既然浅拷贝
达不到我们的要求,本着效率的原则,我们找找有没有帮助我们实现深拷贝
的方法。
数据的方法失败了,还有没有其他办法?我们需要实现真正意义上的拷贝出独立的数据。
这里我们利用JSON的两个方法,JSON.stringify()
,JSON.parse()
来实现最简洁的深拷贝
var arr = ['str', 1, true, [1, 2], {
age: 23}]
var newArr = JSON.parse( JSON.stringify(arr) );
newArr[3][0] = 100;
console.log(arr); // ['str', 1, true, [1, 2], {age: 23}]
console.log(newArr); // ['str', 1, true, [100, 2], {age: 23}]
这个方法应该是实现深拷贝最简洁的方法,但是,它仍然存在问题,我们先来看看刚才都做了些什么:
arr
JSON 字符串
值或对象
理解:
我们可以理解为,将原始数据转换为新字符串
,再通过新字符串
还原为一个新对象
,这中改变数据类型的方式,间接的绕过了拷贝对象引用的过程,也就谈不上影响原始数据。
限制:
这种方式成立的根本就是保证数据在“中转”时的完整性,而JSON.stringify()
将值转换为相应的JSON格式
时也有缺陷:
所以当我们拷贝函数、undefined等stringify
转换有问题的数据时,就会出错,我们在实际开发中也要结合实际情况使用。
举一反三:
既然是通过改变数据类型来绕过拷贝引用这一过程,那么单纯的数组深拷贝是不是可以通过现有的几个API来实现呢?
var arr = [1,2,3];
var newArr = arr.toString().split(',').map(item => Number(item))
newArr[0] = 100;
console.log(arr); // [1,2,3]
console.log(newArr); // [100,2,3]
注意,此时仅能对包含纯数字的数组进行深拷贝,因为:
但我愿称它为纯数字数组深拷贝!
有的人会认为Object.assign()
,可以做到深拷贝,我们来看一下
var obj = {
a: 1, b: {
c: 2 } }
var newObj = Object.assign({}, obj)
newObj.a = 100;
newObj.b.c = 200;
console.log(obj); // {a: 1, b: { c: 200 } }
console.log(newObj) // {a: 100, b: { c: 200 } }
神奇,第一层属性没有改变,但第二层却同步改变了,这是为什么呢?
因为 Object.assign()拷贝的是(可枚举)属性值。
假如源值是一个对象的引用,它仅仅会复制其引用值。MDN传送门
既然现有的方法无法实现深拷贝,不妨我们自己来实现一个吧~
我们只需要将所有属性即其嵌套属性原封不动的复制给新变量一份即可,抛开现有的方法,我们应该怎么做呢?
var shallowCopy = function(obj) {
if (typeof obj !== 'object') return;
// 根据obj的类型判断是新建一个数组还是对象
var newObj = obj instanceof Array ? [] : {};
// 遍历obj,并且判断是obj的属性才拷贝
for (var key in obj) {
if (obj.hasOwnProperty(key)) {
newObj[key] = obj[key];
}
}
return newObj;
}
我们只需要将所有属性的引用拷贝一份即可~
相信大家在实现深拷贝的时候都会想到递归,同样是判断属性值,但如果当前类型为object
则证明需要继续递归,直到最后
var deepCopy = function(obj) {
if (typeof obj !== 'object') return;
var newObj = obj instanceof Array ? [] : {};
for (var key in obj) {
if (obj.hasOwnProperty(key)) {
newObj[key] = typeof obj[key] === 'object' ? deepCopy(obj[key]) : obj[key];
}
}
return newObj;
}
我们用白话来解释一下deepCopy
都做了什么
const obj = [1, {
a: 1, b: {
name: '余光'} } ];
const resObj = deepCopy(obj);
obj
,创建 第一个newObj
[]
0
(for in
以任意顺序遍历,我们假定按正常循序遍历)1
obj[1]
另外请注意递归的方式虽然可以深拷贝,但是在性能上肯定不如浅拷贝,大家还是需要结合实际情况来选择。
前端专项进阶系列的第五篇文章
,希望它能对大家有所帮助,如果大家有什么建议,可以在评论区留言,能帮到自己和大家就是我最大的动力!
JavaScript专题系列:
另外推荐大家阅读一下我的《前端内功进阶系列》
关于我
其他沉淀
《专项系列》以及《内功系列》github传送门,点个star鼓励一下吧~
————————————————
版权声明:本文为CSDN博主「余光、」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:
JavaScript专题之深浅拷贝(系列五)_余光的博客-CSDN博客blog.csdn.net