一文搞懂JS系列(二)之JS内存生命周期,栈内存与堆内存,深浅拷贝

2,273 阅读5分钟

大家好,我是辉夜真是太可爱啦。这是我写的一个一文搞懂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 都是基本数据类型, de 都是引用数据类型,他们在存放方式上有着本质性的区别,基本数据类型的值是存放在栈内存中的,而引用数据类型的值是存放在堆内存中的,栈内存中仅仅存放着它在栈内存中的引用(即它在堆内存中的地址),就和它的名字一样,引用数据类型

内存访问机制

上面讲的是存储,接下来说一下变量的访问,基本数据类型可以直接从栈内存中访问变量的值,而引用数据类型要先从栈内存中找到它对应的引用地址,再拿着这个引用地址,去堆内存中查找,才能拿到变量的值。

所以,当我们在对引用类型赋值的时候,复制的是该对象的引用地址,所以,在执行 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'}

关于循环引用可以看这个例子:

image.png

不过一般来说,不会有那么复杂的循环引用,循环引用的结果,也很容易造成内存泄漏

总的来说, JSON.parse(JSON.stringify()) 就已经足够解决大部分的应用场景了,而且主要是代码少,很方便。

如果非要实现很严谨的深拷贝,可以使用 lodash 的深拷贝函数

系列目录