玩转JS中的堆栈内存及函数底层处理机制
我们都知道 JS 都可以运行在浏览器中,我们还知道它是一门弱类型
,基于原型
的动态脚本,那么它是不是只能在浏览器中运行呢?答案是不是的,如今的JS已经强大到不止浏览器这些平台运行了,还可以在Node环境,WebView中运行,这些都是基于我们强大的V8引擎所赐,赋予了 JS 脱离浏览器也可以运行的能力。
那么 JS 又是如何在浏览器等其他平台运行的呢?
这涉及到编译原理,js在刚开始就是一大坨字符串文本,浏览器中的解释器(编译器)会对这些字符串有序地进行词法解析
,语法解析
后生成AST
(抽象语法书),最后将AST
转换成可执行的代码就是解释器的最后一步工作代码生成
。
从 JS 脚本加载到执行的过程中会有3个巨佬围着他,让他顺利运行且执行,咚咚咚,他们分别就是引擎
,解释器
以及作用域
。
由于这些和我们的主题虽然有关联,但是不是我们主题重点了解的知识点,我们就点到为止,有机会继续深究学习,望解,共勉,嘻嘻~
本文主要讲解的是 JS 运行过程中的堆栈内存变化以及函数底层的处理机制
That's right! 这篇文章我们重点学习的知识点就是理解什么是ECStack
,EC
,VO
,AO
以及GO
通过上文我们了解到 JS 之所以可以在浏览器运行是因为浏览器给它提供了供代码运行的环境。代码在运行前浏览器会分配一个内存供代码执行,而这个内存我们称之为ECStack
,我们也称之为执行栈
。
ECStack (Execution Content Stack)
ECStack 是计算机分配的一块内存,专门供代码执行
。
EC (Execution Content)
代码执行
又可以分为全局代码,函数中代码,私有块代码等等,不同环境下的代码执行都会有自己的上下文,这些上下文就是EC
,我们也可以称之为执行上下文
,也可以称之为当前上下文环境
。
VO(Varibale Object)
VO
就是变量对象
,代码在当前上下文执行时创建的变量总是会存储在当前上下文中指定的变量对象
中 ,简单地说就是变量对象就是用来储存当前上下文创建的创建的变量。
AO (Active Object)
其中,很多童靴再学习 VO
后在了解 AO
可能就会被弄晕,不仅会浮现一个问题 -- VO
与 AO
有什么瓜系???
当初靓靓的笔者在初学的时候也有过这个问题,迷惑了我好久,但是其实AO
可以理解为是 VO
的一种,区别就是 AO
是在函数中的 变量对象
,名曰 AO
, 又可以叫它为 二狗子 活动对象
。
这就有趣了,为什么说是同一个概念,到函数中就变了呢?
其实我们在创建函数的过程中,函数内部的代码不管对还是错我们都可以成功加载到页面中,因为我们还没有调用执行它,也就是我们声明一个函数时,你又没有调用它,那么它就失去了它的作用,相当于内部储存的代码块那么和笔者一样靓也没有用,就一坨字符串而已。
相反,如果我们创建它,而且还有责任心地调用执行它,实现了它的价值,高兴地和用钉钉上课的孩子一样,然后在内部创建一个 AO
,用来储存体内创建的变量,然后会绑定作用域链scope-chain
, ,链的左端是当前上下文EC(FUNC)
,链的右侧就是函数创建时所处的上下文,也叫函数的作用域 EC(G)
作用域链
PS: 只是用来参考,以便理解!!!
顾名思义,作用域链就是一条连接多个不同的作用域的链,就和我们的原型链一样,开发的时候如果双链齐用,哪怕别人学会了葵花宝典都打不过你了。
更为具体地来说,作用域链就是当前私有上下文代码执行,遇到一个变量,首先会问私有上下文中AO
是否创建过这么一个变量,如果有的话就自给自足,接下来的操作都在于私有操作,和其他上下文则没有任何关系。如果没有呢?不是自己私有变量的话就会通过作用域链走向上级的上下文中查找,如果找到了就取它,没有的话就重复继续往在上级的上下文查找,直到访达到全局上下文EC(G)
,如果找到了就取它,如果取不到就看是什么操作,假如是获取值的话找不到也没有用方法,只能给你来一句 “梁非凡,……” ** is not define
;但是如果是设置的话,就会在全局对象 GO
(下文会有解释) 中创建一个变量,并且挂载到 window
,和window中对应的属性变量呈现一种映射的关系。
同时函数执行的过程中,会生成一个封闭,私有的上下文,外界无法访问,用于保护里面的变量不受外界干扰,我们把这种函数执行的这种保护机制称之为闭包
。
GO (Global Object)
文章刚开始的时候我们提及到浏览器会分配一块内存用来执行 JS 代码,就是我们的 ECStack
,同时也会开辟一个堆内存,存储一些内置的方法以及属性,例如我们的setTimeout
,setInterval
,isNaN
等内置属性和方法,然后浏览器会在全局变量对象中创建一个变量在指向全局对象,这个变量就是window
,在 Node 环境中的与全局对象挂钩的变量是 global
。
最后不得不强调一点,全局对象 !== 全局变量对象,这两个含义完全不一样。
纸上谈兵终觉浅,绝知此事要躬行。我准备了一些简单的题目来更加直观地理解上述的名词性知识点,深入浅出供童靴食用更佳。
? ? ?
var a = 1;
var b = a;
b = 3;
console.log(a) // 1
var a = 1;
var b = a;
b = 3;
console.log(a) // 1
为什么变量 a 不会随着 b 的变化而变化呢?
因为刚开始a 与 b 指向共用同一个值,但是后面 b 更换了新的值,与原来的 1 断开了联系,和 2 开始了新的旅程,a 依然指向 1,并不会因为 b 的走心受到影响。
进击的切图仔
? ? ?
var a = {n: 1};
var b = a;
b.n = 3;
console.log(a.n) // 3
var a = {n: 1};
var b = a;
b.n = 3;
console.log(a.n) // 3
看图我们可以看出,因为刚开始a指向一个对象,而对象和基本数据不太一样,基本数据是存储在栈中,对象的话计算机会开辟一个堆内存来存储这个对象,并且将这个堆的地址赋予对应的变量,但要对变量进行操作的话通过地址对对应堆中的对象进行操作即可。
? ? ?
var a = {n: 1};
var b = a;
a.x = a = {n: 2};
console.log(a.x) // undefined
var a = {n: 1};
var b = a;
a.x = a = {n: 2};
console.log(a.x) // undefined
image
? ? ?
var a = {n: 1};
function fun(o){
o.n = 2;
o = {x: 555};
o.y = 233;
console.log(o) // {x: 555,y: 233}
}
fun(a)
console.log(a) // {n: 2}
var a = {n: 1};
function fun(o){
o.n = 2;
o = {x: 555};
o.y = 233;
console.log(o) // {x: 555,y: 233}
}
fun(a)
console.log(a) // {n: 2}
不多解释,咱们还是直接上图吧!
image
其中我们还有一点需要找到,就是函数执行的时候发生了什么。
在我们函数执行的时候,在未执行之前,函数体内的代码是被视为字符串存放到堆内存中的,所以在执行会解析这些代码字符串,并且执行它;
在执行的过程中
- 形成一个全新的私有上下文
- 有存储私有上下文中,声明的私有变量空间
AO
- 将上下文进栈执行
在执行前需要做的事情
- 初始化作用域链
- 初始化 this 指向
- 初始化 Arguments
- 形参赋值
- 变量提升
代码执行一般情况下,函数执行完成后,为了优化栈内存,会将形成的私有上下文移出栈释放掉,这就是我们一直说的“GC浏览器垃圾回收机制”
以上就是笔者在这方面学习的总结,你是否 Get 到了呢?