JS 引擎对 JS 代码的处理包括解析和执行两个阶段。JS 代码的解析和执行并不是一次性完成的,而是交替进行的。当 JS 引擎执行代码时,它可能需要解析新的代码;反之,当它在解析阶段时,也可能会执行一些代码。
本章主要记录执行的过程,解析的过程在《浏览器基础及渲染引擎解析一个网页的过程、JavaScript 引擎解析 JavaScript 代码的过程》一章。
基础概念:
全局对象(Global Object、GO):
JS 引擎在解析 JS 代码时,会先在堆内存中创建一个全局对象,该全局对象默认包含一些全局的类和方法(例如:String、Math、Date、parseInt()
、setTimeout()
等),其中还有一个 window 属性指向自身。所有的作用域都可以访问它。
在浏览器中,Global Object 就是 Window 对象。
执行上下文(Execution Context,GE):
JS 代码要想执行,必须要先为其创建一个执行上下文。执行上下文就是当前代码的执行环境,当 JS 代码执行的时候,会进入不同的执行环境。
执行上下文的类型:
JS 中有三种类型的执行上下文:
- 全局执行上下文(Global Execution Context,GEC): 在执行全局的代码前,JS 引擎会创建一个全局执行上下文;直到应用程序退出(例如:关闭网页或浏览器),全局执行上下文才会被销毁。一个程序中只会有一个全局执行上下文。
- 函数执行上下文(Functional Execution Context,FEC):每当函数被调用时,都会为该函数创建一个新的函数执行上下文;当函数执行完毕后,函数执行上下文被销毁。函数执行上下文可以有无数个。
多次调用同一个函数会创建多个不同的函数执行上下文。
- eval 函数执行上下文:eval 函数内部的代码也有属于它自己的执行上下文。很少用到。
执行上下文中三个重要的概念:
-
变量对象(Variable object,VO):每个执行上下文都会关联一个变量对象。在当前执行上下文中定义的所有变量和函数都会保存在这个对象中。
-
作用域链(Scope Chain):每个执行上下文都会关联一个作用域链。作用域链是变量对象组成的一个对象列表,作用域链的前端,始终都是当前执行上下文的变量对象,下一个变量对象来自包含环境,而再下一个变量对象则来自下一个包含环境,一直延续到全局执行上下文的变量对象。
作用域链用来查找变量和函数,保证对访问的变量和函数的有序访问。当查找一个变量或函数时,都是从作用域链的前端开始,一级一级地向后查找,如果一直找到全局对象上都没有找到,就会报错。
有些语句可以在作用域链的前端临时增加一个变量对象,作用域链就会得到加长,该变量对象会在代码执行后被移出。对 with 语句来说,会将指定的对象添加到作用域链的最前端;对 catch 语句来说,被抛出的错误对象会创建一个新的变量对象加到作用域的最前端。
var obj = { message: 'Hello' } with(obj) { console.log(message) // Hello。message 所在执行执行上下文的作用链现在为 [obj, GO] }
var num = 1 function out(){ var num = 2 inner() } function inner(){ console.log(num) } out() // 1
inner()
函数的作用域链的前端是它自己执行上下文的变量对象,下一个变量对象是来自它的包含环境全局执行上下文。
inner()
函数的作用域链的前端是它自己执行上下文的变量对象,下一个变量对象是来自它的包含环境outer()
函数执行上下文。var num = 1 function out(){ var num = 2 function inner(){ console.log(num) } return inner } out()() // 2
作用域是变量或函数可以被访问的范围。可以分为全局作用域、函数作用域和块级作用域(ES6 中新增)。作用是隔离变量,不同作用域下同名变量不会有冲突。
ES6 之前,if 语句、for 语句等的大括号没有封闭作用域的功能,都是全局作用域。if (true) { var box=’blue’ } console.log(box) // blue
例如:
使用 var 在全局声明的变量,有全局作用域,可以在全局访问。
如果在一个函数内部访问它,就会沿着它所在执行上下文中的作用域链,先在函数执行上下文的变量对象中查找,找不到再去全局执行上下文的变量对象中查找。
也就是说,作用域是一个变量或函数的,是它们可以被访问的范围;作用域链是一个变量或函数所在执行上下文的,是访问它们的一个链条。 -
this 对象。
执行上下文栈(Execution Context Stack,GES)::
JS 引擎内部有一个执行上下文栈,它是用于执行代码的调用栈,被用来存储和管理代码运行时创建的所有执行上下文,拥有 LIFO(后进先出)的数据结构。
执行上下文用来存放和真正执行代码。
执行上下文栈用来调用执行上下文。
当 JS 引擎执行 JS 脚本时,它首先会创建一个全局的执行上下文并且压入执行上下文栈;每当 JS 引擎遇到一个函数调用,它会为该函数创建一个新的执行上下文并压入执行上下文栈的顶部;当该函数执行结束时,执行上下文从执行上下文栈中弹出,控制流程到达执行上下文栈的下一个执行上下文。
全局执行上下文总是在栈的底部;当前运行的执行上下文总是栈顶的那个执行上下文。
let str = 'javascript'
function foo() {
console.log('foo')
bar()
}
function bar() {
console.log('bar')
}
foo()
- 当上述代码在浏览器中运行时,JS 引擎首先会创建一个全局执行上下文并把它压入执行上下文栈。
- 当遇到
foo()
函数调用时, JS 引擎创建了一个 foo 函数的执行上下文并把它压入到执行上下文栈的顶部。 - 当从
foo()
函数内部调用bar()
函数时,JS 引擎创建了一个 bar 函数的执行上下文并把它压入到执行上下文栈的顶部。 - 当
bar()
函数执行完毕,它的执行上下文会从执行上下栈中弹出,控制流程到达下一个执行上下文,即foo()
函数的执行上下文。 - 当
foo()
函数执行完毕,它的执行上下文从执行上下栈中弹出,控制流程到达全局执行上下文。 - 全局执行上下文直到应用程序退出,才会被销毁。
JS 引擎执行代码的过程:
var message = 'Global Message'
// 定义函数
function foo(num) {
var message = 'Foo Message'
}
// 调用函数
foo(1)
var num1 = 10
var num2 = 20
var result = num1 + num2
console.log(result)
JS 引擎执行全局代码的过程:
-
JS 引擎在解析 JS 代码时,会先在堆内存中创建一个全局对象,将声明的全局变量、声明的全局函数等标识符添加到该全局对象中。
在从上到下解析全局代码的过程中:
- 遇到声明的全局变量:将该变量添加到 GO 对象中,并默认赋值为 undefined。
- 遇到声明的全局函数:将该函数添加到 GO 对象中,并将函数提前创建出来。
这就是变量提升和函数提升的原因。
-
为全局代码创建一个全局执行上下文。
- 每个执行上下文都会关联一个 VO 对象,当进入全局执行上下文时,将全局执行上下文关联的 VO 指向 GO。
- 创建作用域链,全局执行上下文的作用域链中只有 GO。
- 确定 this 的指向:this 指向的就是全局对象。
-
将全局执行上下文压入执行上下文栈后,从上往下开始执行全局代码。
- 为声明的全局变量赋值。
- 由于声明的全局函数在解析阶段已经提前创建好了,会直接跳过。
- 执行其他的逻辑代码。
JS 引擎执行函数代码的过程:
- 当执行到一个函数时,会根据函数体创建一个函数执行上下文。
-
当进入函数执行上下文时,会创建一个AO 对象(Activation Object),使用函数的 arguments 来作为其初始化的值,将函数的形参、内部声明的局部变量、内部声明的函数添加到该 AO 对象中。然后将函数执行上下文关联的 VO 指向 AO。
在从上到下解析函数代码的过程中:
- 遇到函数的形参:将形参添加到 AO 对象中,并默认赋值为 undefined。
- 遇到声明的局部变量:将该变量添加到 AO 对象中,并默认赋值为 undefined。
- 遇到声明的函数:将该函数添加到 AO 对象中,并将函数提前创建出来。
这就是变量提升和函数提升的原因。
-
创建作用域链。
-
确定 this 的指向:this 的指向取决于该函数是如何被调用的。
-
- 将函数执行上下文压入执行上下文栈后,从上往下开始执行函数代码。
- 将实参赋值给形参。
- 为声明的变量赋值。
- 由于声明的函数在解析阶段已经提前创建好了,会直接跳过。
- 执行其他的逻辑代码。
- 函数执行完毕,函数执行上下文从执行上下文栈中弹出。