此次的 why what or how
主题:JavaScript
变量存储。javascript
不管那门语言,变量是组成一切的基础,一个数字是一个变量,一个对象也是一个变量,在 JavaScript
中甚至连一个函数都是一个变量。html
那么如此重要的变量,在 JavaScript
中到底是如何进行存储的?java
栈(
Stack
)又名堆栈,它是一种运算受限的线性表。限定仅在表尾进行插入和删除操做的线性表。node
在百度上搜索 JavaScript
变量存储,能看到不少文章,无外乎一个结论:程序员
对于原始类型,数据自己是存在栈内,对于对象类型,在栈中存的只是一个堆内地址的引用。面试
可是,我忽然想到一个问题:若是说原始类型存在在栈中,那么 JavaScript
中的闭包是如何实现的?windows
固然想要深究这个问题,有必要先把栈(Stack
)和堆(Heap
)给说说清楚。浏览器
那好,先说说栈。数据结构
栈是内存中一块用于存储局部变量和函数参数的线性结构,遵循着先进后出的原则。数据只能顺序的入栈,顺序的出栈。固然,栈只是内存中一片连续区域一种形式化的描述,数据入栈和出栈的操做仅仅是栈指针在内存地址上的上下移动而已。以下图所示(以 C 语言为例):闭包
如图所示,栈指针刚开始指向内存中 0x001
的位置,接着 sum
函数开始调用,因为声明了两个变量,往栈中存放了两个数值,栈指针也对应开始移动,当 sum
函数调用结束时,仅仅是把栈指针往下移动而已,并非真正的数据弹出,数据还在,只不过下次赋值时会被覆盖。
挺简单的不是么,但须要注明一点的是:内存中栈区的数据,在函数调用结束后,就会自动的出栈,不须要程序进行操做,操做系统会自动执行,换句话说:栈中的变量在函数调用结束后,就会消失。
所以栈的特色:轻量,不须要手动管理,函数调时建立,调用结束则消失。
堆能够简单的认为是一大块内存空间,就像一个篮子,你往里面放什么都不要紧,可是篮子是私人物品,操做系统并不会管你的篮子里都放了什么,也不会主动去清理你的篮子,所以在 C
语言中,堆中内容是须要程序员手动清理的,否则就会出现内存溢出的状况。
为了必定程度的解决堆的问题,一些高级语言(如 JAVA
)提出了一个概念:GC
,Garbage Collection
,垃圾回收,用于协助程序管理内存,主动清理堆中已不被使用的数据。
既然堆是一个大大的篮子,那么在栈中存储不了的数据(好比一个对象),就会被存储在堆中,栈中就仅仅保留一个对该数据的引用(也就是该块数据的首地址)。
OK
栈和堆内容如上,如今咱们再来看看你们的结论:
对于原始类型,数据自己是存在栈内,对于对象类型,在栈中存的只是一个堆内地址的引用。
感受很符合逻辑啊,按照定义基础类型存在栈中,对象存在堆中,没毛病啊!
可是,请你们思考一个问题:
既然栈中数据在函数执行结束后就会被销毁,那么 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
函数使用。
那么网上你们的结论就是错的了?非也!接下来谈谈我对 JavaScript
变量存储的理解。
既然在 JavaScript
中有闭包的问题,抛开栈(Stack
),仅用堆可否实现变量存储?咱们来看一个特殊的例子:
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
的变量并无存在栈中,而是在堆里,用一个特殊的对象(Scope
)保存。
那么在 JavaScript
变量究竟是如何进程存储的?这和变量的类型直接挂钩,接下来就谈谈在 JavaScript
中变量的类型。
在 JavaScript
中,变量分为三种类型:
局部变量很好理解:在函数中声明,且在函数返回后不会被其余做用域所使用的对象。下面代码中的 local*
都是局部变量。
function test () {
let local1 = 1;
var local2 = 'str';
const local3 = true;
let local4 = {a: 1};
return;
}
复制代码
被捕获变量就是局部变量的反面:在函数中声明,但在函数返回后仍有未执行做用域(函数或是类)使用到该变量,那么该变量就是被捕获变量。下面代码中的 catch*
都是被捕获变量。
function test1 () {
let catch1 = 1;
var catch2 = 'str';
const catch3 = true;
let catch4 = {a: 1};
return function () {
console.log(catch1, catch2, catch3, catch4)
}
}
function test2 () {
let catch1 = 1;
let catch2 = 'str';
let catch3 = true;
var catch4 = {a: 1};
return class {
constructor(){
console.log(catch1, catch2, catch3, catch4)
}
}
}
console.dir(test1())
console.dir(test2())
复制代码
复制代码到 Chrome
便可查看输出对象下的 [[Scopes]]
下有对应的 Scope
。
全局变量就是 global
,在 浏览器上为 window
在 node
里为 global
。全局变量会被默认添加到函数做用域链的最低端,也就是上述函数中 [[Scopes]]
中的最后一个。
全局变量须要特别注意一点:var
和 let/const
的区别。
全局的 var
变量其实仅仅是为 global
对象添加了一条属性。
var testVar = 1;
// 与下述代码一致
windows.testVar = 1;
复制代码
全局的 let/const
变量不会修改 windows
对象,而是将变量的声明放在了一个特殊的对象下(与 Scope
相似)。
let testLet = 1;
console.dir(() => {})
复制代码
复制到 Chrome
有如下结果:
那么变量的类型肯定了,如何进行存储呢?有两种:
Stack
)Heap
)相信看到这里,你们内心应该都清楚了:除了局部变量,其余的全都存在堆中!
但这是理想状况,再问你们一个问题:JavaScript
解析器如何判断一个变量是局部变量呢?
判断出是否被内部函数引用便可!
那若是 JavaScript
解析器并无判断呢?那就只能存在堆里!
那么你必定想问,Chrome
的 V8
可否判断出,从结果看应该是能够的。
红框内仅有变量 a
,而变量 b
已经消失不见了。因为 FireFox
打印不出 [[Scopes]]
属性,所以,笔者判断不出。固然,若是有大佬能深刻了解并补充的话,感激涕零。
好,了解了如何存储,接下来咱们看看如何赋值。
其实不论变量是存在栈内,仍是存在堆里(反正都是在内存里),其结构和存值方式是差很少的,都有以下的结构:
那好如今咱们来看看赋值,根据 =
号右边变量的类型分为两种方式:
何为常量?常量就是一声明就能够肯定的值,好比 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
指向的内存地址不会发生改变,所以,对象是常量!
何为变量?在上述过程当中的 foo
、bar
、obj
,都是变量,变量表明一种引用关系,其自己的值并不肯定。
那么若是我将一个变量的值赋值给另外一变量,会发生什么?
let x = foo;
复制代码
如上图所示,仅仅是将 x
引用到与 foo
同样的地址值而已,并不会使用新的内存空间。
OK
赋值到此为止,接下来是修改。
与变量赋值同样,变量的修改也须要根据 =
号右边变量的类型分为两种方式:
foo = 'foo';
复制代码
如上图所示,内存中保存了 'foo'
并将 foo
的引用地址修改成 0x0204
。
foo = bar;
复制代码
如上图所示,仅仅是将 foo
引用的地址修改了而已。
const
为 ES6
新出的变量声明的一种方式,被 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 boj3 = {
foo: 'foo',
bar: 'bar'
}
console.log(obj1 === obj2);
console.log(obj1 === obj3);
obj2.foo = 'foofoo';
console.log(obj1.foo === 'foofoo');
复制代码
请依次说出 console
的结果。
咱们不讨论结果,先看看内存中的结构。
因此你如今知道答案了吗?
在 JavaScript
中变量并不是完彻底全的存在在栈中,早期的 JavaScript
编译器甚至把全部的变量都存在一个名为闭包的对象中,JavaScript
是一门以函数为基础的语言,其中的函数变化多端,所以使用栈并不能解决语言方面的问题,但愿你们能看到,并真正的了解 JavaScript
在内存中的模型吧。
按照惯例,提几个问题:
JavaScript
变量的类型都有哪些?JavaScript
对于基础类型和对象类型是如何存储的?该系列全部问题由 minimo
提出,爱你哟~~~