Manon.icu

I'm here to make you a better developer by teaching you everything I know about building for the web.

Published 2020-05-19

执行上下文

所有的代码都是在执行上下文中执行的,执行上下文是一个抽象的概念,它是一种规范,用来定义变量或函数有权访问的其他数据,决定它们的各自行为。

在深入研究前,需要了解一些概念:

  • 编译器:负责语法分析和代码生成
  • 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 是一个指针,指向函数执行时所在的作用域。

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()

解释以上代码的执行过程:

  1. 全局上下文被创建,全局上下文被压入执行栈中。
  2. 全局上下文初始化,创建变量对象、作用域链、this。
  3. 全局上下文代码被执行,遇到 first() 函数调用,创建 first() 函数执行上下文,first() 函数执行上下文被压入执行栈中。
  4. first() 函数执行上下文初始化,创建变量对象、作用域链、this。
  5. first() 函数执行上下文代码被执行,遇到 second() 函数调用,创建 second() 函数执行上下文,second() 函数执行上下文被压入执行栈中。
  6. second() 函数执行上下文初始化,创建变量对象、作用域链、this。
  7. second() 函数执行上下文代码被执行,遇到 third() 函数调用,创建 third() 函数执行上下文,third() 函数执行上下文被压入执行栈中。
  8. third() 函数执行上下文初始化,创建变量对象、作用域链、this。
  9. third() 函数执行上下文代码被执行,打印 Hello Victor
  10. third() 函数执行上下文被弹出执行栈。
  11. second() 函数执行上下文代码被执行,打印 Hey Victor
  12. second() 函数执行上下文被弹出执行栈。
  13. first() 函数执行上下文代码被执行,打印 Hi Victor
  14. first() 函数执行上下文被弹出执行栈。
  15. 全局上下文代码被执行,打印 Victor
  16. 全局上下文被弹出执行栈。

AsLji6

全局执行上下文对比函数执行上下文

全局执行上下文和函数执行上下文的区别在于:

  1. 全局执行上下文只有一个,但是函数执行上下文可以有很多个。
  2. 全局执行上下文在浏览器关闭时被销毁,但是函数执行上下文在函数执行完毕后被销毁。

参考

  1. https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Function/Call
  2. JavaScript 执行上下文——JS 的幕后工作原理

Comments

No Comments!