什么是执行上下文
JavaScript是一种客户端脚本语言,通常在Web浏览器中执行。当您在浏览器中加载网页时,浏览器会解析HTML文档并创建文档对象模型 (DOM)。在这个过程中,浏览器会寻找包含JavaScript代码的script标签并执行这些代码。
当浏览器执行JavaScript代码时,会将代码提交给JavaScript引擎进行解析。引擎会将代码转化为由许多小步骤组成的一个指令序列,然后依次执行这些步骤。这个过程被称为执行上下文(execution context)。
执行上下文(Execution context)是 JavaScript 引擎在执行代码时的内部概念,用于描述当前代码的运行环境。执行上下文包含了当前代码的变量、函数和对象的作用域,以及 this 的值。每当 JavaScript 引擎开始执行一段代码时,就会创建一个新的执行上下文,并将其压入执行栈(Execution stack)中。
执行上下文的类型
JavaScript 有三种执行上下文:全局执行上下文(Global execution context)和函数执行上下文(Function execution context),Eval函数执行上下文三种类型。
全局执行上下文是 JavaScript 程序开始运行时创建的第一个执行上下文,它代表全局作用域,即在任何函数外部定义的变量都是全局变量。在全局执行上下文中,this 的值是 window 对象。
函数执行上下文是在调用函数时创建的执行上下文,它代表函数的作用域。在函数执行上下文中,this 的值取决于函数的调用方式。
在js代码中,一个程序是由多个代码片段组成的,有些代码片段会有自己独特的功能和处理逻辑以及独立的运行环境,在函数执行时,js引擎为每一个被调用的函数创建的一个新的代码运行环境(即新的执行上下文),对于一段js脚本来说,它可能会有很多个函数,并且互相之间是互相嵌套的,那他们的执行顺序会像套娃一样一层套一层,执行时也会遵循LIFO(后进先出)的机制进行执行,这种数据结构的栈被称为执行栈,也就是控制代码执行顺序的地方。
执行栈(Execution stack)是 JavaScript 引擎用于维护当前执行的代码的一种数据结构。当 JavaScript 引擎开始执行一段代码时,会创建一个新的执行上下文并将其压入执行栈顶部。当前正在执行的代码始终位于栈顶,执行完毕后会弹出栈顶的执行上下文。
创建执行上下文的过程叫做执行上下文的入栈(Push)。当 JavaScript 引擎遇到 return 语句或者执行完毕时,会执行执行上下文的出栈(Pop)操作,即将当前执行上下文从执行栈中弹出。
下面是一个示例,展示了在全局执行上下文和函数执行上下文之间切换时执行栈的变化:
function foo() {
console.log('foo');
}
function bar() {
foo();
console.log('bar');
}
console.log('global');
bar();
在执行上述代码时,JavaScript 引擎会创建三个执行上下文,分别对应全局作用域、函数 foo 的作用域和函数 bar 的作用域。执行栈的变化如下:
- 创建全局执行上下文并入栈。执行 console.log(‘global’)。
- 创建函数 bar 的执行上下文并入栈。
- 创建函数 foo 的执行上下文并入栈。执行 console.log(‘foo’)。函数 foo 的执行上下文出栈。
- 执行 console.log(‘bar’)。函数 bar 的执行上下文出栈。
注意,JavaScript 是单线程的,所以执行栈中只会存在一个执行上下文。同一时刻,只有栈顶的执行上下文会被执行,其余的执行上下文都会被暂停。
Eval函数执行上下文,一种特殊的函数执行上下文,但有区别与函数执行上下文,在js中eval()函数会将传入的字符串当作js代码执行,因此js引擎也就会为其创建独立的运行环境。
执行上下文生命周期
执行上下文的生命周期指的是从创建执行上下文到销毁执行上下文的过程。执行上下文的生命周期可以分为三个阶段:创建、执行和销毁。
- 创建阶段
在创建阶段,执行上下文被创建并初始化。 创建阶段是在代码即将运行之前,也就是代码被解析之前的过程,主要任务包括确定 this 的值(this 绑定)、创建词法环境和创建变量环境(设置作用域链)。
在全局执行上下文中,this 指向全局对象(如浏览器中的 window 对象)。在函数执行上下文中,this 的值取决于函数的调用方式,可以是默认绑定、隐式绑定、显式绑定、new 绑定或箭头函数绑定。
词法环境是一种根据 ECMAScript 代码的词法嵌套结构来定义标识符和具体变量和函数的关联的规范类型。它由环境记录器和一个可能的外部词法环境的引用组成。环境记录器存储了当前环境中的变量和函数声明的实际位置,而对外部环境的引用则允许访问其外部词法环境。词法环境有两种类型:全局环境和函数环境。全局环境是没有外部变量环境是一种规范类型,用于描述变量的作用域链。它由一个环境记录器和可能的外部变量环境的引用组成。环境记录器存储了当前环境中的变量声明的实际位置,而对外部环境的引用则允许访问其外部变量环境。
变量环境与词法环境之间的关系是,变量环境是通过词法环境来实现的。每个执行上下文都有一个词法环境和一个变量环境。在创建阶段,词法环境被创建并将其赋值给执行上下文的 LexicalEnvironment 属性,变量环境被创建并将其赋值给执行上下文的 VariableEnvironment 属性。
- 执行阶段
执行阶段是指在代码已经解析完成并准备运行时的过程 。 在执行阶段,执行上下文会根据词法环境和变量环境来执行代码,执行上下文会维护执行过程中的状态,例如变量的值和执行的位置。
在执行阶段,当执行到一个声明语句(如变量声明或函数声明)时,会将声明的标识符添加到变量环境中。当执行到一个表达式(如赋值操作或函数调用)时,会在变量环境中查找标识符的值。如果找不到,会继续在外部变量环境中查找,直到找到为止,或者如果找不到就会抛出一个 ReferenceError 错误。
执行阶段的主要过程如下:
- 从执行栈的顶部开始执行代码。
- 在执行过程中,当遇到一个函数调用时,会创建一个新的执行上下文并将其加入执行栈的顶部。
- 在执行过程中,当遇到一个 return 语句时,会终止当前执行上下文并从执行栈中弹出。
- 在执行过程中,当执行栈为空时,代码执行完毕。
- 销毁阶段
在销毁阶段,执行上下文被销毁。这包括清除执行上下文中的信息,并释放相关的资源。
执行上下文的生命周期是动态的,并且取决于代码的执行情况。例如,如果代码中有一个函数调用,那么会创建一个新的执行上下文来执行函数体,并在函数执行完后销毁这个执行上下文。
在 JavaScript 中,执行上下文是动态的,并且在代码执行过程中不断地创建和销毁。以下是一个简单的示例,展示了如何创建、执行和销毁执行上下文:
function greet(name) {
console.log(`Hello, ${name}!`);
}
greet('John'); // 创建执行上下文,执行函数体,销毁执行上下文
在这个示例中,当函数 greet
被调用时,会创建一个新的执行上下文来执行函数体。执行上下文会维护函数的参数和局部变量,并在函数执行完后销毁。
执行上下文还可以嵌套,例如:
function outer() {
function inner() {
console.log('I am inside!');
}
inner(); // 创建执行上下文,执行函数体,销毁执行上下文
}
outer(); // 创建执行上下文,执行函数体,销毁执行上下文
在这个示例中,当函数 outer
被调用时,会创建一个新的执行上下文来执行函数体。当函数 inner
被调用时,又会创建一个新的执行上下文来执行函数体。最后,这两个执行上下文都会在执行完后销毁。
变量对象 作用域
执行上下文的另一个重要概念是变量对象(Variable object)。变量对象是执行上下文中存储变量和函数声明的对象。在全局执行上下文中,变量对象是 window 对象;在函数执行上下文中,变量对象是 arguments 对象。
变量对象有一个属性叫做作用域链(Scope chain),用于描述执行上下文中可访问的变量和函数。 作用域是指在代码中变量和函数可以被访问的范围。在 ECMAScript 中,作用域是以词法方式实现的,也就是说,在代码中写的位置就决定了变量和函数的作用域。 作用域链是一个从内到外的双向链表,其中第一个节点是变量对象本身,每个后继节点都是一个对象。在全局执行上下文中,作用域链只包含一个节点,即变量对象本身;在函数执行上下文中,作用域链包含变量对象和它的所有父级函数的变量对象。
ECMAScript 中有两种作用域:全局作用域和局部作用域。全局作用域是最外层的作用域,它包含了整个代码。局部作用域是在函数内部创建的作用域,它只包含了函数内部的代码。
在 ECMAScript 中,变量的作用域是在它被声明的地方确定的。如果一个变量在全局作用域中被声明,那么它就是全局变量,可以在整个代码中访问。如果一个变量在函数作用域中被声明,那么它就是局部变量,只能在函数内部访问。
函数的作用域也是在它被声明的地方确定的。如果一个函数在全局作用域中被声明,那么它就是全局函数,可以在整个代码中访问。如果一个函数在函数作用域中被声明,那么它就是局部函数,只能在函数内部访问。
在 ECMAScript 中,作用域链是一个由作用域组成的链表,用于查找变量和函数的值。当查找一个标识符的值时,会按照从内到外的顺序查找作用域链中的每一个作用域。如果在作用域链中找到了标识符,则返回其值;如果没有找到,则抛出一个 ReferenceError 错误。
在全局执行上下文中,作用域链只包含一个作用域,即全局作用域。在函数执行上下文中,作用域链包含多个作用域。首先是函数作用域,然后是包含函数的作用域,以此类推直到全局作用域。
下面是一个示例,展示了在全局执行上下文和函数执行上下文中的变量对象和作用域链的变化:
var x = 1;
function foo() {
var y = 2;
console.log(x); // 1
console.log(y); // 2
}
foo();
在执行上述代码时,JavaScript 引擎会创建两个执行上下文,分别对应全局作用域和函数 foo 的作用域。
在全局执行上下文中,变量对象是 window 对象,作用域链只包含 window 对象本身。所以在全局执行上下文中,可以访问全局变量 x。
在函数 foo 的执行上下文中,变量对象是 arguments 对象,作用域链包含 arguments 对象和 window 对象。所以在函数 foo 的执行上下文中,可以访问局部变量 y 和全局变量 x。
另外,JavaScript 还有一种特殊的执行上下文——严格模式执行上下文(Strict mode execution context)。严格模式下,JavaScript 引擎会对代码进行更严格的检查,并会改变 this 的默认值。可以在函数或者全局作用域中使用 “use strict” 指令启用严格模式。
例如,在严格模式下,this 的默认值为 undefined,而不是非严格模式下的 window 对象。
下面是一个示例,展示了在严格模式和非严格模式下 this 的取值:
function foo() {
console.log(this);
}
foo(); // window (非严格模式) / undefined (严格模式)