JavaScript中的执行上下文和堆栈是什么?

更新日期: 2019-04-26阅读: 1.6k标签: 堆栈

在这篇文章中,我将深入研究JavaScript最基本的部分之一,即执行上下文。在这篇文章的最后,您应该更清楚地了解解释器要做什么,为什么在声明一些函数/变量之前可以使用它们,以及它们的值是如何确定的。


什么是执行上下文?

代码在JavaScript中运行时,执行它的环境是非常重要的,并被评估为以下之一:

1:全局代码——第一次执行代码的默认环境。

2:函数代码——每当执行流进入函数体时。

3:要在内部Eval函数中执行的文本。

您可以在线阅读大量参考资料,其中涉及scope ,本文的目的是使事情更容易理解,让我们将术语 execution context(执行上下文) 看作当前代码正在计算的环境/范围。现在,讨论得够多了,让我们来看一个包含 global 和 function / local 上下文计算代码的示例。

这里没什么特别的,我们有1个 global context 用紫色边框表示,3个不同的function contexts 用绿色、蓝色和橙色边框表示。只能有一个 global context ,它可以从程序中的任何其他上下文访问。

您可以有任意数量的 function contexts ,并且每个函数调用都创建一个新的上下文,该上下文创建一个私有范围,其中函数内部声明的任何内容都不能从当前函数范围外部直接访问。在上面的例子中,一个函数可以访问当前上下文之外声明的变量,但是外部上下文不能访问其中声明的变量/函数。为什么会这样?这段代码究竟是如何计算的?


执行上下文堆栈

浏览器中的JavaScript解释器是作为一个线程实现的。这实际上意味着,在浏览器中,一次只能发生一件事,其他操作或事件将排队在所谓的执行堆栈中。下图是单线程栈的抽象视图:

我们已经知道,当浏览器第一次加载脚本时,默认情况下它会进入 global execution context 。如果在全局代码中调用一个函数,程序的序列流将进入被调用的函数,创建一个新的 execution context 并将该上下文推到 execution stack 的顶部。

如果在当前函数中调用另一个函数,也会发生同样的事情。代码的执行流进入内部函数,该函数创建一个新的 execution context ,并将其推到现有堆栈的顶部。浏览器将始终执行位于堆栈顶部的当前 execution context ,一旦函数执行完当前 
execution context ,它将从堆栈顶部弹出,将控制权返回到当前堆栈中下面的上下文。下面的例子展示了一个递归函数和程序的 execution stack :

(function foo(i) {
    if (i === 3) {
        return;
    }
    else {
        foo(++i);
    }
}(0));

代码简单地调用自身3次,将i的值增加1。每次调用函数foo时,都会创建一个新的执行上下文。一旦上下文执行完毕,它就会从堆栈中弹出并返回到它下面的上下文,直到再次到达 global context 为止。

关于执行堆栈,有5个关键点需要记住:

1:单线程的。

2:同步执行。

3:1个全局上下文。

4:无限的函数上下文。

5:每个函数调用都会创建一个新的执行上下文,甚至是对自身的调用。


详细执行上下文

现在我们知道,每次调用一个函数,都会创建一个新的 execution context 。然而,在JavaScript解释器中,对 execution context 的每个调用都有两个阶段:

1:创建阶段[当函数被调用,但在执行任何代码之前]:

创建范围链。

创建变量、函数和参数。

确定“this”的值。

2:激活/代码执行阶段:

为函数赋值、引用并解释/执行代码。

可以将每个 execution context (执行上下文)概念上表示为一个具有3个属性的对象:

executionContextObj = {
    'scopeChain': { /* 变量对象+所有父执行上下文的变量对象 */ },
    'variableObject': { /* 函数参数/参数,内部变量和函数声明 */ },
    'this': {}
}

激活/变量对象[AO/VO]

这个 executionContextObj 在调用函数时创建,但在实际函数执行之前创建。这被称为阶段1,创建阶段。在这里,解释器通过扫描函数寻找传入的参数或参数、局部函数声明和局部变量声明来创建executionContextObj 。该扫描的结果成为executionContextObj 中的variableObject。

下面是解释器如何评估代码的伪概述:

找到一些代码来调用函数。

在执行函数代码之前,创建执行上下文。

进入创作阶段:

初始化范围链。

创建变量对象:

创建arguments对象,检查参数上下文,初始化名称和值,并创建引用副本。

扫描上下文中的函数声明:

对于找到的每个函数,在变量对象中创建一个属性,该属性是确切的函数名,该函数在内存中有一个指向该函数的引用指针。

如果函数名已经存在,则重写引用指针值。

扫描上下文变量声明:

对于找到的每个变量声明,在变量对象中创建一个属性,即变量名,并初始化值为undefined。

如果变量名已经存在于变量对象中,则什么也不做,继续扫描。

