js从编译到执行过程

前言

之前写博客,经常需要引用一些基础的内容,每次都花不少时间找合适的文章,索性花点时间自己写。于是有了这个系列的文章。

概论

JavaScript 执行过程分为两个阶段,编译阶段和执行阶段。本文将重点分析第二阶段,并且在这基础上简单讲解变量提升、作用域链和闭包的原理。

编译阶段

 

复制代码

词法分析 语法分析 生成可执行代码,确定作用域规则

执行阶段

 

kotlin

复制代码

1,该阶段会进行执行上下文(Execution Context)的创建,包括创建变量对象、建立作用域链、确定 this 的指向等。每进入一个不同的运行环境时,V8 引擎都会创建一个新的执行上下文。 2,将创建的执行上下文压入调用栈,并成为正在运行的执行上下文,代码执行结束后,将其弹出调用栈。

一,编译阶段

1.1,词法分析

JS 引擎会将我们写的代码当成字符串分解成词法单元(token)。例如,var answer = 6 * 7; ,这段程序会被分解成七个 token 。每个词法单元token不可再分割。可以试试这个网站地址查看 token :Esprima: Parser

 

json

复制代码

[ { "type": "Keyword", "value": "var" }, { "type": "Identifier", "value": "answer" }, { "type": "Punctuator", "value": "=" }, { "type": "Numeric", "value": "6" }, { "type": "Punctuator", "value": "*" }, { "type": "Numeric", "value": "7" }, { "type": "Punctuator", "value": ";" } ]

1.2,语法分析

将词法单元转换成抽象语法树(AST)以便于理解和执行,也就是上面所说的token, 转换成树状结构的 “抽象语法树(AST)”

 

json

复制代码

{ "type": "Program", "body": [ { "type": "VariableDeclaration", "declarations": [ { "type": "VariableDeclarator", "id": { "type": "Identifier", "name": "answer" }, "init": { "type": "BinaryExpression", "operator": "*", "left": { "type": "Literal", "value": 6, "raw": "6" }, "right": { "type": "Literal", "value": 7, "raw": "7" } } } ], "kind": "var" } ], "sourceType": "script" }

1.3,生成可执行代码

将优化后的代码转换为机器代码(或者字节码),以便在执行阶段被执行。需要通过某种方法将 var a = 2; 的 AST 转化为一组机器指令,用来创建 a 的变量(包括分配内存),并将值存储在 a 中。

二,执行阶段

执行程序需要有执行环境,类似于 Java 需要 Java 虚拟机,同样解析 JavaScript 也需要执行环境,我们称它为“执行上下文”。

编译阶段的主要任务是为执行代码做好准备工作,创建好代码的执行环境。主要做的是:

 

kotlin

复制代码

创建执行上下文:绑定this,创建词法环境,创建变量环境,确定作用域链。

2.1,什么是执行上下文

执行上下文是对 JavaScript 代码执行环境的一种抽象,每当 JavaScript 运行时,它都是在执行上下文中运行。

全局执行上下文 — 这是默认或者说基础的上下文,任何不在函数内部的代码都在全局上下文中。它会执行两件事:创建一个全局的 window对象(浏览器的情况下),并且设置 this 的值等于这个全局对象。一个程序中只会有一个全局执行上下文。

函数执行上下文 — 当执行一个js函数时,js引擎会创建一个函数执行上下文,当函数执行结束之后,函数的执行上下文会被销毁。一个函数被多次调用,会创建多个执行上下文。

Eval 函数执行上下文 — 执行在 eval 函数内部的代码也会有它属于自己的执行上下文,因为开发者很少使用eval,所以在这里不作讨论。

2.2,执行上下文的创建

创建执行上下文有明确的几个步骤:

  1. 确定 this,即我们所熟知的 this 绑定。

  2. 创建 词法环境(LexicalEnvironment) 组件。

  3. 创建 变量环境(VariableEnvironment) 组件。

    对于一个执行上下文,它包含两个对象:变量环境对象词法环境变量

2.2.1,this的绑定

在全局执行上下文中,this 的值指向全局对象。(在浏览器中,this 引用 Window 对象)。

在函数执行上下文中,this 的值取决于该函数是如何被调用的:

 

kotlin

复制代码

通过对象方法调用函数,this 指向调用的对象 声明函数后使用函数名称普通调用,this 指向全局对象,严格模式下 this 值是 undefined 使用 new 方式调用函数,this 指向新创建的对象 使用 call、apply、bind 方式调用函数,会改变 this 的值,指向传入的第一个参数

这个后续文章展开讲解。

