前情提要
不知道大家有没有在项目遇到这种情况:
我们定义了一个数组A,然后我们可能要对这个数组进行数据清洗,但是为了后面可能还会用到数组A,所以我们就另外拷贝出来一份数组B处理,不动原数据。但是清洗之后我们经过打印会发现之前留存的数据A也跟着变化了。
一时间让人头皮发麻,头大。
这是为什么呢?
还能为什么?
这是因为涉及到了js引擎中对于数据类型的存储问题了,涉及到了堆区和栈区以及存放内容的问题。简短点讲就是js中深浅拷贝的问题。
下面就来讲讲这个问题吧:
什么是深浅拷贝?
js中数据类型分为:
相信大家已经很熟知js中数据类型的分类了,没错,主要分为两大类基本数据和引用类型数据。
- 基本数据类型:
- Number, Null, Undefined, String, Boolean, Symbol (es6), BigInt (es11)
- 一般存放在内存中的栈区,存取速度快,存放量小。
- 引用数据类型(即object类型):
- Array, Function, Date等
- 一般存放在内存中的堆区,存取速度慢,存放量大,其引用指针存于栈区,并指向引用本身。
深浅拷贝是相对于引用类型而言的:
为什么说深浅拷贝是相对于引用类型而言的呢?
- 因为对于基本数据类型而言,其值都是存储在栈区的,这个上面有介绍。而直接赋值拷贝的方式就是直接在栈区开辟一个新空间,存储的就是目标对象在栈区的内容,所以,基本数据类型的拷贝,根本不存在什么深浅拷贝的问题。所有的基本数据类型的拷贝都是深的不能再深的拷贝了。
- 而对于引用数据类型就不一样了,此时引用类型真正的数据是藏在幕后的堆区,堆区空间大,适合存储数据复杂的引用数据类型。所以引用数据类型的实际存储地都在堆区。
- 但是我们在写代码时,是没有办法直接获取堆区的内容,而是经过中间层:栈区。我们会将数据存在堆区,地址存在栈区,然后我们在获取引用类型的数据的过程就是从栈区找到地址,再根据地址到堆区找到数据。
- 而我们直接赋值等操作,是相当于为新变量在栈区开辟了空间,存储的是从赋值过来的堆区的地址。
- 也就是说虽然两个变量在栈区具有不同的空间,但是却有相同的堆区的内存地址。于是改变其中一个属性值,由于两个变量指向的堆区地址一样,因此获取的数据也都一样,所以改变其中一个值,另一个值也就改变了,这就是经典的浅拷贝过程。
- 而更多的时候我们是不希望这样的,我们希望改变其中一个变量的值另一个变量并不跟着变化,这就是经典的深拷贝。
- 下面将让我们来看看怎么进行深浅拷贝吧👇🏻
深浅拷贝方法👇🏻
- 如果数组内部的元素均为基本数据类型,那数组的slice(),concat(),Object的assign()以及es6中的扩展运算符均属于深拷贝。
- 如果数组内部的元素存在引用数据类型,那数组的slice(),concat(),Object的assign()以及es6的扩展运算符只会深拷贝其中的基本数据类型,而引用数据类型只是浅拷贝,当其中一个的属性值改变,另一个也会改变。
- 直接赋值属于浅拷贝。
- JSON.parse(JSON.stringify(obj))属于深拷贝。但是需要注意:
- 如果对象里有时间对象,则将对象进行序列化之后得到的结果,时间将只是字符串样式,而不是时间对象
let obj = { a: 1, time: new Date() };
let obj2 = JSON.parse(JSON.stringify(obj));
console.log(obj2); // {a: 1, time: '2022-02-10T15:39:52.135Z'}
typeof obj.time // 'object'
typeof obj2.time // 'string'
- 如果对象里有RegExp, Error对象,则序列化的结果将只得到空数组
let obj = { re: new RegExp('\\w+'), error: new Error('error') };
let obj2 = JSON.parse(JSON.stringify(obj));
console.log(obj2); // { re: {}, error: {} }
- 如果对象里有函数,undefind,则序列化得到的结果会把函数或者undefined的键值对丢失
let obj = { fn: function() { console.log('fn') }, a: undefined, c: 1 };
let obj2 = JSON.parse(JSON.stringify(obj));
console.log(obj2); // { c: 1 }
- 如果对象里有NaN,Infinity和-Infinity,则序列化之后的结果会变成null
let obj = { a: NaN, b: Infinity, c: -Infinity }
let obj2 = JSON.parse(JSON.stringify(obj));
console.log(obj2); // {a: null, b: null, c: null}
- 如果对象里有对象是构造函数生成的,则序列化之后constructor强行转为Object,无法获取自己原型链上的内容,只能获取Object原型内容
let Person = function() {
this.name = '张三';
}
Person.prototype.speek = function() {
console.log(this.name);
}
let OtherPerson = function() {
Person.call(this, arguments**);**
this.name = '李四';
}
let realPeople = new OtherPerson();
console.log(JSON.parse(JSON.stringify(realPeople))); // {name: '李四'}
- 如果对象里循环引用的情况也无法正确实现深拷贝
- 递归实现深拷贝
let deepClone = function(obj) {
// 首先如果传过来的参数不是对象类型就直接返回,那说已经是基本数据类型了,直接返回即可。
if (typeof obj !== 'object') return obj;
let newObj = obj;
// 这一步主要是利用instanceof判断是否是某个构造方法的实例来获取参数具体是属于引用数据类型的哪一种,最后通过对象原型上得toString方法以及正则方法来清洗出现而意见的字符,以方便进行判断。
let type = obj instanceof Element ? 'element' : Object.prototype.toString.call(obj).split(' ')[1].replace(/\]/g, '').toLowerCase();
// 如果是数组就创建新的数组,并将每一个元素再递归再push给新建的数组
if (type === 'arry') {
newObj = [];
for (let i = 0; i < obj.length; i++) {
newObj.push(deepClone(obj[i]))
}
};
// 如果是对象,那就遍历属性,再递归,再赋值给新对象的属性
if (type === 'object') {
newObj = {};
for (let key in obj) {
obj[key] = deepClone(obj[key])
}
};
// 最后返回清洗之后的数据
return newObj;
}
- 使用lodash工具库中的_.cloneDeep()。属于第三方库,不过lodash库还是不小的,如果仅仅是使用其中的一个cloneDeep,那我还是建议自己按照上面的递归,自己在项目里封装一个工具函数类吧。