上面标记的答案将执行上下文比作堆栈帧。但是 JavaScript 中的 Execution Context 不是普通的 Stack Frame。
在全局执行上下文中,JavaScript 引擎为您创建了两个东西,一个全局对象(一个对象是名称/值对的集合)和一个名为“this”的特殊变量。在浏览器中,全局对象是一个窗口对象。在 NodeJS 中,全局对象是另外一回事。关键是总有一个全局对象。
当您创建不在其他函数内的变量和函数时,这些变量在全局上下文中,因此会附加到全局对象,在浏览器的情况下是窗口对象。
hello = 'hello world'
> "hello world"
window.hello
> "hello world"
JavaScript 中的执行上下文是分两个阶段创建的。第一阶段是创造阶段。在全局执行上下文中,全局对象被设置并且在内存中,特殊变量'this'被设置,指向全局对象并且在内存中,并且有一个外部环境(Lexical Environment)。当解析器开始执行上下文的创建阶段时,它首先会识别您创建变量和函数的位置。因此解析器为变量和函数设置了内存空间。此步骤称为“吊装”。因此,在您的特定代码逐行执行之前,JavaScript 引擎已经为您在全局执行上下文中创建的变量和函数留出了内存空间:
console.log(a);
console.log(b());
console.log(d);
var a = 'a';
function b(){
return 'called from b';
}
function c(){
d = 'd';
}
> undefined
> called from b
> Uncaught ReferenceError: d is not defined
在上面的例子中,由于变量 'a' 和函数 b() 是在全局执行上下文中创建的,因此为它们分配了内存空间。请注意,尽管变量没有初始化,只是用未定义的值声明。这不是函数的情况。函数都被声明和初始化,因此函数的标识符和实际代码都存储在内存中。另请注意,由于 d(即使未使用 var、let 或 const 指定)不在全局执行上下文中,因此没有为其分配内存空间。因此,当我们尝试访问 d 标识符时会引发异常。
现在,如果我们在引用 d 变量之前调用 c(),则会评估一个新的执行上下文(它不是全局执行上下文),然后 d 将在内存中(在那个新的执行上下文中,this 关键字将指向全局对象,因为我们没有在函数调用之前放置“new”,因此 d 将附加到全局对象):
console.log(a);
console.log(b);
console.log(c());
console.log(d);
var a = 'a';
function b(){
return 'called from b';
}
function c(){
d = 'd';
return 'called from c';
}
> undefined
> b() { return 'called from b' }
> called from c
> d
关于执行上下文的创建阶段的最后一点。由于发生了“提升”,因此函数定义或变量的顺序在词法范围方面并不重要。
执行上下文的第二个阶段称为执行阶段,这是发生分配的地方。 JavaScript 引擎开始解析您的代码。这就是变量将被赋值的时候。在第一阶段,它们只是被声明并以未定义的值存储在内存中。 'undefined' 是一个占位符,它是 JavaScript 表示“我还不知道这个值是什么”的方式。当你声明一个变量而不给它赋值时,这与 JavaScript 给出的占位符相同。因此,依赖 JavaScript 的“提升”功能并不是一个好主意。简单地说,在使用 var、const 或 let 声明变量之前,不要在全局执行上下文(或任何执行上下文)中使用变量。所以最好这样做:
var a = 'a';
function b(){
return 'called from b';
}
console.log(a);
console.log(b());
不要将自己与 JavaScript 内置数据类型“未定义”与解析器引发的未定义异常混淆。当变量没有在任何地方声明并且您尝试使用它时,JavaScript 引擎将引发异常'Uncaught ReferenceError: [variable] is not defined'。 JavaScript 说变量不在内存中。这与使用未定义数据类型初始化变量不同。
除了全局执行上下文之外,函数调用还会创建一个新的执行上下文。首先,在下面的示例中,创建了一个全局执行上下文。全局执行上下文的创建阶段将由 JavaScript 引擎处理。它将创建一个全局对象(浏览器中的窗口),并将创建一个特殊变量“this”并将其指向全局对象。然后 b 和 a 函数将附加到全局对象。将为它们分配内存空间,并且由于它们是函数,因此它们的代码也将存储在内存中。如果有任何变量,它们也将存储在内存中,但它们不会被初始化,因此以未定义的数据类型存储。然后执行阶段开始。由于 JavaScript 是单线程的,它会逐行执行。在此期间,它遇到 a()。它看到'()'并且知道它必须调用函数a。创建一个新的执行上下文并将其放置在执行堆栈上。您可能知道,堆栈数据结构是后进先出的。执行上下文被压入执行堆栈,完成后从堆栈中弹出。无论最上面的上下文是什么,这就是当前正在运行的执行上下文。
function b(){
}
function a(){
b();
}
a();
a() 执行上下文将堆叠在全局执行上下文之上。它将有自己的内存空间用于局部变量和函数。它将经历执行上下文的创建阶段和执行阶段。在 a() 执行上下文的创建阶段,由于它没有声明任何变量或函数,它不会为任何新的变量或函数分配空间。如果它确实声明了任何局部变量或函数,它将经历与全局执行上下文中相同的“提升”过程。此外,还会为该特定函数创建一个新的特殊“this”变量。请注意,如果您在没有 new 关键字的情况下调用该函数,那么 'this' 仍将引用全局对象,即浏览器中的窗口对象。
然后,它进入执行阶段。在这里它调用 b() 函数,现在创建了第三个执行上下文。这是 b() 执行上下文。它与其他执行上下文经历相同的创建和执行阶段。
当 b() 完成时,它会从堆栈中弹出,然后当 a() 完成时,它会从堆栈中弹出,然后我们返回到全局执行堆栈。重要的是,每个执行上下文都存储了一个指针,指向它在调用函数时停止的位置,从而创建了一个新的执行上下文。因此,当 b() 完成时,a() 返回到它调用 b() 的语句。然后继续执行该执行上下文中的下一条语句。同样,请记住,JavaScript 是单线程的,因此它会逐行执行。
关于词法环境的关键是它有一个链接到任何外部环境(即它的作用域链),所以它被用来解析当前执行上下文之外的标识符。最终,为每个执行上下文创建一个对应的词法环境。词法环境关心代码在你的应用程序中物理(词法)的位置。