js基础: js变量到底是存储在栈还是堆上呢?

2,193 阅读9分钟

如果你去百度这个问题“JavaScript的变量存储机制”,可以看到很多的回答都是:对于原始类型,数据本身是存在栈内,对于对象类型,在栈中存的只是一个堆内地址的引用

那么真的是如此吗? 回答中提到的 是怎么样的存储结构?下面我们一一来看

栈和堆

想要搞清楚 JavaScript的变量存储机制 ,首先必须要弄明白 ,先来看下 的概念和特点。

可以把堆认为是一个很大的内存存储空间,你可以在里面存储任何类型数据。但是这个空间是私有的,操作系统不会管在里面存储了什么,也不会主动的去清理里面的内容,因此在C语言中需要程序员手动进行内存管理,以免出现内存泄漏,进而影响性能。

但是在一些高级语言 如JAVA会有 垃圾回收(GC) 的概念,用于协助程序管理内存空间,自动清理堆中不再使用的数据。

在栈中存储不了的数据比如对象就会被存储在堆中,在栈中呢是保留了对象在堆中的地址,也就是对象的引用。提到了栈那么接下来我们看下什么是栈?

栈是内存中一块用于存储局部变量和函数参数的线性结构,遵循着先进后出的原则。数据只能顺序的入栈,顺序的出栈。当然,栈只是内存中一片连续区域一种形式化的描述,数据入栈和出栈的操作仅仅是栈指针在内存地址上的上下移动而已。如下图所示: 栈

如图所示,栈指针开始指向内存中 0x001 的位置,add 函数开始调用,由于声明了两个变量,往栈中存放了两个数值,栈指针也对应开始移动,当 add 函数调用结束时,仅仅是把栈指针往下移动而已,并不是真正的数据弹出,数据还在,只不过下次赋值时会被覆盖。

但需要注意的是:内存中栈区的数据,在函数调用结束后,就会自动的出栈,不需要程序进行操作,操作系统会自动回收,也就是:栈中的变量在函数调用结束后,就会消失。 这也正是栈的特点:无需手动管理、轻量、函数调时创建,调用结束则消失

JS中的变量存储机制与闭包

弄清楚了 存储数据的方式和特点,那么对于JS中的变量存储机制的结论: 对于原始类型,数据本身是存在栈内,对于对象类型,在栈中存的只是一个堆内地址的引用 应该是符合逻辑的,但是,我们都知道js中存在闭包的概念,所谓的闭合包指:

一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure)。也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。(引用自MDN)

既然栈中数据在函数执行结束后就会被销毁,那么 JavaScript 中函数闭包该如何实现的呢?来看下如下代码

function sum () {
    let i = 0;
    return function () {
        i++;
        return i;
    }
}

let sumFun = sum();
sumFun(); // 1
sumFun(); // 2

如果按照 上面的结论 isum 函数调用时创建,在函数调用结束 retun时就从栈中弹出,变量 i 会被销毁。那么为啥 调用 sumFun 函数时会输出结果 1 呢?

因此 在上面的例子中也就是存在闭包的情况,变量并没有保存在栈中,而应该保存在堆中,供 sumFun 函数使用。 要弄明白这个问题,首先我们要好好了解下JavaScript 中不同的变量类型,不同类型的变量存储机制是不同的。

JavaScript 变量存储

在讲解三种类型前,我们先通过一个例子来看下 JavaScript 变量存储

function testScope () {
    let num = 1;
    let string = 'string';
    let bool = true;
    let obj = {
        attr1: 2,
        attr2: 'string',
        attr3: false,
        attr4: 'attr4'
    }
    return function scopeFun() {
        console.log(num, string, bool, obj);
    }
}

随着 testScope 的调用,为了保证变量不被销毁,在堆中先生成一个对象就叫 Scope 吧,把变量作为 Scope 的属性给存起来。堆中的数据结构大致如下所示: Scope

这样就能解决闭包的问题了, 由于 Scope 对象是存储在堆中,因此返回的 scopeFun 函数完全可以拥有 Scope 对象 的访问。下图是该段代码在 Chrome 中的执行效果: scopeFun

上面的结果也可以反应出 JavaScript 的变量并没有存在栈中,而是在堆里,用一个特殊的对象(Scope)保存。

那么在 JavaScript 变量到底是如何进程存储的?这和变量的类型直接挂钩,接下来就看下在 JavaScript 中变量的类型。

局部变量、全局变量、被捕获变量

局部变量

局部变量:在函数中声明,且在函数返回后不会被其他作用域所使用的对象。下面代码中的 local* 都是局部变量。

function testLocal () {
    let local1 = 1;
    var local2 = 'str';
    const local3 = true;
    let local4 = {a: 1};
}

