JS中的栈内存、堆内存

浅谈JS中的堆栈

写在正文之前:我个人写博客的灵感大部分源于我最近学到了什么新的或者好玩的东西,写下来加深记忆并且和大家分享,还有就是源于某些偏原理的东西,我喜欢去和大家分享和讨论某些东西实现的原理,而不是它具体的实现,实现的方法太多了,原理虽有区别,但基本上大同小异。

引言:我们都熟知并且常用JS变量的声明以及初始化(赋值),比如一行极其简单的代码var str = '我是字符串',那么这行代码执行的时候发生了什么呢?再比如var obj = {name: 'reslicma'}又发生了什么?他们一样吗?请看正文。

JS中变量的类型

我们先行讨论JS中变量的类型,因为JS中变量的具体存储方式是取决于这个变量的类型的。JS中的变量共有两大类:基本数据类型引用数据类型,我们下文说基本型就是基本数据类型,引用型就是引用数据类型。

基本数据类型(简单数据类型)

JS中的基本型共有五种:string,number,Boolean,undefined,null。分别对应:字符串类型,数字类型,布尔类型,undefined(变量声明未初始化),null(空对象或理解为空指针)。

引用数据类型

JS中的引用型:Array,Function,Object。但是实际上就是一种:Object型,没错,就是对象,毕竟Array,Function也是对象。JS一切皆对象这句话并不为过······

栈内存和堆内存

介绍完了基本型和引用型就可以真正的进入正题了。我们知道声明一个变量并且给它赋值这样的操作对于这两种类型而言没什么区别,但是对这两种类型的具体的操作却大不相同。 在JS中,栈内存用于存储基本型的变量值,堆内存用于存储引用型的值。这是为什么呢?因为JS这门语言和其他语言有一个不同之处:不允许直接访问内存的位置,也就是说不能直接操作对象的内存空间,操作的是对象的引用而已,那么引用可以理解为是一个指针,是一个具体的堆内存的地址。我写下这几行代码,并且附上一张图,让我们来详细的看一下:

var str = `我是字符串`,
    num = 1,
    bl = true,
    nu = null,
    un = undefined,
    obj = {
        name: 'reslicma'
    }
复制代码

在内存中就发生了如下图这样的事情:

堆栈内存图
我们接下来详细的分析它: 首先,我定义的前五个变量都是基本型,那么他们都是存储在 栈内存当中,并且他们存储的就是 值本身,所以说访问基本型变量就能访问得到 。而obj这个变量是个引用型,所以它在栈内存中只保存了一个指针(或者理解为一个地址),比如上图中的这个地址,那么这个地址指向了堆内存中的一块内存空间,这个空间才是真正存储了这个obj对象的内存空间。

理解栈内存和堆内存

我们来说一下栈内存和堆内存具体的区别和联系。

栈内存就像一个线性的、规则的、大小基本固定的、有序的排列起来的一块块内存空间,就像我上图画的那样,每个单元大小固定,规则有序的排列下来,就是栈。所以,在定义一个基本型变量的时候,发生的事情如下:向栈内存申请(注意是申请)一块空间,然后把你声明的变量名和这个变量的具体的值本身压入这个申请好的小空间内。

堆内存就像一个不规则的、大小不固定的、无序的一块块内存空间,像上图中我画的堆内存图中,大小不固定,并且每一块堆内存都有一个自己的地址(指针),用来操作它们。这个地址很有意思,在你定义一个引用型变量的时候,向堆内存申请一块空间,用于存储这个具体的引用型的值(对象),同时JS会随机分配给这块堆内存的小空间一个地址,然后,把这个变量名和这个地址压入申请好的栈空间内。这里就是我详细说明的地方,其实上图就很形象的表达了这个流程。

栈内存和堆内存的优缺点

那么为什么JS要这样区分栈内存和堆内存呢?在JS中,这些基本型变量大小固定,并且操作容易简单,所以把它们放入栈中存储。引用型变量大小不固定,所以把它们分配给堆中,让他们申请空间的时候自己确定大小,这样把它们分开存储能够使得程序运行起来占用的内存最小。

栈内存由于它的特点,所以它的系统效率较高,堆内存需要分配空间和地址,还要把地址存到栈中,所以效率低于栈。

栈内存和堆内存的垃圾回收

我们知道JS是有垃圾回收制的(不详细说),栈内存中基本型一般在它的当前执行环境结束就会被销毁被垃圾回收制回收,而引用类型不会,因为不确定其他的地方是不是还有一些对它的引用,所以引用型只有在所有对它的引用都结束的时候才会被回收掉。

加深理解

注意:在JS中访问变量时,是把变量名作为索引来寻找值的,无论是基本型还是引用型。也就是说,访问变量的过程就是:通过变量名找到栈内存中存储的具体的值,如果是基本型,直接就返回值,如果是引用型,返回一个指向堆内存的地址

案例1:基本型的复制:

var num1 = 1
var num2 = num1
// 修改num1的值
num1 = 2
console.log(num2) // 还是1,不会改变
复制代码

解析具体过程:首先在栈内存中压入一个变量名为num1、值为1的一个变量。然后,第二行代码:赋值操作,先执行赋值运算符右边的式子,所以通过变量名找到了num1的值1,然后把这个值1返回并且赋值给了num2这个变量,所以栈内存中就又压入了一个变量名为num2、值为1的变量,这两个变量的值1相等但不是同一个。所以,改变num1的值就只改变了num1栈内存中的值,对num2没有任何影响。图解:

基本型复制图
案例2:引用型的复制:

var obj1 = {name: 'reslicma'}
var obj2 = obj1
obj1.name = '我被修改了'
console.log(obj2.name) // 我被修改了
复制代码

我们前面已经说过了,无论是基本型还是引用型都是按变量名访问栈内存中的值,所以,第二句代码执行的时候,就相当于把通过obj1找到的那个内存地址赋值给了obj2这个变量,所以这两个变量在栈内存中都是存储的同一个地址、指针,指向的是同一块堆内存中的空间,我修改了obj1的内容,那么由于obj2也是指向这个内容,所以obj2的内容也会随之改变。

内存图解:

基本型复制图
基本型复制图

猜你喜欢

转载自juejin.im/post/5c7157f3e51d4526643fac46