JavaScript 脚本编译与执行过程简述

简介: JavaScript 脚本编译与执行过程简述

前言


由于上一篇文章对作用域、执行上下文等写得过于详细,而且参杂了很多内容,看起来并不好理解,因此就简化一下。


正文


在网页里,JavaScript 脚本是插入在 <script> 标签内的(内联或外部引入)。

<script>
  // some JavaScript code...
</script>
<script>
  // some JavaScript code...
</script>
<script src="xxx"></script>


一般情况下,脚本是按顺序一块一块地执行的(一个 <script> 标签为一块)。只有执行完一块,才会执行下一块的。如果当前块有语法错误或其他错误导致当前块终止执行,不会影响下一块的执行。


对于外部引入的 JavaScript 脚本,若设置 <script> 标签的 deferasync 属性会影响脚本的加载顺序,不会按着编写顺序执行。而这些属性对于内联脚本是没有作用的。


要让 JavaScript 代码运行起来,包括编译阶段执行阶段。每一块脚本的运行都包含这两个过程。请注意,不是将“所有块”编译完再执行的。

编译阶段:
  词法分析:将源码分解成很多个有意义的 token
  语法分析:通篇检查当前块的语法是否有误,若无,将 tokens 转换为 AST
  代码生成:将 AST 转换为 JS 引擎可执行代码
执行阶段:
  创建执行上下文
  初始化执行上下文
  代码执行


JS 引擎的优化工作是在编译阶段完成的,例如 V8 引擎。若编译阶段检查出语法有误,将会终止当前块的后续阶段。


待编译阶段完成后,接着进入执行阶段。而执行阶段可以分为两个步骤:


  1. 进入执行上下文
  2. 代码执行


进入执行上下文步骤,可以理解为,真正一行一行执行代码的前期准备工作:


  1. JS 引擎创建一个叫做:执行上下文栈(Execution Context Stack,ECS)的东西。顾名思义,就是用了维护执行上下文的。
  2. 最先进入的肯定是全局代码,这个执行上下文被称为:全局上下文。首先插入 ECS,因此 ECS 栈底永远是全局上下文。后面调用函数时,会进入函数上下文,该上下文也会插入 ECS。
  3. 每进入一个执行上下文,都会做一些初始化工作。

待准备工作完成后,就会进入代码执行步骤,即一行一行地去执行代码。

大家可能疑惑的地方,应该是“前期准备工作”的具体过程是怎样的?


执行上下文栈


进入全局代码,ECS 插入 GlobalContext,后面每调用一个函数就往 ECS 栈顶插入 FunctionalContext,函数执行完 FunctionalContext 又会从栈顶删除......周而复始的过程。ESC 栈底永远保留着 GlobalContext。直到页面销毁,它才会被删除,ESC 的使命也就结束了,被 JS 引擎清空。

ECS = [
  FunctionalContext // 栈顶
  ...
  FunctionalContext
  GlobalContext  // 栈底
]


前面提到上下文:GlobalContext(全局上下文)、FunctionalContext(函数上下文)。


执行上下文


其实 JavaScript 中有三种执行上下文:

全局上下文:
  全局下都是属于全局上下文。
函数上下文:
  顾名思义,就是 JavaScript 函数内的执行上下文
eval 上下文:
  就是 eval('some code...'),尽可能不要用,会影响性能、欺骗词法作用域


每个执行上下文,可以看作是一个 JavaScript 对象,它有三个重要属性:

Variable Object: 
  即变量对象,简称 VO。我们声明的变量、函数就是放在 VO 中的。
Scope Chain: 
  即作用域链,简称 Scope。它包含了当前上下文 VO,以及各父级的 VO。查找变量就是根据这链条去查询的。
This: 
  即 this 对象,这是一个很灵活的对象,跟函数调用相关。
}


执行上下文的初始化工作,就是确定这些属性的一些值。这个“初始化”的过程也常被称为“预编译”,网上很多文章都有这个说法。


举例


上面的执行阶段的过程,举例会更好地说明。

var a = 'global'
function foo() {
  var a = 'local'
  function bar() {
    console.log(a)
  }
  bar()
}
foo() // "local"


当编译过程之后,进入执行阶段。JS 创建一个执行上下文栈 ECStack,总是先进入全局上下文,全局上下文被插入到执行上下文栈:

ECStack = [ GlobalContext ]


接着对 GlobalContext 进行初始化:

GlobalContext = {
  VO: window, 
  Scope: [ GlobalContext.VO ],
  this: GlobalContext.VO
}
// 注意,不同宿主环境顶层对象是不一样的。
// 浏览器下为 window 对象,Node 中为 global 对象。
// 本文以浏览器为例,因此直接写了 window 对象。


初始化的同时 foo 函数也被创建,会保存当前上下文的作用域链到函数内部的 [[scope]] 属性中。该属性可以通过 console.dir(foo) 查看。

foo.[[scope]] = [ GlobalContext.VO ]


当全局上下文初始化完成之后,开始执行全局代码,当进行到 foo() 时,JS 引擎又会创建 foo 函数执行上下文,并插入 ECStack 栈顶。

ECStack = [ GlobalContext, FunctionalContext<foo> ]