看下在 Chrome 中执行的结果 局部变量

全局变量

全局变量 ,在浏览器上为 window ,在 node 里为 global。全局变量会被默认添加到函数作用域链的最低端,也就是上述函数中 [[Scopes]] 中的最后一个,可以看下上面局部变量例子中 Scopes的最后一个。

全局变量需要特别注意一点:var 和 let/const 的区别

var

全局的 var 变量其实仅仅是为 global 对象添加了一条属性。

var testVar = 1;
// 等同于
windows.testVar = 1;

let / const

全局的 let/const 变量不会修改 window 对象,而是将变量的声明放在了一个特殊的对象下(与 Scope 类似)。

let testLet = 1;
console.dir(() => {})

如下在 Chrome 中执行的结果 局部变量1

被捕获变量

被捕获变量就是局部变量的反面:在函数中声明,但在函数返回后仍有未执行作用域(函数或是类)使用到该变量,那么该变量就是被捕获变量。下面代码中的 catch* 都是被捕获变量。

function testCatch1 () {
    let catch1 = 1;
    var catch2 = 'str';
    const catch3 = true;
    let catch4 = {a: 1};
    return function () {
        console.log(catch1, catch2, catch3, catch4)
    }
}

function testCatch2 () {
    let catch1 = 1;
    let catch2 = 'str';
    let catch3 = true;
    var catch4 = {a: 1};
    return class {
        constructor(){
            console.log(catch1, catch2, catch3, catch4)
        }
    }
}
console.dir(testCatch1())
console.dir(testCatch2())

看下在 Chrome 中执行的结果 被捕获变量

变量的存储

讲到这里应该都清楚了:除了局部变量,其他的全都存在堆中。根据变量的数据类型,分为以下两种情况:

  • 如果是基础类型,那栈中存的是数据本身。
  • 如果是对象类型,那栈中存的是堆中对象的引用。

那么 JavaScript 解析器如何判断一个变量是局部变量呢?判断出是否被内部函数引用即可,如果 JavaScript 解析器并没有判断,则存储在堆上。在来执行下如下代码

function test () {
    let num1 = 1;
    var num2 = 2;
   return function() {
       console.log(num1)
   }
}
console.dir(test())

变量的存储 可以看到 Scopes 中只有变量 num1

变量的赋值

不论变量是存在栈内,还是存在堆里(反正都是在内存里),其结构和存值方式是差不多的,都有如下的结构: 变量的赋值

那好现在我们来看看赋值,根据 = 号右边变量的类型分为两种方式:

赋值为常量

常量 就是一声明就可以确定的值,比如 1、"string"、true、{a: 1},都是常量,这些值一旦声明就不可改变,有些人可能会犟,对象类型的这么可能是常量,它可以改变啊,这个问题先留着,等下在解释。

如下代码: let foo = 1;,JavaScript 声明了一个变量 foo,且让它的值为 1 赋值为常量

如果在 声明了一个 bar 变量:let bar = 2;,内存中就会变成这样: 赋值为常量1

如果我们在声明一个对象呢?

let obj = {
    a: 1,
    b: 2
}

赋值为常量1

其实 obj 指向的内存地址保存的也是一个地址值,那好,如果让 obj.foo = 3 其实修改的是 0x1021 所在的内存区域,但 obj 指向的内存地址不会发生改变。

赋值为变量

在上述过程中的 foo、bar、obj,都是变量,变量代表一种引用关系,其本身的值并不确定。

如果我将一个变量的值赋值给另一变量,如:let x = foo; 则仅仅是将 x 引用到与 foo 一样的地址值而已,并不会使用新的内存空间。如下图所示 赋值为变量

const机制

const 为 ES6 新出的变量声明的一种方式,被 const 修饰的变量不能改变。其实对应到 JavaScript 的变量储存图中,就是变量所指向的内存地址不能发生变化。也就是那个箭头不能有改变。

例如执行如下代码

const foo = 'lxm';
foo = 'lxm love js'; // Uncaught TypeError: Assignment to constant variable.

const机制

如果定义一个const 类型对象并修改 对象的属性呢?

const obj = {
    a: 1,
    b: 2
};
obj.a = 2;

obj 所引用的地址并没有发生变化,发生的变化是obj对应的堆中,如下图 const机制

好了,讲到这里应该已经很清楚题目中的问题了

在 JavaScript 中变量并非完完全全的存在在栈中,早期的 JavaScript 编译器甚至把所有的变量都存在一个名为闭包的对象中,JavaScript 是一门以函数为基础的语言,其中的函数变化无穷,因此使用栈并不能解决语言方面的问题,而堆则方便存储各种类型得到变量。想可以帮助你理解 JavaScript的变量存储机制。写的不对的还请批评指正~😊