JavaScript中的执行上下文

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

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


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

什么是执行上下文?

JavaScript的执行环境很是重要,当JavaScript代码在行时,会被预处理为如下状况之一:java

  • Global code - 首次执行代码的默认环境。
  • Function code - 每当执行流程进入函数体时。
  • Eval code - 要在eval函数内执行的文本。

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

clipboard.png

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

你能够拥有任意数量的函数上下文,而且每一个函数调用都会建立一个新的上下文,从而建立一个私有做用域,其中没法从当前函数做用域外直接访问函数内部声明的任何内容。 在上面的示例中,函数能够访问在其当前上下文以外声明的变量,但外部上下文没法访问在其中声明的变量或函数。 为何会这样呢? 这段代码到底是如何处理的?ecmascript

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

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

clipboard.png

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

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

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

clipboard.png

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

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

  • 单线程。
  • 同步执行。
  • 一个全局上下文。
  • 任意多个函数上下文。
  • 每一个函数调用都会建立一个新的执行上下文execution context,甚至是对自身的调用。

执行上下文的细节

因此咱们如今知道每次调用一个函数时,都会建立一个新的执行上下文。 可是,在JavaScript解释器中,对执行上下文的每次调用都有两个阶段:

  1. 建立阶段 [调用函数时,但在执行任何代码以前]:

    • 建立做用域链
    • 建立变量,函数和参数。
    • 肯定“this”的值。
  2. 激活/代码执行阶段:

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

能够将每一个执行上下文在概念上表示为具备3个属性的对象:

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

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

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

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

  1. 找一些代码来调用一个函数。
  2. 在执行功能代码以前,建立执行上下文
  3. 进入建立阶段:

    • 初始化做用域链。
    • 建立variable object

      • 建立arguments object,检查参数的上下文,初始化名称和值并建立引用副本。
      • 扫描上下文以获取函数声明:

        • 对于找到的每一个函数,在variable object中建立一个属性,该属性是函数的确切名称,该属性存在指向内存中函数的引用指针。
        • 若是函数名已存在,则将覆盖引用指针值。
      • 扫描上下文以获取变量声明:

        • 对于找到的每一个变量声明,在variable object中建立一个属性做为变量名称,并将该值初始化为undefined
        • 若是变量名称已存在于variable object中,则不执行任何操做并继续扫描。
    • 肯定上下文中“this”的值。
  4. 激活/执行阶段:

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

咱们来看一个例子:

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呢?

进一步阅读


本文首发微信公众号:jingchengyideng

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

相关文章
相关标签/搜索