2.2.2,词法环境

词法环境其实是一个栈结构,栈成员是一个对象,保存let和const变量。

如下代码创建的执行上下文的词法环境便如下:

 

js

复制代码

let a=0 function fn(){ let a=1 { let b=2 console.log(a,b)//1 2 } console.log(b)// b is not defined } fn()

4.PNG

2.2.3,变量环境

变量环境是一个对象,保存var变量和function函数声明。

变量环境与词法环境差不多。在 ES6 中,词法环境和变量环境的明显不同就是前者被用来存储函数声明和变量(let/const)的绑定,而变量环境只用来存储 var 变量和function的绑定。

如下代码的执行上下文:

 

js

复制代码

var a = "123" function func1() { var b = "123" console.log(b) func2() } funcgion func2() { const c = "456" console.log(c) } func1()

3.png

三,执行阶段

执行栈,又叫做“调用栈”,被用来存储代码运行时创建的所有执行上下文。

当 JS 引擎开始执行第一行 JavaScript 代码时,它会创建一个全局执行上下文然后将它压到执行栈中,每当引擎遇到一个函数调用,它会为该函数创建一个新的执行上下文并压入栈的顶部。

引擎会执行那些执行上下文位于栈顶的函数当该函数执行结束时,执行上下文从栈中弹出,控制流程到达当前栈中的下一个上下文。

1.PNG

代码的示意在上文2.2.3小节中已经体现出来了。

四,从执行上下文理解作用域和作用域链

4.1,作用域和变量提升的理解

对于作用域的创建时间,网上很多文章说得很乱,一会说编译的时候确定,一会又说是创建执行上下文的时候确定。

其实作用域是一套规则,是在 JavaScript 引擎编译的时候就已经确定。甚至可以说我们写下js代码的时候就已经能肉眼看明白作用域了。

而实际上,对于要运行的代码而言,它的作用域就等同于执行上下文。因为是这个执行环境,将代码中的var变量和function函数放入到变量环境中,并且变量会被默认设置为undefined。在执行阶段,js引擎会在变量环境中查找声明的变量和函数。这就是我们所说的“变量提升”,这也是为什么函数可以在函数的实现之前调用。

这里需要注意的是:作用域等同于执行上下文(包含变量的访问权限),但是作用域链不等同于执行上下文栈,这是两个不同的概念,作用域链是变量访问路径,执行上下文栈是代码执行环境。

4.2,块级作用域的理解

4.1中说了普通的var变量和fun在执行上下文中作用域的确定,那块级作用域呢?

按照我们的理解,平时的普通代码中,一个函数执行上下文会且仅会生成一个执行上下文。

那函数中如果又有let的块级作用域怎么办?

其实在词法环境中,维护了一个作用域栈,栈底是函数的最外层变量(letconst声明的变量),进入一个作用域块后,就会把该作用域中的变量入栈;当作用域中的代码执行完成之后,该作用域的信息就会从栈顶弹出。

 

js

复制代码

function fun() { var c=4 let a = 1 { let a = 2 let b = 3 console.log(a) console.log(b) } console.log(a) console.log(b) } fun()

在上下文内部,变量访问总是从词法环境的栈顶开始,从栈顶到栈底,然后到变量环境。

2.png

 

css

复制代码

1,当fun函数被编译时,外层的a变量首先被创建,并存放至词法环境作用域栈中,此时函数内部的块级作用域中的变量不会被创建。 2,当函数执行至作用域块时,let a和let b也被创建并入栈存放至栈顶。并将a赋值为2,将b赋值为3。 3,当执行至console.log(a)和console.log(b)时,js引擎首先从栈顶找到a和b的值并打印出2和3。 4,当作用域块执行完成之后,作用域块中的变量信息从栈中弹出。 5,接着执行console.log(a)找到的是栈底的a变量,并打印出1。接着执行console.log(b),由于在词法环境和变量环境中都找不到b变量,所以便会报错b is not defined。

4.3,作用域链的生成

作用域链是在执行上下文栈的创建阶段创建的。

在创建执行上下文的时候,在创建完词法环境和变量环境后,作用域链自然而然得到了。

因为在上下文内部,变量访问总是从词法环境的栈顶开始,从栈顶到栈底,然后到变量环境。在上下文之间,通过outer变量(每个执行上下文都会创建一个outer变量用来指向下一个作用域)访问下一级上下文(注意是下一级不是下一层,下文会讲),直到全局上下文结束。这个变量访问顺序就是作用域链

这时候代码还没有执行,只是在构建代码执行的运行环境。所以说我们的作用域链是指静态作用域。

 

js

复制代码

console.log(a)//undefined var a=3 function test(){ console.log(b)//暂时性死区,Cannot access 'b' before initialization let b=4 console.log(a,b)//3,4 } test()

如上代码,从创建上下文到执行过程中,执行栈的变化如下:

5.PNG

到这第四步,函数test中代码还没执行的时候。let定义的b只是在词法变量中创建,但是没有初始化,所以就会构成【暂时性死区】,这时候打印b,会报错:“Cannot access 'b' before initialization”。

接着代码继续执行,执行栈的变化如下图:

6.PNG

当执行console.log(a,b)的时候,查找a的值,就是顺着作用域链,跨执行上下文,在全局上下文找到的。

值得注意的是:这里的outer变量访问下一级上下文,并不是下一层上下文,而是outer变量指向的下一个上下文,这就是我们上文说作用域链不等同于执行上下文栈的原因

例如:

 

js

复制代码

function bar(){ console.log(myName)//我名字 } function foo(){ var myName='啦啦啦' bar() } var myName='我名字' foo()

当代码执行到bar函数的console.log(myName)的时候,outer的指向和执行上下文栈是这样的:

7.PNG

可以看到,真正的作用域链,在当前执行文的变量查找顺序是:从词法环境的栈顶开始,从栈顶到栈底,然后到变量环境。

跨上下文之间,则是通过outer变量的指向来决定下一级执行上下文。

这些都是在编译阶段确定好的规则,所以一直说作用域链基于静态作用域。

五,从执行上下文理解闭包

5.1,闭包产生的条件
 

scss

复制代码

函数嵌套 内部函数引用了外部函数的数据(变量/函数) 外部函数执行

5.2,js运行过程中的数据存储

然后讲讲js在运行的过程中,数据是怎么存储的。

在js的执行中有三种内存空间:代码空间、栈空间、堆空间

代码空间是存储可执行代码的,我们主要来看看栈空间和堆空间。

上文一直讲到的执行上下文栈就是我们说的栈空间,主要用来存储上下文。

而我们的堆空间,主要用来存储引用类型的数据。

如下代码:

 

js

复制代码

function foo(){ var myName = "苏轼" const test1 = 2 function bar(){ console.log(myName,test1) } bar() } foo()

当执行到foo内部,bar还未执行时(还未生成闭包时)所对应的堆栈存储情况如下:

8.png

5.3,从执行上下文和内存角度理解闭包

上文中说到,作用链其实就是通过每个执行上下文的outer链接起来形成的。实际上,每个执行上下文在创建的时候,都会生成一个名为[[scoped]]的属性。

它是一个数组,在创建执行上下文的时候,就会顺着outer连接的链查找自由变量(既不是形参也不是函数内部定义的局部变量的变量即自由变量),outer变量每连接上一个执行上下文(或者闭包),它就会往数组添加一条。

如下代码:

 

js

复制代码

var a=1 function foo(){ var b=3 var d=4 function test1(){ var c=5 console.log(a,b,c) } console.dir(test1) test1() } foo()

console.dir(test1)打印出来的结果就是:

 

json

复制代码

ƒ test1() arguments: null caller: null length: 0 name: "test1" prototype: {constructor: ƒ} [[FunctionLocation]]: index.js:5 [[Prototype]]: ƒ () [[Scopes]]: Scopes[2] 0: Closure (foo) {b: 3} 1: Global {window: Window, a: 1,...}

可以看到在test1生成的执行上下文,它内部的自由变量是a和b,于是顺着outer连接的执行上下文查找,第一个找到了foo执行上下文中的b,第二个找到了全局执行上下文的a,所以最后生成了Scopes[2]是两个值的数组。

理解了[[scoped]]和outer的关系,就可以接着理解闭包的原理,修改上文代码为:

 

js

复制代码

function foo(){ var myName = "苏轼" const test1 = 2 const test2=3 function getValue(){ var test3=4 console.dir(getValue) console.log(myName,test1) } function setName(name){ myName=name } return { getValue, setName } } var result=foo() result.getValue() result.setName('李白') result.getValue()

我们不需要打印,也可以在chrome浏览器中打断点查看这个作用域链的变化过程:

9.PNG

这时候打印出来的[[scoped]]就是作用域链减去当前执行上下文的内容,于是就会是:

 

json

复制代码

[ 0: Closure (foo) {myName: '李白', test1: 2, getValue: ƒ} 1: Global {window: Window, self: Window, document: document, name: '', location: Location, …} ]

这其中的Closure (foo)就是foo函数的闭包作用域的意思。那我们讲到闭包的原理,最主要的就是想明白,为啥它能够在foo函数的执行上下文出栈后,还能访问其中的变量?

要搞明白这一点,就得搞清楚这个闭包作用域存储在哪里,如何被访问的。

结合5.2节的知识,我们来画下这段代码在内存中的存储情况。

当执行到var result=foo()的时候的内存情况

 

scss

复制代码

js执行foo函数时,首先会编译,编译过程中遇到内部函数getValue和setName,js引擎还要对内部函数做一次词法扫描,发现内部函数引用了foo函数中的myName变量和test1,js引擎会判断这是一个闭包,于是会在堆空间中创建一个"closure(foo)"对象(内部对象,js无法访问)来保存myName和test1。

10.png

当执行到result.getValue()的时候,内存存储情况:

11.png

注意到getValue的执行上下文的outer指针指向了堆空间中的闭包,而闭包中又有个outer指向了全局上下文。所以形成的作用域能够让getValue访问test1和myName变量。

上文其实已经给出了这个作用域链。看起来就是在当前作用域后边多了个闭包作用域:

12.PNG

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: JavaScript 是一种解释性语言,这意味着它在执行代码时并不会先进行编译,而是直接解释执行代码。这也是为什么 JavaScript 可以直接在浏览器中执行,而不需要经过任何预处理的原因。 在 JavaScript 中,当你在网页中包含了一段 JavaScript 代码,浏览器就会执行这段代码。当你在浏览器中打开控制台,并输入一段 JavaScript 代码,浏览器也会立即执行这段代码。这就是 JavaScript 解释执行过程。 在 JavaScript 中,解释器会一行一行地读取代码,并立即执行。这也是为什么 JavaScript 中的代码必须按照正确的顺序书写的原因。如果你的代码中有语法错误或逻辑错误,解释器就会在执行过程中报告错误,并停止执行。 总的来说,JavaScript 解释执行过程就是浏览器在读取和执行 JavaScript 代码时所进行的过程。 ### 回答2: JavaScript 是一种解释型语言,它的代码在运行时通过解释器进行解释执行,而不需要经过编译器的编译过程JavaScript 的解释器会逐行解释代码,并在解释过程中动态执行代码。 首先,JavaScript代码不需要显式地进行编译,在代码执行之前不会产生一个完整的编译结果。相反,JavaScript 解释器会逐行解释代码,并即时执行。这种即时执行的特性使得开发者能够更加迅速地测试和调试代码,提高了开发效率。 其次,JavaScript 解释器的工作方式与编译器不同。编译器将源代码编译为机器可执行的二进制代码,而解释器则是逐行解释代码,并将解释结果直接执行。解释过程中,解释器会逐行扫描代码,将代码转换为机器指令并执行。这种即时翻译和执行的方式使得JavaScript代码能够适应不同平台和环境,无需重新编译。 总之,JavaScript 的解释执行并不依赖于编译编译代码,而是通过解释器进行逐行解释和执行。这种即时执行的方式使得JavaScript适用于快速开发和动态调试,并能够在不同平台和环境中运行。 ### 回答3: javascript解释执行的确是编译编译代码。在传统的编译语言中,代码执行之前需要经过两个步骤:编译执行编译器会先将源代码转换成机器代码,再由计算机执行。 然而,javascript是一种解释型语言,不同于传统编译语言。在javascript中,代码并不是直接编译成机器代码,而是由javascript解释器逐行解释执行。 在javascript代码执行过程中,解释器会将代码逐行解析并执行。它会先进行语法解析,将代码分解成语法树,并进行词法分析,确定每个标识符的含义和作用域。然后,解释器会对代码进行逐行解释和执行,一边解析一边执行。 这种解释执行的方式带来了一些优势。首先,代码无需编译成机器码,可以直接在平台上运行,简化了开发流程。其次,解释器能够根据不同平台和环境的特性进行实时的优化,提高程序的性能和效率。 然而,与编译型语言相比,javascript的解释执行也存在一些劣势。由于每次都需要解释执行代码,相同的代码可能会被多次解释执行,导致性能下降。为了解决这个问题,javascript引入了即时编译(Just-In-Time Compilation)技术,将一些频繁执行代码编译成机器码,减少解释执行的时间,提高性能。 综上所述,javascript是一种解释型语言,它的执行过程是由解释器逐行解释和执行编译编译代码。这种解释执行的方式简化了开发流程,但也存在性能方面的劣势,通过即时编译技术可以提高性能。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值