大家好,我是辉夜真是太可爱啦。这是我写的一个一文搞懂JS系列专题。文章清晰易懂,会将会将关联的只是串联在一起,形成自己独立的知识脉络,整个合集读完相信你也一定会有所收获。写作不易,希望您能给我点个赞!
合集地址:一文搞懂JS系列专题
概览
- 食用时间: 6-12分钟
- 难度: 简单,别跑,看完再走
先从一道题说起
let name = 'jack';
let nameCopy = name;
nameCopy = 'bob';
console.log(name); //jack
这道题,我相信大家应该都会,答案也很简单,就是 jack 。
那么,接下来,我们换种方式:
let obj = {
age:24
};
var objCopy = obj;
objCopy.age = 15;
console.log(obj.age); //15
你就会发现,你明明修改的是 objCopy
,为什么 obj
的值也被修改了。
这是因为 objCopy = obj
在赋值的同时,赋予的并不是对象的值,而是对象的引用。可能大家就更懵了,什么是引用,接下来,让我们从栈内存和堆内存开始说起。
栈内存与堆内存
JS数据类型
在讲栈内存与堆内存之前,大家应该都知道JS分为两种数据类型:
-
基本数据类型
String , Number , Boolean , null , undefined , Symbol (大小固定,体积轻量,相对简单)
-
引用数据类型
Object , Array , Function (大小不一定,占用空间较大,相对复杂)
内存存储机制
var a=true; //布尔型,基本数据类型
var b='jack'; //字符型,基本数据类型
var c=18; //数值型,基本数据类型
var d={name:'jack'}; //对象,引用数据类型
var d=[0,1,2,3,4,5]; //数组,引用数据类型
正是因为数据类型的不同,所以他们的存放方式也不同,就和现实生活中穷人和富人的住所完全不一样(扯远了)。我们先来看一张图:
可以看到, a
, b
, c
都是基本数据类型, d
和 e
都是引用数据类型,他们在存放方式上有着本质性的区别,基本数据类型的值是存放在栈内存中的,而引用数据类型的值是存放在堆内存中的,栈内存中仅仅存放着它在栈内存中的引用(即它在堆内存中的地址),就和它的名字一样,引用数据类型
内存访问机制
上面讲的是存储,接下来说一下变量的访问,基本数据类型可以直接从栈内存中访问变量的值,而引用数据类型要先从栈内存中找到它对应的引用地址,再拿着这个引用地址,去堆内存中查找,才能拿到变量的值。
所以,当我们在对引用类型赋值的时候,复制的是该对象的引用地址,所以,在执行 objCopy = obj;
的时候,将 obj
的引用地址复制给了 objCopy
,所以,这两个对象实际指向的是同一个对象,即改变 objCopy
的同时也改变了 obj
的值,仅仅复制了对象的引用,并没有开辟新的内存。(只有引用类型才会出现共享引用地址的情况)
如果我们不希望这种情况发生,我们就可以使用浅拷贝。
浅拷贝
Object.assign()
Object.assign
会拷贝所有的属性值到新的对象中,当子属性值是对象的话,拷贝的是仍然是引用,所以并不是深拷贝。
let obj = {
age:'24'
}
let objCopy = Object.assign({},obj);
objCopy.age=15
console.log(obj.age); // 24
... 扩展运算符
扩展运算符也同样可以达到浅拷贝的作用:
let obj = {
age:'24'
}
let objCopy = {...obj};
objCopy.age=15
console.log(obj.age); // 24
浅拷贝的局限性
浅拷贝只能解决第一层的问题,当子属性是对象的时候,那么,就又回到上面的问题了,两者享有共同的引用地址。我们可以通过一个例子来了解下:
let obj = {
age:'24',
children:{
age:5
}
}
let objCopy = {...obj};
objCopy.children.age=15
console.log(obj.children.age); // 15
所以,当对象的子属性中也有对象的时候,浅拷贝就不适用了,这时候,就需要使用深拷贝才行。
深拷贝
JSON.parse(JSON.stringify())
这是实现深拷贝中,最简单也是最常用的一种方式:
let obj = {
age:'24',
children:{
age:5
}
}
let objCopy = JSON.parse(JSON.stringify(obj));
objCopy.children.age=15
console.log(obj.children.age); // 5
可以发现,在经过 JSON.parse(JSON.stringify(obj))
转换了以后,实现了深拷贝,深拷贝开辟了新的堆内存地址,并且将对象的引用指向了新开辟的内存地址,和前面复制的对象完全独立,自立根生,拷贝地很深,学功夫学到家,自立门户的感觉。
当然,它也有自己的局限性:
-
会忽略
undefined
-
会忽略
symbol
-
不能序列化函数
-
不能解决循环引用的对象
关于上面三点,相信大家也很好理解:
let obj = {
name: 'jack',
age: undefined,
sex: Symbol('male'),
play: function() {}
}
let objCopy = JSON.parse(JSON.stringify(obj))
console.log(objCopy) // {name: 'jack'}
关于循环引用可以看这个例子:
不过一般来说,不会有那么复杂的循环引用,循环引用的结果,也很容易造成内存泄漏。
总的来说, JSON.parse(JSON.stringify())
就已经足够解决大部分的应用场景了,而且主要是代码少,很方便。
如果非要实现很严谨的深拷贝,可以使用 lodash 的深拷贝函数