# JavaScript中的执行上下文和队列(栈)的关系?

  • 原文:What is the Execution Context & Stack in JavaScript?
  • git地址:JavaScript中的执行上下文和队列(栈)的关系?
  • 导读:之前老是看到相关文章提到什么变量提高,函数提高啥的,什么函数提高优先级大于变量的,老是知其然,不知其因此然,当面试官拿着同一name,却不断function, 和var赋值,而后让你告诉他每个阶段该是什么值的时候,拿着啥变量提高和函数提高是解释不通的,至少我不能-_-。David Shariff的这篇文章为咱们讲述了其中的原理,让人看了豁然开朗
在这篇文章中,我将深刻探讨JavaScript的一个最基本的部分,执行上下文。 在本文结束时,您会更清楚解释器都作了些什么,以致于某些函数、变量在声明它们以前就可使用,它们的值是如何肯定的。

什么是执行上下文?

当代码在JavaScript中运行时,它的执行环境很是重要,而且它们分为如下几类:javascript

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

为了便于理解,本文中执行上下文是指:当前被执行的代码的环境、做用域;接下来让咱们看一个执行上下文中包含global、function content的代码:java

图片描述

这里没有什么特别之处,1个global context由紫色边框表示,3个不一样的function contexts分别由绿色、蓝色和橙色边框表示。只能有1个global context,能够从程序中的任何其余上下文访问。git

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

执行上下文堆栈

浏览器中的JavaScript解释器单线程运行。这就意味着同一时间浏览器只执行一件事,其它的事件在执行队列中排队。下图是单线程队列的抽象视图:面试

图片描述

咱们已经知道,当浏览器首次加载您的脚本时,它默认进入全局执行上下文(global execution contenrt)。若是在您的全局代码中调用一个函数,程序的顺序流进入被调用的函数,建立一个新函数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(koa2的洋葱图想到了没?)koa

如下是执行队列的5个关键点:

  • 单线程、
  • 同步执行
  • 全局上下文
  • 无限级的函数上下文
  • 每一个函数调用都会建立一个新的执行上下文(execution context),包括对自身的调用(递归)

![es1](./static/es1.gif)

执行上下文详情

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

  1. 建立阶段 [调用函数时,但在执行任何代码以前]:
  • 建立做用域链
  • 建立变量(variables),函数(functions )和参数(arguments)
  • 肯定"this"。
  1. 激活/执行阶段:
  • var 赋值,(function声明)指向函数,解释/执行代码

能够将每一个execution context概念上表示为具备3个属性的对象:函数

executionContextObj = {
    'scopeChain': { /* variableObject + all parent execution context's variableObject */ },
    'variableObject': { /* function arguments / parameters, inner variable and function declarations */ },
    'this': {}
}

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

这executionContextObj是在调用函数时,但在执行实际函数以前建立的。这是第一阶段:建立阶段。这里,解释器经过扫描传入的参数或arguments、本地函数声明和局部变量声明来建立executionContextObj。此次扫描的结果就变成了executionContextObj.variableObject。

如下是解释器如何解析代码的伪概述:

  1. 遇到函数调用。
  2. 在执行function代码以前,建立执行上下文(execution context)。
  3. 进入建立阶段:

    • 初始化做用域链(Scope Chain)。
    • 建立变量对象(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),creation stage长这样子:

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

正如您所看到的,creation stage定义属性的name,不为它们赋值,但formal arguments / parameters(函数传参,arguments)除外。一旦creation stage完成后,执行流程进入函数体,在函数已经完成执行以后的execution stage以下:

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?

    • 若是咱们遵循creation stage,咱们知道变量在activation / code execution stage以前就建立了。因此当功能流程开始执行时,foo早就在activation object中定义了。
  • foo是声明了两次,为何显示foo的是 function ,__不是__ undefined string?

    • 即便foo声明了两次,咱们也知道在creation stage函数在变量以前就在activation objectbefore上建立了,若是属性名已经存在于activation object,解释器会忽略掉这次声明。
    • 所以,首先会在activation object上建立一个foo()的引用,当解释器到达时var foo,属性名称foo存在,因此代码什么也不作,而后继续。
  • 为何 bar 是 undefined?

    • bar其实是一个具备函数赋值的变量,咱们知道变量是在creation stage建立的,但它们的初始值为undefined。

概要

但愿到如今您已经很好地掌握了JavaScript解释器如何执行您的代码。理解执行上下文和队列可让您了解代码没有达到预期的缘由

您是否定为了解解释器的内部工做原理是您的JavaScript知识的重要组成部分?知道执行上下文的每一个阶段是否有助于您编写更好的JavaScript?

__注意__:有些人一直在问关于闭包,回调,超时等,我将在在下一篇文章中涉及,主要概述做用域链与execution context的关系。

拓展

相关文章
相关标签/搜索