资讯专栏INFORMATION COLUMN

JavaScript中的执行上下文

cfanr / 2217人阅读

摘要:如果在全局代码中调用函数,程序的顺序流进入被调用的函数,创建新的执行上下文并将其推送到执行堆栈的顶部。每次调用函数时,都会创建一个新的执行上下文。

翻译:疯狂的技术宅
链接:http://davidshariff.com/blog/...

本文首发微信公众号:jingchengyideng
欢迎关注,每天都给你推送新鲜的前端技术文章

在这篇文章中,我将深入探讨JavaScript的最基本部分之一,即Execution Context(执行上下文)。 在本文结束时,你应该对解释器了解得更清楚:为什么在声明它们之前可以使用某些函数或变量?以及它们的值是如何确定的?

什么是执行上下文?

JavaScript的执行环境非常重要,当JavaScript代码在行时,会被预处理为以下情况之一:

Global code - 首次执行代码的默认环境。

Function code - 每当执行流程进入函数体时。

Eval code - 要在eval函数内执行的文本。

你可以阅读大量涉及作用域的在线资料,不过为了使事情更容易理解,让我们将术语“执行上下文”视为当前代码的运行环境或作用域。接下来让我们看一个包含global和function / local上下文的代码示例。

这里没有什么特别之处,我们有一个由紫色边框表示的全局上下文,和由绿色,蓝色和橙色边框表示的3个不同的函数上下文。 只能有1个全局上下文,可以从程序中的任何其他上下文访问。

你可以拥有任意数量的函数上下文,并且每个函数调用都会创建一个新的上下文,从而创建一个私有作用域,其中无法从当前函数作用域外直接访问函数内部声明的任何内容。 在上面的示例中,函数可以访问在其当前上下文之外声明的变量,但外部上下文无法访问在其中声明的变量或函数。 为什么会这样呢? 这段代码究竟是如何处理的?

Execution Context Stack(执行上下文堆栈)

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

我们已经知道,当浏览器首次加载脚本时,它默认进入全局上下文执行。 如果在全局代码中调用函数,程序的顺序流进入被调用的函数,创建新的执行上下文并将其推送到执行堆栈的顶部。

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

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

代码简单地调用自身3次,并将i的值递增1。每次调用函数foo时,都会创建一个新的执行上下文。 一旦上下文完成执行,它就会弹出堆栈并且讲控制返回到它下面的上下文,直到再次达到全局上下文

关于执行堆栈execution stack有5个关键要点:

单线程。

同步执行。

一个全局上下文。

任意多个函数上下文。

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

执行上下文的细节

所以我们现在知道每次调用一个函数时,都会创建一个新的执行上下文。 但是,在JavaScript解释器中,对执行上下文的每次调用都有两个阶段:

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

创建作用域链

创建变量,函数和参数。

确定“this”的值。

激活/代码执行阶段:

分配值,引用函数和解释/执行代码。

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

executionContextObj = {
    "scopeChain": { /* variableObject + 所有父执行上下文的variableObject */ },
    "variableObject": { /* 函数实参/形参,内部变量和函数声明 */ },
    "this": {}
}
激活对象/变量对象 [AO/VO]

在调用该函数,并且在实际执行函数之前,会创建这个executionContextObj。 这被称为第1阶段,即创造阶段。 这时解释器通过扫描函数传递的实参或形参、本地函数声明和局部变量声明来创建executionContextObj。 此扫描的结果将成为executionContextObj中的variableObject

以下是解释器如何预处理代码的伪代码概述:

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

在执行功能代码之前,创建执行上下文

进入创建阶段:

初始化作用域链。

创建variable object

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

扫描上下文以获取函数声明:

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

如果函数名已存在,则将覆盖引用指针值。

扫描上下文以获取变量声明:

对于找到的每个变量声明,在variable object中创建一个属性作为变量名称,并将该值初始化为undefined

如果变量名称已存在于variable object中,则不执行任何操作并继续扫描。

确定上下文中“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: { ... }
}
关于hoisting

你可以找到许多使用JavaScript定义术语hoisting的在线资源,解释变量和函数声明被hoisting到其函数范围的顶部。 但是没有人能够详细解释为什么会发生这种情况,掌握了关于解释器如何创建激活对象的新知识,很容易理解为什么。 请看下面的代码示例:

