资讯专栏INFORMATION COLUMN

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

miguel.jiang / 2040人阅读

摘要:每次调用函数时,都会创建一个新的执行上下文。理解执行上下文和堆栈可以让您了解代码为什么要计算您最初没有预料到的不同值的原因。

首发:https://www.love85g.com/?p=1723

在这篇文章中,我将深入研究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/...

欢迎关注小程序,感谢您的支持!

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/103817.html

相关文章

  • []了解Javascript中的执行下文执行堆栈

    摘要:理解执行上下文和执行堆栈对于理解的其它概念如提升,范围和闭包至关重要。正确地理解执行上下文和执行堆栈将帮助你更好地使用开发应用。引擎执行位于执行堆栈顶部的方法。当调用时,为该函数创建一个新的执行上下文,并且把它推入到当前执行堆栈。 By Sukhjinder Arora | Aug 28, 2018 原文 如果你是或者你想要成为一名js开发者,那么你必须了解js程序内部的运作。理解执行...

    qujian 评论0 收藏0
  • JavaScript 工作原理之一-引擎,运行时,调用堆栈()

    摘要:本章会对语言引擎,运行时,调用栈做一个概述。调用栈只是一个单线程的编程语言,这意味着它只有一个调用栈。查看如下代码当引擎开始执行这段代码的时候,调用栈会被清空。之后,产生如下步骤调用栈中的每个入口被称为堆栈结构。 原文请查阅这里,本文采用知识共享署名 4.0 国际许可协议共享,BY Troland。 本系列持续更新中,Github 地址请查阅这里。 这是 JavaScript 工作原...

    Betta 评论0 收藏0
  • JavaScript 工作原理之一-引擎,运行时,调用堆栈()

    摘要:本章会对语言引擎,运行时,调用栈做一个概述。调用栈只是一个单线程的编程语言,这意味着它只有一个调用栈。查看如下代码当引擎开始执行这段代码的时候,调用栈会被清空。之后,产生如下步骤调用栈中的每个入口被称为堆栈结构。 原文请查阅这里,本文采用知识共享署名 4.0 国际许可协议共享,BY Troland。 本系列持续更新中,Github 地址请查阅这里。 这是 JavaScript 工作原...

    Alex 评论0 收藏0
  • JavaScript 如何工作:对引擎、运行时、调用堆栈的概述

    摘要:调用栈是一种数据结构,它记录了我们在程序中的位置。当从这个函数返回的时候,就会将这个函数从栈顶弹出,这就是调用栈做的事情。而且这不是唯一的问题,一旦你的浏览器开始处理调用栈中的众多任务,它可能会停止响应相当长一段时间。 原文地址: https://blog.sessionstack.com... PS: 好久没写东西了,最近一直在准备写一个自己的博客,最后一些技术方向已经敲定了,又可以...

    Warren 评论0 收藏0
  • []JavaScript的调用栈、回调队列事件循环

    摘要:在这个视频中,将的调用栈回调队列和事件循环的内容讲的很清晰。调用栈可以往里面放东西,可以在事件结束的时候把回调函数放进回调队列,然后是事件循环。为的时候这个过程看起来可能不明显,除非考虑到调用栈的执行环境和事件循环的情况。 译者按这篇文章可以看做是对Philip Roberts 2014年在JSConf演讲的《What the heck is the event loop anyway...

    YancyYe 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<