执行上下文
所有的代码都是在执行上下文中执行的,执行上下文是一个抽象的概念,它是一种规范,用来定义变量或函数有权访问的其他数据,决定它们的各自行为。
在深入研究前,需要了解一些概念:
- 编译器:负责语法分析和代码生成
- JavaScript 引擎:负责执行代码
- 函数声明:指的是使用 function 关键字声明的函数
- 函数表达式:指的是使用 var 关键字声明的函数
JavaScript 是如何被执行的
JavaScript 代码在执行之前,会经历三个阶段:
- 词法分析(Lexical Analysis)
- 语法分析(Syntactic Analysis)
- 代码生成(Code Generation)
词法分析阶段,会将代码分解成一个个词法单元,比如 var、function、if、else、for、while 等等。
语法分析阶段,会将词法单元转换成一个个语法单元,比如 var a = 1,会被转换成一个个词法单元,然后再转换成一个个语法单元,比如 var、a、=、1。
代码生成阶段,会将语法单元转换成可执行的代码,比如 var a = 1,会被转换成一个个语法单元,然后再转换成可执行的代码,比如 var a = 1。
JavaScript 中的执行上下文
JavaScript 中的执行上下文是一个抽象的概念,它是一种规范,用来定义变量或函数有权访问的其他数据,决定它们的各自行为。
执行上下文分为三种:
- 全局执行上下文
- 函数执行上下文
- eval 函数执行上下文
全局执行上下文
全局执行上下文是默认的执行上下文,它会在 JavaScript 代码执行之前被创建,它会被放到执行上下文栈中,当 JavaScript 代码执行完毕后,它会被从执行上下文栈中移除。
全局执行上下文包含三个部分:
- 变量对象(Variable Object,VO)
- 作用域链(Scope Chain)
- this
变量对象
变量对象是与执行上下文相关的数据作用域,存储了在上下文中定义的变量和函数声明。
变量对象包含两个部分:
- 函数的所有形参(如果是函数上下文)
- 函数声明
- 变量声明
变量对象的创建过程:
- 创建一个 VO
- 将所有形参,变量和函数声明添加到 VO 中
- 将 VO 赋值给活动对象(AO)
作用域链
作用域链是一个指向变量对象的指针列表,它只引用但不实际包含变量对象。
作用域链的创建过程:
- 创建一个作用域链
- 将当前执行上下文的变量对象添加到作用域链中
- 将父级执行上下文的变量对象添加到作用域链中
- 将父级的父级执行上下文的变量对象添加到作用域链中
- 依次类推,直到全局执行上下文的变量对象添加到作用域链中
this
this 是一个指针,指向函数执行时所在的作用域。
函数执行上下文
函数执行上下文是在函数被调用时被创建,它会被放到执行上下文栈中,当函数执行完毕后,它会被从执行上下文栈中移除。
JavaScript 中的提升
JavaScript 中的提升是指变量和函数的声明会被提升到当前作用域的顶部。
变量提升
变量提升是指变量的声明会被提升到当前作用域的顶部,但是变量的赋值不会被提升。
console.log(a) // undefined
var a = 1
函数提升
函数提升是指函数的声明会被提升到当前作用域的顶部,但是函数的赋值不会被提升。
console.log(a) // function a() {}
function a() {}
JavaScript 执行栈
JavaScript 执行栈是一个用于存储函数调用的栈结构,它遵循先进后出的原则。
当浏览器加载脚本,JS 引擎从全局上下文也就是默认上下文开始执行代码,所以全局上下文被放在执行栈的最底部。
然后 JS 引擎再搜索代码中被调用的函数。每一次函数被调用,一个新的 FEC 就会被创建,并被放置在当前执行上下文的上方。
执行栈最顶部的执行上下文会成为活跃执行上下文,并且始终是 JS 引擎优先执行。
一旦活跃执行上下文中的代码被执行完毕,JS 引擎就会从执行栈中弹出这个执行上下文,紧接着执行下一个执行上下文,以此类推。
为了了解执行栈的工作流,请考虑下面的代码:
var name = 'Victor'
function first() {
var a = 'Hi!'
second()
console.log(`${a} ${name}`)
}
function second() {
var b = 'Hey!'
third()
console.log(`${b} ${name}`)
}
function third() {
var c = 'Hello!'
console.log(`${c} ${name}`)
}
first()
解释以上代码的执行过程:
- 全局上下文被创建,全局上下文被压入执行栈中。
- 全局上下文初始化,创建变量对象、作用域链、this。
- 全局上下文代码被执行,遇到 first() 函数调用,创建 first() 函数执行上下文,first() 函数执行上下文被压入执行栈中。
- first() 函数执行上下文初始化,创建变量对象、作用域链、this。
- first() 函数执行上下文代码被执行,遇到 second() 函数调用,创建 second() 函数执行上下文,second() 函数执行上下文被压入执行栈中。
- second() 函数执行上下文初始化,创建变量对象、作用域链、this。
- second() 函数执行上下文代码被执行,遇到 third() 函数调用,创建 third() 函数执行上下文,third() 函数执行上下文被压入执行栈中。
- third() 函数执行上下文初始化,创建变量对象、作用域链、this。
- third() 函数执行上下文代码被执行,打印
Hello Victor
。 - third() 函数执行上下文被弹出执行栈。
- second() 函数执行上下文代码被执行,打印
Hey Victor
。 - second() 函数执行上下文被弹出执行栈。
- first() 函数执行上下文代码被执行,打印
Hi Victor
。 - first() 函数执行上下文被弹出执行栈。
- 全局上下文代码被执行,打印
Victor
。 - 全局上下文被弹出执行栈。
全局执行上下文对比函数执行上下文
全局执行上下文和函数执行上下文的区别在于:
- 全局执行上下文只有一个,但是函数执行上下文可以有很多个。
- 全局执行上下文在浏览器关闭时被销毁,但是函数执行上下文在函数执行完毕后被销毁。
参考
Comments
No Comments!