这边文章我们来学习JavaScript中的核心概念------------执行上下文。理解执行上下文是理解JavaScript代码如何运行(底层机制)的关键,对于弄懂作用域、闭包、变量提升等现象至关重要。
什么是执行上下文?
执行上下文是 JavaScript 代码被解析和执行时所在环境的抽象概念 。每当 JavaScript 引擎执行一段可执行代码时,都会创建一个对应的执行上下文。
你可以把它想象成一个包含代码运行所需所有信息的"容器"或"快照"。
执行上下文的类型
1. 全局执行上下文
- 默认的、最外层的上下文
- 在浏览器环境中,它关联着
window
对象。 - 一个程序中只会有一个全局执行上下文。
- 它会做两件事:① 创建一个全局对象(浏览器中为 window);② 将 this 指这个全局对象。
2. 函数执行上下文
- 每次调用函数时,都会为该函数创建一个新的函数执行上下文。
- 每个函数都有自己独立的执行上下文,即使是同一个函数被多次调用。
执行栈(调用栈 Call Stack)
执行上下文是如何管理的呢?答案是通过执行栈(也称为调用栈)。
- 执行栈是一个后进先出(LIFO)的栈结构,用于存储在代码执行期间创建的所有执行上下文。
- 当 JavaScript 引擎开始执行脚本时,它首先会创建一个全局执行上下文并将其压入栈底。
- 每当遇到一个函数调用,引擎就会为该函数创建一个新的函数执行上下文并将其压入栈顶。
- 引擎会执行位于栈顶的执行上下文中的代码。
- 当该函数执行完毕后,它的执行上下文会从栈中弹出,控制权交还给栈中的下一个(即之前的)执行上下文。
示例
javascript
let a = 'Hello';
function first() {
console.log('Inside first function');
second();
console.log('Again inside first function');
}
function second() {
console.log('Inside second function');
}
first();
console.log('Inside Global Execution Context');
执行栈的变化过程:
- 初始状态:栈为空。
- 脚本开始:创建全局执行上下文并压入栈。
- 调用 first():创建 first 的函数执行上下文并压入栈。
- 在 first() 中调用 second():创建 second 的函数执行上下文并压入栈。
- second() 执行完毕:second() 的函数执行上下文从栈中弹出。
- first() 执行完毕:first() 的函数执行上下文从栈中弹出。
- 脚本执行完毕:全局执行上下文从栈中弹出,栈清空。
图解: 输出结果:
text
Inside first function
Inside second function
Again inside first function
Inside Global Execution Context
执行上下文的生命周期
1. 创建阶段
2. 执行阶段
我们重点看创建阶段 ,因为变量提升等行为就发生在这里。在创建阶段,执行上下文会关联一个非常重要的对象------变量对象(Variable Object, VO) 。
对于全局上下文 和函数上下文,变量对象的细微差别导致了不同的行为:
- 全局上下文中的变量对象(VO):
- 其实就是全局对象本身(浏览器中是 window)。
- 所有在全局层面声明的变量和函数都成为全局对象(window)的属性。
- 函数上下文中的变量对象(VO):
- 它被称为活动对象(Activation Object, AO)。你可以认为在函数上下文中,VO 就是 AO。
- AO 除了包含变量和函数声明,还包含了 arguments 对象。
在创建阶段,引擎会扫描当前上下文中的代码,并完成以下工作:
- 创建变量对象(VO/AO):
- 处理函数声明:扫描并将所有函数声明添加到 VO/AO 中,并指向函数在内存中的引用。如果函数名已存在,则覆盖之前的引用。
- 处理变量声明:扫描并将所有通过 var 声明的变量添加到 VO/AO 中,并初始化为 undefined。如果变量名已存在,则跳过本次声明,防止覆盖函数。
- 处理参数(仅函数上下文):处理函数的参数。对于形参,将其添加到 AO 中并赋值为实参的值;如果没有实参,则赋值为 undefined。
这个过程就是大家常说的变量提升 的本质。函数声明会整体提升,而 var 变量只会声明提升,赋值操作留在原地。
不了解变量提升的本质可以看作者写的另一篇文章:JavaScript预编译机制详解
- 创建作用域链
- 作用域链是当前 VO/AO 和所有父级执行上下文的 VO/AO 的集合。它保证了当前执行上下文有权访问的所有变量和函数的有序访问。
- 作用域链是在函数定义时就确定的,而不是调用时(这就是词法作用域/静态作用域)。
- 确定 this 的指向:
- 在全局上下文中,this 指向全局对象(浏览器中是 window)。
- 在函数上下文中,this 的指向取决于函数是如何被调用的。
执行阶段:
- 创建阶段完成后,引擎开始按顺序一行一行执行代码。
- 此时,会根据代码逻辑,对 VO/AO 中的变量进行赋值。
- 如果执行过程中遇到新的函数调用,则重复上述过程:为该函数创建新的执行上下文,并压入执行栈。
示例:分解执行上下文的创建
javascript
function greet(name) {
var welcome = 'Hello ';
function sayHello() {
console.log(welcome + name);
}
return sayHello();
}
var message = greet('World');
分析 greet('World') 函数执行上下文的创建阶段:
- 创建 AO
- 处理参数:name: 'World'
- 处理函数声明:sayHello: <指向 sayHello 函数>
- 处理变量声明:welcome: undefined
此时,greet 的 AO 大致是:
javascript
AO = {
name: 'World',
sayHello: <reference to function>,
welcome: undefined
}
- 创建作用域链:
- greet 的作用域链是: [greet's AO] + [Global VO]
- sayHello 函数在定义时,其内部属性 [[Scope]] 就已经保存了这条作用域链。
- 确定 this:
- 在这个简单的调用中(greet('World')),this 通常指向全局对象(非严格模式)。
执行阶段:
然后引擎开始执行 greet 函数体内的代码:
- var welcome = 'Hello '; -> 将 AO 中的 welcome 从 undefined 赋值为 'Hello '。
- 执行 return sayHello(); -> 调用 sayHello 函数,从而为 sayHello 创建新的执行上下文,并压入栈中。