在JavaScript中:理解奇怪的部分词法环境被解释为代码的范围,而执行上下文是词汇环境的集合,并且它包含超出编写代码的内容.
这些术语的描述在功能上仍然存在重叠,并且不清楚执行上下文的作用或执行方式.
执行上下文和执行上下文堆栈:执行上下文是用于跟踪函数或全局代码执行的内部 javascript 构造。js 引擎维护了一个栈-执行上下文栈或调用栈,其中包含这些上下文,全局执行上下文停留在这个栈的底部。当函数开始执行时,会创建一个新的执行上下文并将其推送到堆栈中。特定的执行上下文跟踪正在执行相应函数的语句的指针。当相应函数的执行完成时,执行上下文从堆栈中弹出。
词法环境:它是保存标识符变量映射的内部 js 引擎构造。(这里标识符是指变量/函数的名称,变量是对实际对象[包括函数类型对象]或原始值的引用)。词法环境还持有对父词法环境的引用。
现在,对于每个执行上下文——1) 创建一个相应的词法环境 2) 如果在该执行上下文中创建了任何函数,则对该词法环境的引用存储在该函数的内部属性 ( [[Environment]] ) 中. 因此,每个函数都会跟踪与其创建的执行上下文相关的词法环境。
并且每个词法环境都跟踪其父词法环境(父执行上下文的词法环境)。因此,每个函数都有一个附加的词法环境链。【注:在js中函数就是对象,通过语句创建函数就是创建一个Function类型的对象。所以像其他对象一样,一个函数可以保存内部和用户定义的属性]
js 引擎搜索当前词法环境中的任何变量或函数标识符,如果没有找到,它将搜索附加到封闭函数的链。(对于全局代码,此链不存在)。因此,您了解如何维护变量和函数的作用域。闭包的概念也得到了这个链的支持(不是标准术语,我只是为了便于理解而使用它)。当一个函数实例作为参数传递给另一个函数以用作回调时,它会携带它的链(有点)。
注意:答案基于我从“Javascript Ninja 的秘密,2/e”中学到的知识
上面标记的答案将执行上下文比作堆栈帧。但 JavaScript 中的执行上下文并不是普通的堆栈框架。
在全局执行上下文中,JavaScript 引擎为您创建两个东西,一个全局对象(对象是名称/值对的集合)和一个名为“this”的特殊变量。在浏览器中,全局对象是一个窗口对象。在 NodeJS 中,全局对象是另外一回事。关键是总有一个全局对象。
当您创建不在其他函数内部的变量和函数时,这些变量位于全局上下文中,因此会附加到全局对象,在浏览器中是窗口对象。
hello = 'hello world'
> "hello world"
window.hello
> "hello world"
Run Code Online (Sandbox Code Playgroud)
JavaScript 中的执行上下文是分两个阶段创建的。第一阶段是创建阶段。在全局执行上下文中,全局对象被设置并在内存中,特殊变量'this'被设置,指向全局对象并在内存中,并且有一个外部环境(词法环境)。当解析器开始执行上下文的创建阶段时,它首先识别您创建变量和函数的位置。因此解析器为变量和函数设置内存空间。此步骤称为“提升”。因此,在逐行执行特定代码之前,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
Run Code Online (Sandbox Code Playgroud)
在上面的示例中,由于变量“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
Run Code Online (Sandbox Code Playgroud)
关于执行上下文的创建阶段的最后一点。由于发生“提升”,函数定义或变量的顺序在词法作用域方面并不重要。
执行上下文的第二阶段称为执行阶段,这是分配发生的地方。JavaScript 引擎开始解析您的代码。这就是变量被赋值的时候。在第一阶段,它们只是被声明并以未定义的值存储在内存中。“undefined”是一个占位符,JavaScript 表示“我还不知道这个值是什么”。这与声明变量而不为其赋值时 JavaScript 给出的占位符相同。因此,依赖 JavaScript 的“提升”功能并不是一个好主意。简而言之,在用 var、const 或 let 声明变量之前,不要在全局执行上下文(或任何执行上下文)中使用变量。所以最好这样做:
var a = 'a';
function b(){
return 'called from b';
}
console.log(a);
console.log(b());
Run Code Online (Sandbox Code Playgroud)
不要将 JavaScript 内置数据类型“未定义”与解析器引发的未定义异常混淆。当变量未在任何地方声明并且您尝试使用它时,JavaScript 引擎将引发异常“未捕获的引用错误:[变量]未定义”。JavaScript 说该变量不在内存中。这与使用未定义的数据类型初始化变量不同。
除了全局执行上下文之外,函数调用还会创建一个新的执行上下文。首先,在下面的示例中,创建了一个全局执行上下文。全局执行上下文的创建阶段将由 JavaScript 引擎处理。它将创建一个全局对象(在浏览器中为窗口),并将创建一个特殊变量“this”并将其指向全局对象。然后 b 和 a 函数将被附加到全局对象。将为它们分配内存空间,并且由于它们是函数,因此它们的代码也将存储在内存中。如果有任何变量,它们也会存储在内存中,但它们不会被初始化,因此以未定义的数据类型存储。然后执行阶段开始。由于 JavaScript 是单线程的,它会逐行执行。在此期间,它遇到了a()。它看到“()”并知道它必须调用函数 a。创建一个新的执行上下文并将其放置在执行堆栈上。您可能知道,堆栈数据结构是后进先出的。执行上下文被压入执行堆栈,完成后从堆栈中弹出。无论顶部的上下文是什么,都是当前正在运行的执行上下文。
function b(){
}
function a(){
b();
}
a();
Run Code Online (Sandbox Code Playgroud)
a() 执行上下文将堆叠在全局执行上下文之上。它将有自己的局部变量和函数的内存空间。它将经历执行上下文的创建阶段和执行阶段。在a()执行上下文的创建阶段,由于它没有声明任何变量或函数,因此它不会为任何新变量或函数分配空间。如果它确实声明了任何局部变量或函数,它将经历与全局执行上下文中的情况相同的“提升”过程。此外,还会为该特定函数创建一个新的特殊“this”变量。请注意,如果您在不使用 new 关键字的情况下调用该函数,则“this”仍将引用全局对象,即浏览器中的 window 对象。
然后,它进入执行阶段。这里它调用 b() 函数,现在创建了第三个执行上下文。这是 b() 执行上下文。它经历与其他执行上下文相同的创建和执行阶段。
当b()完成时,它会从堆栈中弹出,然后当a()完成时,它也会从堆栈中弹出,然后我们返回到全局执行堆栈。重要的是,每个执行上下文都存储一个指针,指向它调用函数时停止的位置,从而创建一个新的执行上下文。因此,当 b() 完成时,a() 返回到调用 b() 的语句。然后继续执行该执行上下文中的下一条语句。再次记住,JavaScript 是单线程的,因此它是逐行执行的。
词法环境的关键是它具有到任何外部环境(即其作用域链)的链接,因此它用于解析当前执行上下文之外的标识符。最终,为每个执行上下文创建相应的词法环境。词法环境关心代码在应用程序中的物理(词法)位置。