zl程序教程

您现在的位置是:首页 >  后端

当前栏目

在考虑闭包的情况下JS变量存储在栈与堆的区分

JS存储变量 情况 闭包 考虑 区分
2023-06-13 09:14:10 时间

变量存储在闭包中的问题

按照常理来说栈中数据在函数执行结束后就会被销毁,那么 JavaScript 中函数闭包该如何实现,先简单来个闭包:

function count () {
    let num = -1;
    return function () {
        num++;
        return num;
    }
}

let numCount = count();
numCount();
// 0
numCount();
// 1

按照结论,num 变量在调用 count 函数时创建,在 return 时从栈中弹出。 既然是这样的逻辑,那么调用 numCount 函数如何得出 0 呢?num 在函数 return 时已经在内存中被销毁了啊! 因此,在本例中 JavaScript 的基础类型并不保存在栈中,而应该保存在堆中,供 numCount 函数使用。

抛开栈,只在堆中存储数据

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

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

由于 Scope 对象是存储在堆中,因此返回的 log 函数完全可以拥有 Scope 对象 的访问。下图是该段代码在 Chrome 中的执行效果:

例子中 JavaScript 的变量并没有存在栈中,而是在堆里,用一个特殊的对象(Scopes)保存。

变量到底是如何在 JavaScript 中存储的

JavaScript 中,变量分为三种类型:

  1. 局部变量
  2. 被捕获变量
  3. 全局变量

局部变量

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

function zxxFn () {
    let zxx1 = 1
    var zxx2 = 'str'
    const zxx3 = true
    let zxx4 = {name: 'zxx'}
    return
}

被捕获变量

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

function zxxFn () {
    let zxx1 = 1
    var zxx2 = 'str'
    const zxx3 = true
    let zxx4 = {name: 'zxx'}
    return function () {
        console.log(zxx1, zxx2, zxx3, zxx4)
    }
}
let zxx = zxxFn()
console.dir(zxx)
function zxxFn () {
    let zxx1 = 1
    var zxx2 = 'str'
    const zxx3 = true
    let zxx4 = {name: 'zxx'}
    return class {
        constructor() {
            console.log(zxx1, zxx2, zxx3, zxx4)
        }
    }
}
let zxx = zxxFn()
console.dir(zxx)

复制代码到 Chrome 即可查看输出对象下的 [[Scopes]] 下有对应的 Scope

全局变量

全局变量就是 global,在 浏览器上为 windownode 里为 global。全局变量会被默认添加到函数作用域链的最低端,也就是上述函数中 [[Scopes]] 中的最后一个。

全局变量需要特别注意一点:varlet/const 的区别。

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

var name = 'zxx';

// 与下述代码一致
windows.name = 'zxx';

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

var pwd = 123

变量赋值

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

赋值为常量

何为常量?常量就是一声明就可以确定的值,比如 1"string"true{a: 1},都是常量

假设现在有如下代码:

let foo = 1

JavaScript 声明了一个变量 foo,且让它的值为 1,内存中就会发生如下变化

如果现在又声明了一个 bar 变量:

let bar = 2

那么内存中就会变成这样:

对于对象类型

let obj = {
    foo: 1,
    bar: 2
}

内存模型如下:

通过该图,我们就可以知道,其实 obj 指向的内存地址保存的也是一个地址值,那好,如果我们让 obj.foo = 'foo' 其实修改的是 0x1021 所在的内存区域,但 obj 指向的内存地址不会发生改变,因此,对象是常量!

赋值为变量

何为变量?在上述过程中的 foobarobj,都是变量,变量代表一种引用关系,其本身的值并不确定。

那么如果我将一个变量的值赋值给另一变量,会发生什么?

let x = foo

如上图所示,仅仅是将 x 引用到与 foo 一样的地址值而已,并不会使用新的内存空间。

OK 赋值到此为止,接下来是修改。

变量修改

与变量赋值一样,变量的修改也需要根据 = 号右边变量的类型分为两种方式:

修改为常量

foo = 'foo'

如上图所示,内存中保存了 'foo' 并将 foo 的引用地址修改为 0x0204

修改为变量

foo = bar

如上图所示,仅仅是将 foo 引用的地址修改了而已。

const 的工作机制

constES6 新出的变量声明的一种方式,被 const 修饰的变量不能改变。

其实对应到 JavaScript 的变量储存图中,就是变量所指向的内存地址不能发生变化。也就是那个箭头不能有改变。

比如说以下代码:

const foo = 'foo';
foo = 'bar'; // Error

如上图的关系图所示,foo 不能引用到别的地址值。那好现在是否能解决你对下面代码的困惑:

const obj = {
    foo: 1,
    bar: 2
};
obj.foo = 2;

obj 所引用的地址并没有发生变化,发生变的部分为另一区域。如下图所示

对象的修改

OK 进入一个面试时极度容易问到的问题:

let obj1 = {
    foo: 'foo',
    bar: 'bar'
}

let obj2 = obj1;
let obj3 = {
    foo: 'foo',
    bar: 'bar'
}

console.log(obj1 === obj2);
console.log(obj1 === obj3);

obj2.foo = 'foofoo';

console.log(obj1.foo === 'foofoo');

请依次说出 console 的结果。

我们不讨论结果,先看看内存中的结构。所以结果为 true false true