(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显示为function而不是undefinedstring

即使foo被声明两次,我们通过创建阶段知道函数在变量之前就被创建在激活对象上了,而且如果激活对象上已经存在了属性名称,我们只是绕过了声明这一步骤。

因此,首先在激活对象上创建对函数foo()的引用,并且当解释器到达var foo时,我们已经看到属性名称foo存在,因此代码不执行任何操作并继续处理。

为什么bar未定义?

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

总结

希望到这里你已经能够很好地掌握了JavaScript解释器如何预处理你的代码。 理解执行上下文和堆栈可以让你了解背后的原因:为什么代码预处理后的值和你预期的不一样。

你认为学习解释器的内部工作原理是多此一举还是非常必要的呢? 了解执行上下文阶段是否能够帮你你写出更好的JavaScript呢?

进一步阅读

ECMA-262 5th Edition (http://www.ecma-international...

ECMA-262-3 in detail. Chapter 2. Variable object (http://dmitrysoshnikov.com/ec...

Identifier Resolution, Execution Contexts and scope chains (http://jibbering.com/faq/note...

本文首发微信公众号:jingchengyideng


欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章

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

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

相关文章

  • JavaScript深入之执行下文

    摘要:深入系列第三篇,讲解执行上下文栈的是如何执行的,也回答了第二篇中的略难的思考题。 JavaScript深入系列第三篇,讲解执行上下文栈的是如何执行的,也回答了第二篇中的略难的思考题。 顺序执行? 如果要问到 JavaScript 代码执行顺序的话,想必写过 JavaScript 的开发者都会有个直观的印象,那就是顺序执行,毕竟: var foo = function () { ...

    codecraft 评论0 收藏0
  • JavaScript执行下文执行环境

    摘要:也就是说,当代码执行的时候,会进入不同的执行上下文,这些执行上下文就构成了一个执行上下文栈,。它是一个与上下文相关的特殊对象,其中存储了在上下文中定义的变量和函数声明。 明白的人,看标题这么写,会发现是有问题的,对的,在JavaScript中执行上下文与执行环境是同一个东西,标题这么写肯定是有问题的。但是有些人是搞不清执行上下文与执行环境的,所以我才这么写,以便于他们好搜索到。下面我们...

    ZHAO_ 评论0 收藏0
  • 深入理解JavaScript执行下文执行

    摘要:执行上下文和执行栈是中关键概念之一,是难点之一。理解执行上下文和执行栈同样有助于理解其他的概念如提升机制作用域和闭包等。函数执行完成,函数的执行上下文出栈,并且被销毁。 前言 如果你是一名 JavaScript 开发者,或者想要成为一名 JavaScript 开发者,那么你必须知道 JavaScript 程序内部的执行机制。执行上下文和执行栈是JavaScript中关键概念之一,是Ja...

    silenceboy 评论0 收藏0
  • 深入理解JavaScript执行下文执行

    摘要:执行上下文和执行栈是中关键概念之一,是难点之一。理解执行上下文和执行栈同样有助于理解其他的概念如提升机制作用域和闭包等。函数执行完成,函数的执行上下文出栈,并且被销毁。 前言 如果你是一名 JavaScript 开发者,或者想要成为一名 JavaScript 开发者,那么你必须知道 JavaScript 程序内部的执行机制。执行上下文和执行栈是JavaScript中关键概念之一,是Ja...

    leiyi 评论0 收藏0
  • 深入学习js之——执行下文

    摘要:当遇到函数调用时,引擎为该函数创建一个新的执行上下文并把它压入当前执行栈的顶部。参考链接理解中的执行上下文和执行栈深入之执行上下文栈 开篇 作为一个JavaScript的程序开发者,如果被问到JavaScript代码的执行顺序,你脑海中是不是有一个直观的印象 -- JavaScript 是顺序执行的,可事实真的是这样的吗? 让我们首先看两个小例子: var foo = functio...

    Lucky_Boy 评论0 收藏0
  • 【进阶1-1期】理解JavaScript 中的执行下文执行

    摘要:首次运行代码时,会创建一个全局执行上下文并到当前的执行栈中。执行上下文的创建执行上下文分两个阶段创建创建阶段执行阶段创建阶段确定的值,也被称为。 (关注福利,关注本公众号回复[资料]领取优质前端视频,包括Vue、React、Node源码和实战、面试指导) 本周正式开始前端进阶的第一期,本周的主题是调用堆栈,,今天是第一天 本计划一共28期,每期重点攻克一个面试重难点,如果你还不了解本进...

    import. 评论0 收藏0

发表评论

0条评论

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