JavaScript进阶(一)
一. 深入理解js的执行堆栈
js是单线程的,但是严格来说,负责执行js代码的程序(js引擎)是单线程的,js 代码的执行需要环境提供内存空间、依赖的全局变量、Event Loop系统,这些是由 js引擎所在的宿主环境提供的,该环境可以是浏览器或者 Node.js(下面的讨论基于浏览器中的V8环境)。
1)js代码执行流程
在熟悉浏览器执行js的过程中,简单了解过js代码的执行过程,这里进一步理解一下该过程。
- 词法分析:将代码的字符串分析得到词法单元token。
- 语法分析:将词法单元流解析成AST(抽象语法树),该过程包括词法作用域的生成、变量提升等阶段。
- 代码生成:AST转换成字节码,这部分由 V8 中的 Ignition 解释器来生成的。
- 代码执行:逐条解释执行字节码,注意了,当 V8 发现有大量重复字节码时(热点代码 HotSpot ),会将其编译成机器码(由引擎中 TurboFan 编译器进行编译),下次再碰到类似字节码不需要解释,直接执行,这种与解释器配合的过程也称为 JIT (即时编译)。
- 垃圾回收
第1、2步统称为代码解析过程。
2)可执行环境(执行上下文 ECS)
js中的可执行环境主要有三种:全局执行环境、函数执行环境和eval执行环境,eval函数不常用也不提倡使用,我们主要说前两种。
- 全局执行环境
一般情况下,当一个页面被打开的时候,浏览器就会分配一个独立的渲染进程给它,里面包含了js引擎工作需要的一切,包括内存、全局变量等,以及全局执行环境,有了执行环境js引擎就会开始工作。
一个程序只有一个全局执行环境。 - 函数执行环境
js引擎执行过程中遇到函数调用就会创建一个新的执行环境,也就是函数执行环境。
一个程序可以有任意多个函数执行环境。
执行上下文的生命周期分为三个阶段:创建 -> 执行 -> 回收。
-
创建阶段
发生在js代码执行之前,在该阶段程序主要做三件事:
1、创建词法环境组件
2、创建变量环境组件
3、this绑定
(所以,词法环境是存在于执行上下文中的,这方便我们接下来理解区分两者) -
执行阶段
正常对变量进行声明、赋值、执行其它代码。 -
回收阶段
发生在js代码执行结束,出栈之后,等待虚拟机回收。
3)执行上下文栈(调用栈、执行栈)
这是一个后进先出(LIFO)的栈结构,用来存储程序运行过程中创建的所有执行上下文。
当js引擎第一次遇到我们写的脚本时,会创建一个全局执行的上下文,压入调用栈;以后每遇到一个函数调用就创建一个新的函数执行上下文并压入调用栈顶部。然后引擎会依次执行栈中的栈顶上下文,每执行完一个就弹出一个……一旦所有的代码执行完毕,js引擎就会从调用栈中移除全局执行上下文。
二. 弄清作用域、执行上下文和词法环境
执行上下文中包含作用域,执行上下文中包含词法环境。在 ES5 后,Scope 被替代为 Environment,Environment 取代了作用域,称为 Lexical Environment(词法环境)。
1)作用域
作用域是在代码解析过程中就确定的一套规则,用于确定在何处以及如何查找变量(标识符)。我们可以理解为:作用域就是一个独立区域,这个区域里的变量不能被暴露出去,外部不可直接访问。所以,作用域最大的用处就是隔离变量,达到封装的目的,不同作用域下同名变量不会有冲突。
然后我们看一段代码:
function foo(){
console.log(a)
}
var a = 1
function func(){
var a = 2
console.log(a)
foo()
}
func()
//打印结果
//2
//1
内层作用域可以访问外层作用域,函数在创建AO的时候会存放上一层的词法作用域引用,所以函数foo中没有声明a变量的情况下,会通过该引用往外层的作用域查找。而一层一层向外部作用域寻找的过程又形成了作用域链。
还需注意的是,foo的外层作用域是全局作用域,而非func函数的作用域!!!
作用域和执行上下文之间最大的区别是: 执行上下文在运行时确定,随时可能改变;作用域在定义时就确定,并且不会改变。
2)词法环境
该部分基于ES6版本进行总结
标识那些 用来解析由此执行上下文中的代码所创建的标识符 的词法环境
词法环境可以理解为一个拥有 标识符–变量的映射 的结构,这里的标识符指的是变量/函数的名字,而变量是对实际对象(包含函数类型对象)或原始数据的引用。一个词法环境由环境记录器和一个外部词法环境的引用组成。
了解了执行上下文和作用域,不难猜到,词法环境也分全局词法环境和函数词法环境两种。
-
全局环境
没有外部环境的引用(外部环境引用为null),拥有一个全局对象(浏览器中为window )及其关联的方法和属性,以及任何用户自定义的全局变量,this 的值指向这个全局对象。 -
函数环境
用户在函数中定义的变量被存储在环境记录中,包含了arguments 对象。对外部环境的引用可能是全局环境,也可能是包含内部函数的外部函数环境。 -
模块环境
包含了当前块级区域最高层的绑定,以及模块显示导入的绑定。对外部环境的引用是全局环境。
// 伪代码表示两种词法环境
GlobalExectionContent = { //全局执行上下文
LexicalEnvironment: { //词法环境
EnvironmentRecord: { //环境记录
Type: "Object",
// 剩余标识符
},
Outer: null, //外部环境引用
}
}
FunctionExectionContent = { //函数执行上下文
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
// 剩余标识符
},
Outer: [Global or outer function environment reference],
}
}
进一步了解一下环境记录和外部环境的引用。
- 环境记录(Environment Record)
可以看作一个存储了当前词法环境中所有标识符的对象(ES3中的VO和AO),换句话说,任何在环境记录中的标识符都可以在当前词法环境直接以标识符形式访问,它记录的是标识符和变量的映射。
环境记录可以分为以下三种:
1. 对象式环境记录(Object Environment Record)
对象式环境记录项用来定义那些将标识符与某些对象属性相绑定的ES语法元素。(ES2018)
- 以对象的形式存储了一组字符串标识符名称,有一个关联的绑定对象(binding object),这些名称直接对应于其绑定对象的属性名称。
比如,var num=100
,则window.num
也可以访问到该变量,得到变量值100;window.sun=200
,则直接通过sum
访问该变量也可以。 - 只存在于全局上下文和with语句中,但with语句的使用场景很少见(目前还不了解,没有用过)。
- 只记录全局var、function声明的标识符。
- 每个标识符在绑定后都会直接实例化并初始化为undefined(所以可以变量提升),如果绑定对象上已经存在该属性,那么初始化的值就是绑定对象的原有值。
- 标识符可以重复声明。
2. 声明式环境记录(Declarative Environment Record)
声明式环境记录项是用来定义那些直接将标识符与语言值直接绑定的ES语法元素,例如变量,常量,let,class,module,import以及函数声明等。(ES2018)
- 记录const、let、function等等,除了var声明的标识符,没有关联的绑定对象。
- 将所有非var声明的标识符实例化但不初始化,也就是变量处于uninitialized状态(所以let 和const 没有变量提升),也就是说内存中已经为变量预留出空间,但是还没有和对应的标识符建立绑定关系。
但是JS引擎对函数的声明进行了特殊的处理,允许像var那样进行提升。 - 标识符不能重复声明,和var声明的标识符也会有冲突。
该变量记录又可以细分为两类:
(1). 函数环境记录(Function)
声明式环境记录的子类型,继承了其所有方法,代表了一个函数的顶层作用域。非箭头函数中,还会提供this绑定和super方法。
所以,函数环境中的是声明式环境记录,而不是对象式环境记录。
(2). 模块环境记录(Module)
另一个子类型,用于体现一个模块(ES Module)的外部作用域,即模块export所在环境。
除普通的可变和不可变绑定外,还提供了不可变的import绑定(就是Module作用域中可以通过import语法引入外部的绑定,并且这些绑定不可变)。
还有个全局环境记录,这个比较特殊,在ES5中的描述说到,这是一个在所有代码运行之前就创建的一个独立记录,它也是一个对象环境记录,只不过绑定的对象是globalObject。所以,很多文章在讨论ES5的时候就不把它单独归为一种类型了。
然后到了ES6,标准文档中对于全局环境记录的描述又有所不同,不再把它说成是一个对象式环境记录了,而是封装了对象式环境记录和声明式环境记录的一个复合记录。
有些文章中把环境记录分为三种类型应该也是这个原因,博主说的可能是ES6及以后的标准。
接下来我也对新的全局环境记录做了解释:
3. 全局环境记录(Global Environment Record)
A global Environment Record is logically a single record but it is specified as a composite encapsulating an object Environment Record and a declarative Environment Record.
全局环境记录在逻辑上是一种独立的记录,但实际上它被指定为封装了对象式环境记录和声明式环境记录的一个复合记录。
这个比较特殊,它绑定了全局对象,所以肯定是需要对象式环境记录,当然规范中也写了;而在规范中也明确指出了它包含 [[DeclarativeRecord]] 这个指向声明式环境记录的字段。
这个环境记录中包含了js内置对象和全局对象的属性,以及所有在script中的顶级声明。
- 外部环境引用(OuterEnv)
通过[[OuterEnv]] ,当前的环境记录可以访问到最近的外部环境记录;如果是全局词法环境,这个值为null。也就是,它把不同的代码段串成了链式结构,在当前环境记录中没有目标值的时候,就会通过[[OuterEnv]] 从外部环境一层一层寻找,这就构成了新的作用域链。
3)变量环境
也是一种词法环境,和词法环境的结构是一样的。这两者的区别在于,词法环境用于存储非var绑定(let、const、with()、try-catch创建的那些),而变量环境仅用于存储当前执行上下文中的var声明和函数声明。
变量环境只有全局和函数作用域,词法环境则是有全局、块、函数。
标识那些 环境记录中包含 该执行上下文中的变量语句和函数声明所创建的绑定 的词法环境
上代码,看实例:
let a = 100;
const b = 200;
var c;
function multiply(e, f) {
var g = 300;
return e + f + g;
}
c = multiply(a, b);
上面一段代码的执行上下文用伪代码的方式表示出来就是:
//全局上下文
GlobalExectionContext={
ThisBinding: <Global Object>,
LexicalEnvironment[0x01]: { //词法环境
EnvironmentRecord: { //环境记录
Type: "Declarative",
//在这里绑定标识符
a: <uninitialized>,
b: <uninitialized>,
multiply: <func>
},
OuterEnv: VariableEnvironment[0x02]
},
VariableEnvironment[0x02]: { //变量环境
EnvironmentRecord: { //环境记录
Type: "Object",
//在这里绑定标识符
c: undefined
},
OuterEnv: null
},
GlobalEnvironment[0x00]: { //全局环境
[[DeclarativeRecord]]: 0x01,
[[ObjectRecord]]: { // 与 window 对象绑定
c : undefined
...
},
[[VarNames]]: ["a", "b", "foo",...]
}
}
//函数上下文
FunctionExectionContext={
ThisBinding: <Global Object>,
LexicalEnvironment: { //词法环境
EnvironmentRecord: { //环境记录
Type: "Declarative",
//在这里绑定标识符
Arguments: {0: 100, 1: 200, length: 2}, // arguments对象
},
OuterEnv: <GlobalLexicalEnvironment>
},
VariableEnvironment: { //变量环境
EnvironmentRecord: { //环境记录
Type: "Declarative",
//在这里绑定标识符
g: undefined
},
OuterEnv: <GlobalLexicalEnvironment>
}
}
有点乱是不是?!!
词法环境中不是有个对象式环境记录就是记录var声明的吗?为什么后边又说词法环境只记录非var声明啊??
(一开始我以为是因为函数词法环境中没有对象式环境记录,所以需要变量环境来干对象式环境记录的事儿。可是,从上边的示例中能看到,即便在全局环境中,var声明还是存在变量环境中……我醉了==)
然后我看到了这样一段话:
之所以在 ES5 的规范里要单独分出一个变量环境的概念是为 ES6 服务的: 在 ES6 中,词法环境组件和变量环境的一个不同就是前者被用来存储函数声明和变量(let 和 const)绑定,而后者只用来存储 var 变量绑定。
感谢博主,,,原文在这里:https://juejin.cn/post/7043408377661095967
我个人的理解:在ES5以后,其实已经趋向于使用块级作用域,尽量降低var这类绑定全局对象的声明的比重,但是又不能完全摒弃,所以为了将var类型的声明和其它声明区分的更明确,就把原来的对象式环境记录从词法环境中拿出来“自成一派”,叫变量环境。