如果你去百度这个问题“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
如果按照 上面的结论 i
在 sum
函数调用时创建,在函数调用结束 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
对象是存储在堆中,因此返回的 scopeFun
函数完全可以拥有 Scope
对象 的访问。下图是该段代码在 Chrome 中的执行效果:
上面的结果也可以反应出 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
中执行的结果
被捕获变量
被捕获变量就是局部变量的反面:在函数中声明,但在函数返回后仍有未执行作用域(函数或是类)使用到该变量,那么该变量就是被捕获变量。下面代码中的 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;
,内存中就会变成这样:
如果我们在声明一个对象呢?
let obj = {
a: 1,
b: 2
}
其实 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 obj = {
a: 1,
b: 2
};
obj.a = 2;
obj 所引用的地址并没有发生变化,发生的变化是obj对应的堆中,如下图
好了,讲到这里应该已经很清楚题目中的问题了
在 JavaScript 中变量并非完完全全的存在在栈中,早期的 JavaScript 编译器甚至把所有的变量都存在一个名为闭包的对象中,JavaScript 是一门以函数为基础的语言,其中的函数变化无穷,因此使用栈并不能解决语言方面的问题,而堆则方便存储各种类型得到变量。想可以帮助你理解 JavaScript的变量存储机制。写的不对的还请批评指正~?
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!