跟着,会进入 foo 函数上下文,该上下文也会进行初始化工作:


  1. 复制 foo 函数 [[scope]] 属性创建作用域链。
  2. arguments 创建活动对象 AO。
  3. 初始化活动对象,即 AO 按顺序加入:形参、函数声明、变量声明。若存在同名情况,函数声明会覆盖形参,变量声明会被忽略。
  4. 将 AO 对象加入 foo 作用域链顶端。

FunctionalContext<foo> = {
  AO: {
    arguments: {
      length: 0
    },
    a: undefined,
    bar: ƒ bar()
  },
  Scope: [ GlobalContext.VO, AO ],
  this: undefined // this 是在函数执行的时候才确定下来的,foo 函数的 this 的值跟作用域链没有关系
}


初始化完成,开始执行 foo 函数体代码,AO 对象也会更新。当执行到 bar() 函数时,又将会进入 bar 函数上下文,并进行初始化工作:

ECStack = [ GlobalContext, FunctionalContext<foo>, FunctionalContext<bar> ]

FunctionalContext<bar> = {
  AO: {
    arguments: {
      length: 0
    }
  },
  Scope: [ GlobalContext.VO, FunctionalContext<foo>.VO, AO ],
  this: undefined
}


初始化完成,开始执行 bar 函数代码,在 console.log(a) 中,会查找 a 变量。


按作用域链顺序查找,先从当前上下文的 AO 中查找,若查找不到再往上一层 AO 中查找...直到全局上下文的 VO 中查找,若再查找不到就会抛出引用错误(ReferenceError)。

AO -> FunctionContext<foo>.AO -> GlobalContext.VO


很明显,a 变量将会在 FunctionContext<foo>.AO 中查找到,其值为 "local",因此打印结果就是 "local"


bar() 执行完毕,bar 函数上下文会被从栈顶删除。

ECStack.pop(FunctionalContext<bar>)


又将执行权交还给 foo 函数上下文,它也将会执行完毕,也会从栈顶移除。

ECStack.pop(FunctionalContext<foo>)


然后又交还给全局上下文

ECStack = [ GlobalContext ]


至此,这个程序执行完毕。当页面销毁时,ECStack 才会被清空。

相关实践学习
2分钟自动化部署人生模拟器
本场景将带你借助云效流水线Flow实现人生模拟器小游戏的自动化部署
7天玩转云服务器
云服务器ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,可降低 IT 成本,提升运维效率。本课程手把手带你了解ECS、掌握基本操作、动手实操快照管理、镜像管理等。了解产品详情:&nbsp;https://www.aliyun.com/product/ecs
目录
相关文章
|
7月前
|
资源调度 前端开发 JavaScript
Babel:JavaScript代码的编译利器
Babel:JavaScript代码的编译利器
|
7月前
|
JavaScript 前端开发 测试技术
使用Selenium执行JavaScript脚本:探索Web自动化的新领域
本文介绍了如何在Selenium中使用JavaScript解决自动化测试中的复杂问题。Selenium的`execute_script`函数用于同步执行JS,例如滑动页面、操作时间控件等。在滑动操作示例中,通过JS将页面滚动到底部,点击下一页并获取页面信息。对于只读时间控件,利用JS去除readonly属性并设置新日期。使用JS扩展了Selenium的功能,提高了测试效率和精准度,适用于各种自动化测试场景。
|
7月前
|
JavaScript 前端开发 Java
liteflow规则引擎 执行Javascript脚本
liteflow规则引擎 执行Javascript脚本
207 1
|
7月前
|
JavaScript
node下的two.js调用one.js出现无法编译问题 Cannot find module ‘c:
node下的two.js调用one.js出现无法编译问题 Cannot find module ‘c:
80 0
|
7月前
|
移动开发 JavaScript 数据可视化
分享88个JS播放器脚本,总有一款适合您
分享88个JS播放器脚本,总有一款适合您
126 0
|
7月前
|
移动开发 JavaScript 前端开发
分享95个JS表单脚本,总有一款适合您
分享95个JS表单脚本,总有一款适合您
62 0
|
7月前
|
移动开发 JavaScript 前端开发
分享106个JS表单脚本,总有一款适合您
分享106个JS表单脚本,总有一款适合您
50 0
|
7月前
|
移动开发 JavaScript 前端开发
分享98个JS表单脚本,总有一款适合您
分享98个JS表单脚本,总有一款适合您
68 0
|
7月前
|
消息中间件 Web App开发 JavaScript
Node.js【简介、安装、运行 Node.js 脚本、事件循环、ES6 作业队列、Buffer(缓冲区)、Stream(流)】(一)-全面详解(学习总结---从入门到深化)
Node.js【简介、安装、运行 Node.js 脚本、事件循环、ES6 作业队列、Buffer(缓冲区)、Stream(流)】(一)-全面详解(学习总结---从入门到深化)
296 0
|
1月前
|
JSON 移动开发 JavaScript
在浏览器执行js脚本的两种方式
【10月更文挑战第20天】本文介绍了在浏览器中执行HTTP请求的两种方式:`fetch`和`XMLHttpRequest`。`fetch`支持GET和POST请求,返回Promise对象,可以方便地处理异步操作。`XMLHttpRequest`则通过回调函数处理请求结果,适用于需要兼容旧浏览器的场景。文中还提供了具体的代码示例。
在浏览器执行js脚本的两种方式