确定上下文中“this”的值。

激活/代码执行阶段:

在上下文中运行/解释函数代码,并在逐行执行代码时分配变量值。

让我们来看一个例子:

function foo(i) {
    var a = 'hello';
    var b = function privateB() {

    };
    function c() {

    }
}

foo(22);

调用foo(22)时,创建阶段如下:

fooExecutionContext = {
    scopeChain: { ... },
    variableObject: {
        arguments: {
            0: 22,
            length: 1
        },
        i: 22,
        c: pointer to function c()
        a: undefined,
        b: undefined
    },
    this: { ... }
}

如您所见,创建阶段处理定义属性的名称,而不是为它们赋值,只有形式参数/参数例外。创建阶段完成后,执行流程进入函数,函数完成执行后,激活/代码执行阶段如下:

fooExecutionContext = {
    scopeChain: { ... },
    variableObject: {
        arguments: {
            0: 22,
            length: 1
        },
        i: 22,
        c: pointer to function c()
        a: 'hello',
        b: pointer to function privateB()
    },
    this: { ... }
}


关于吊装的说明

您可以在网上找到许多用JavaScript定义术语提升的资源,解释变量和函数声明被提升到函数作用域的顶部。但是,没有人详细解释为什么会发生这种情况,而且有了解释器如何创建 activation object(激活对象)的新知识,就很容易理解为什么会发生这种情况。以下面的代码为例:

​(function() {

    console.log(typeof foo); // function pointer
    console.log(typeof bar); // undefined

    var foo = 'hello',
        bar = function() {
            return 'world';
        };

    function foo() {
        return 'hello';
    }

}());​

我们现在可以回答的问题是:

为什么我们可以在声明foo之前访问它?

如果我们遵循创建阶段,我们就知道在激活/代码执行阶段之前已经创建了变量。因此,当函数流开始执行时,foo已经在激活对象中定义。

Foo声明了两次,为什么Foo是函数而不是未定义或字符串?

尽管foo声明了两次,但从创建阶段我们就知道函数是在变量之前在激活对象上创建的,如果激活对象上的属性名已经存在,那么我们只需绕过解密。

因此,首先在激活对象上创建对函数foo()的引用,当解释器到达var foo时,我们已经看到了属性名foo的存在,所以代码什么也不做,继续执行。

为什么bar没有定义?

bar实际上是一个具有函数赋值的变量,我们知道这些变量是在创建阶段创建的,但是它们是用undefined值初始化的。


总结

希望现在您已经很好地理解了JavaScript解释器是如何评估代码的。理解执行上下文和堆栈可以让您了解代码为什么要计算您最初没有预料到的不同值的原因。

您是否认为了解解释器的内部工作方式对您的JavaScript知识来说是太大的开销还是必需的?了解执行上下文阶段是否有助于编写更好的JavaScript ?


原文:http://davidshariff.com/blog/what-is-the-execution-context-in-javascript/#first-article

链接: https://www.fly63.com/article/detial/3040

如何优雅地查看 JS 错误堆栈?

在前端,我们经常会通过 window.onerror 事件来捕获未处理的异常。假设捕获了一个异常,上报的堆栈是这个:这个堆栈,你看得出问题来吗?我们发布到 CDN 的脚本文件,普遍是经过 UglifyJS 压缩的,所以堆栈可读性相当的差。

连续赋值(从堆栈角度解析) a.x = a = {n:2}

今天看到一个面试题,一直想把这个题目解析更加直观化,就跟看小人书一样,看图就能明白其中的原理,所以用PPT做了几张图。接下来我们从以下几点分析以下:

js中的堆和栈

栈(stack):栈会自动分配内存空间,会自动释放,存放基本类型,简单的数据段,占据固定大小的空间;堆(heap):动态分配的内存,大小不定也不会自动释放

5 张图描绘Web3 堆栈全景

Web3 堆栈最令人难以置信的一点是,它们不需要任何集中协调就可以组合在一起。开发本身是去中心化的。没有主架构师。这与地球上几乎所有其他的开发堆栈项目形成了鲜明的对比。在 Linux 基金会,少数人设定整个 Linux 的方向

异步堆栈追踪:为什么 await 胜过 Promise?

与直接使用 Promise 相比,使用 async/await 不仅可以使代码更具可读性,而且还可以在 JavaScript 引擎中实现一些有趣的优化。这篇文章是关于一个这样的优化,涉及异步代码的堆栈追踪。

使用 JavaScript 的数据结构:堆栈和队列

Web 开发中最常用的两种数据结构是堆栈和队列。许多 Internet 用户,包括 Web 开发人员,都没有意识到这一惊人的事实。如果您是这些开发人员中的一员,那么请准备好两个具有启发性的示例:文本编辑器的撤消操作使用堆栈来组织数据

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!