乐趣区

关于javascript:JS执行上下文和调用栈

执行上下文在 JavaScript 是十分重要的基础知识,想要了解 JavaScript 的执行过程,执行上下文 是你必须要把握的常识。否则只能是知其然不知其所以然。
了解执行上下文有什么益处呢?
它能够帮忙你更好的了解代码的执行过程,作用域,闭包等要害知识点。特地是闭包它是 JavaScript 中的一个难点,当你了解了执行上下文在回头看闭包时,应该会有恍然大悟的感觉。
这篇文章咱们将深刻理解 执行上下文,读完文章之后你应该能够分明的理解到 JavaScript 解释器到底做了什么,为什么能够在一些函数和变量之前应用它,以及它们的值是如何确定的。


什么是执行上下文

在 JavaScript 中运行代码时,代码的执行环境十分重要,通常是下列三种状况:

  • Global code:代码第一次执行时的默认环境。
  • Function code:函数体中的代码
  • Eval code:eval 函数内执行的文本(理论开发中很少应用,所以见到的状况不多)

在网上你能够读到很多对于作用域的文章,为了便于了解本文的内容,咱们将 执行上下文 当作代码的 执行环境 / 作用域。当初就让咱们看一个例子:它包含 全局和函数 / 本地执行上下文。

下面的例子咱们看到,紫色的框代表全局上下文,绿色、蓝色、橙色代表三个不同的函数上下文。全局上下文执行有一个,它能够被其余上下文拜访到。

你能够有任意数量的函数上下文,每个函数在调用时都会创立一个新的上下文,它是一个公有范畴,函数外部申明的所有货色都不能在函数作用域外拜访到。

下面的例子中,函数外部能够拜访以后上下文之外申明的变量,然而内部却不能拜访函数外部的变量 / 函数。这到底是为什么?其中的代码是如何执行的?


执行上下文栈

浏览器中的 JavaScript 解释器是单线程实现的。这意味着在浏览器中一次只能做一件事件。而其余的行为或事件都会在执行栈中排队期待。如图:

咱们晓得,当浏览器第一次加载脚本时,默认状况下,它会进入全局上下文。如果在全局代码中调用了一个函数,则代码的执行会进入函数中,此时会创立一个新的执行上下文,它会被推到执行上下文栈中。

如果在这个过程中函数外部调用了另一个函数,会产生同样的事件,代码的执行会进入函数中,而后创立一个新的执行上下文,它会被推到上下文栈 的顶部。浏览器始终执行栈顶部的执行上下文。

一旦函数实现执行,以后的执行上下文将从栈的顶部弹出,而后继续执行上面的,上面程序演示了一个递归函数的执行上下文状况。

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


本人调用本人三次,每次将 i 递增 1,每次函数 foo 被调用的时候,就会创立一个新的执行上下文。一旦以后上下文执行结束之后,它就会从栈中弹出并转移到上面的上下文中,直到全局高低。
执行上下文栈的 5 个关键点:

  • 单线程
  • 同步执行
  • 只有一个全局上下文
  • 任意数量的函数上下文
  • 每个函数调用都会创立一个新的执行上下文,包含本人调用本人
详解执行上下文

到此,咱们晓得每次调用一个函数时,都会创立一个新的执行上下文。然而在 JavaScript 解释器中,每次调用执行上下文会有两个阶段:

创立阶段
  • 创立作用域链
  • 创立变量,函数,arguments 列表。
  • 确定 this 的指向
执行阶段
  • 赋值,寻找函数援用,解释 / 执行代码

执行上下文能够形象为一个对象它具备三个属性:

executionContextObj = {'scopeChain': { /* variableObject + all parent execution context's variableObject */},'variableObject': {/* function arguments / parameters, inner variable and function declarations */},'this': {}}
流动 / 变量对象[AO/VO]

executionContextObj 对象在函数调用时创立,但它是在函数真正执行之前就创立的,这就是咱们所说的第一个阶段 创立阶段,此时解释器通过扫描函数的传入参数,arguments,本地函数申明,局部变量申明来创立 executionContextObj 对象。将后果变成 variableObject 放入 executionContextObj 中。
解释器执行代码时的大抵形容:

  • 调用函数
  • 在执行代码时,创立执行上下文
  • 进入创立阶段:
  1. 初始化作用域链
  2. 创立变量对象(variableObject)
  3. 创立参数对象(arguments object),查看参数的上下文,初始化名称和值,并创立援用正本
  4. 扫描上下文中的函数申明
  5. 每发现一个函数,就会在 variableObject 中创立一个名称,保留函数的援用
  6. 如果名称曾经存在,则笼罩援用
  7. 扫描上下文中的变量申明
  8. 每发现一个变量,就在 variableObject 中创立一个名称,并初始化值为 undefined
  9. 如果变量名曾经存在,什么都不做,持续扫描
  10. 确定上下文中的 this 指向
  • 执行代码阶段:
  1. 在上下文中执行 / 解释代码,在代码逐行执行时进行变量复赋值

让咱们看一个例子:

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: {...}
}

如上所述,除了形参 i 和 arguments 外,在创立阶段咱们只把变量进行申明而不进行赋值。

在创立阶段实现后,程序会进入函数中执行代码,如下所示:

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

网上很多对于申明提前的内容,它是用来解释变量和函数在申明时会被提前到作用域的顶部。然而并没有人具体解释为什么会产生这种状况,有了方才对于解释器如何创建活动对象(AO)的认知,咱们将很容易看出起因。例如:

(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 而不是 undefined 或者 string

尽管 foo 被申明了两次,然而咱们在创立阶段中说到,函数是在变量之前创立在变量对象中,当变量对象中名称曾经存在时,变量申明什么也不做。

因而 foo 会被先创立为函数 function foo() 的援用,当执行到 var foo 时发现变量对象中已将存在了,所以此时什么也不做,而是持续扫描。

为什么 barundefined

bar 实际上是一个变量只不过它的值是函数,而变量在创立阶段的值为 undefined


总结

咱们再来梳理下重要的知识点:

  • 首先在程序执行时会创立一个全局的执行上下文,有且只有一个。
  • 函数在每次调用时就会创立一个函数上下文,能够有很多。
  • 函数上下文能够拜访全局上下文的内容,反之则不行。
  • 创立的上下文会被推入到上下文栈中,而后从顶部开始顺次执行。
  • 执行上下文会分为两个阶段:创立阶段和执行阶段。
  • 创立阶段会先进行函数申明和变量申明提前。
  • 创立阶段会先进行函数申明,而后进行变量申明,同时会被放入变量对象中,如果变量对象中曾经存在:函数则进行援用的笼罩,变量则什么都不做。
  • 执行阶段才会进行赋值和运行。
退出移动版