深刻理解JavaScript执行上下文、函数堆栈、提高的概念

首先明确几个概念:javascript

  • EC:函数执行环境(或执行上下文),Execution Contextjava

  • ECS:执行环境栈,Execution Context Stacksegmentfault

  • VO:变量对象,Variable Object浏览器

  • AO:活动对象,Active Objectide

  • scope chain:做用域链函数

想当初本身看到这几个概念的时候是一(m)脸(d)懵(z)逼(z),可是不得不说这几个概念对之后深刻学习JS有很大的帮助。来不及解释了,赶忙上车~学习

EC(执行上下文)

每次当控制器转到ECMAScript可执行代码的时候,就会进入到一个执行上下文。ui

那什么是可执行代码呢?this

可执行代码的类型

全局代码(Global code
  • 这种类型的代码是在"程序"级处理的:例如加载外部的js文件或者本地<script></script>标签内的代码。全局代码不包括任何function体内的代码。 这个是默认的代码运行环境,一旦代码被载入,引擎最早进入的就是这个环境。spa

函数代码(Function code
  • 任何一个函数体内的代码,可是须要注意的是,具体的函数体内的代码是不包括内部函数的代码

Eval代码(Eval code
  • eval内部的代码

这里仅仅引入EC这个概念,后面还有关于EC创建细节的介绍。

ECS(执行环境栈)

咱们用MDN上的一个例子来引入函数执行栈的概念

function foo(i) {
  if (i < 0) return;
  console.log('begin:' + i);
  foo(i - 1);
  console.log('end:' + i);
}
foo(2);

// 输出:

// begin:2
// begin:1
// begin:0
// end:0
// end:1
// end:2

这里先不关心执行结果。磨刀不误砍柴功,先了解一下函数执行上下文堆栈的概念。相信弄明白了下面的概念,一切也就水落石出了

咱们都知道,浏览器中的JS解释器被实现为单线程,这也就意味着同一时间只能发生一件事情,其余的行为或事件将会被放在叫作执行栈里面排队。下面的图是单线程栈的抽象视图:

执行堆栈

当浏览器首次载入你的脚本,它将默认进入全局执行上下文。若是,你在你的全局代码中调用一个函数,你程序的时序将进入被调用的函数,并建立一个新的执行上下文,并将新建立的上下文压入执行栈的顶部。

若是你调用当前函数内部的其余函数,相同的事情会在此上演。代码的执行流程进入内部函数,建立一个新的执行上下文并把它压入执行栈的顶部。浏览器总会执行位于栈顶的执行上下文,一旦当前上下文函数执行结束,它将被从栈顶弹出,并将上下文控制权交给当前的栈。这样,堆栈中的上下文就会被依次执行而且弹出堆栈,直到回到全局的上下文。

看到这里,想必你们都已经深谙上述例子输出结果的缘由了,这里我大概绘了一个流程图来帮助理解。

![Alt text](./屏幕快照 2017-04-11 下午7.29.55.png)

VO(变量对象)/AO(活动对象)

这里为何要用一个/呢?按照字面理解,AO其实就是被激活的VO,两个实际上是一个东西。下面引用知乎上的一段话,帮助理解一下。原文连接

变量对象(Variable object)是说JS的执行上下文中都有个对象用来存放执行上下文中可被访问可是不能被delete函数标示符形参变量声明等。它们会被挂在这个对象上,对象的属性对应它们的名字对象属性的值对应它们的值但这个对象是规范上或者说是引擎实现上的不可在JS环境中访问到活动对象

激活对象(Activation object)有了变量对象存每一个上下文中的东西,可是它何时能被访问到呢?就是每进入一个执行上下文时,这个执行上下文儿中的变量对象就被激活,也就是该上下文中的函数标示符、形参、变量声明等就能够被访问到了

EC创建的细节

一、建立阶段【当函数被调用,但未执行任何其内部代码以前】
  • 建立做用域链(Scope Chain)

  • 建立变量,函数和参数。

  • 求”this“的值

二、执行阶段
  • 初始化变量的值和函数的引用,解释/执行代码。

咱们能够将每一个执行上下文抽象为一个对象,这个对象具备三个属性

ECObj: {
    scopeChain: { /* 变量对象(variableObject)+ 全部父级执行上下文的变量对象*/ }, 
    variableObject: { /*函数 arguments/参数,内部变量和函数声明 */ }, 
    this: {} 
}

解释器执行代码的伪逻辑

一、查找调用函数的代码。

二、执行代码以前,先进入建立上下文阶段:

  • 初始化做用域链

  • 建立变量对象:

    • 建立arguments对象,检查上下文,初始化参数名称和值并建立引用的复制。

    • 扫描上下文的函数声明(而非函数表达式)

      • 为发现的每个函数,在变量对象上建立一个属性——确切的说是函数的名字——其有一个指向函数在内存中的引用。

      • 若是函数的名字已经存在,引用指针将被重写。

    • 扫描上下文的变量声明

      • 为发现的每一个变量声明,在变量对象上建立一个属性——就是变量的名字,而且将变量的值初始化为undefined

      • 若是变量的名字已经在变量对象里存在,将不会进行任何操做并继续扫描。

    • 求出上下文内部“this”的值

三、激活/代码执行阶段:

  • 在当前上下文上运行/解释函数代码,并随着代码一行行执行指派变量的值。

VO --- 对应上述第二个阶段
function foo(i){
            var a = 'hello'
            var b = function(){}
            function c(){}
        }
        foo(22)

当咱们调用foo(22)时,整个建立阶段是下面这样的

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

正如咱们看到的,在上下文建立阶段,VO的初始化过程以下(该过程是有前后顺序的:函数的形参==>>函数声明==>>变量声明):

  • 函数的形参(当进入函数执行上下文时) —— 变量对象的一个属性,其属性名就是形参的名字,其值就是实参的值;对于没有传递的参数,其值为undefined

  • 函数声明(FunctionDeclaration, FD) —— 变量对象的一个属性,其属性名和值都是函数对象建立出来的;若是变量对象已经包含了相同名字的属性,则替换它的值

  • 变量声明(var,VariableDeclaration) —— 变量对象的一个属性,其属性名即为变量名,其值为undefined;若是变量名和已经声明的函数名或者函数的参数名相同,则不会影响已经存在的属性。

对于函数的形参没有什么可说的,主要看一下函数的声明以及变量的声明两个部分。

一、如何理解函数声明过程当中若是变量对象已经包含了相同名字的属性,则替换它的值这句话?

看以下这段代码:

function foo1(a){
    console.log(a)
    function a(){} 
}
foo1(20)//'function a(){}'

根据上面的介绍,咱们知道VO建立过程当中,函数形参的优先级是高于函数的声明的,结果是函数体内部声明的function a(){}覆盖了函数形参a的声明,所以最后输出a是一个function

二、如何理解变量声明过程当中若是变量名和已经声明的函数名或者函数的参数名相同,则不会影响已经存在的属性这句话?

//情景一:与参数名相同
function foo2(a){
    console.log(a)
    var a = 10
}
foo2(20) //'20'

//情景二:与函数名相同

function foo2(){
    console.log(a)
    var a = 10
    function a(){}
}
foo2() //'function a(){}'

下面是几个比较有趣的例子,当作加餐小菜,你们细细品味。这里给出一句话当作参考:

函数的声明比变量优先级要高,而且定义过程不会被变量覆盖,除非是赋值

function foo3(a){
    var a = 10
    function a(){}
    console.log(a)
}
foo3(20) //'10'

function foo3(a){
    var a 
    function a(){}
    console.log(a)
}
foo3(20) //'function a(){}'
AO --- 对应第三个阶段

正如咱们看到的,建立的过程仅负责处理定义属性的名字,而并不为他们指派具体的值,固然还有对形参/实参的处理。一旦建立阶段完成,执行流进入函数而且激活/代码执行阶段,看下函数执行完成后的样子:

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

提高(Hoisting)

对于下面的代码,相信不少人都能一眼看出输出结果,可是却不多有人能给出为何会产生这种输出结果的解释。

(function() {
    console.log(typeof foo); // 函数指针
    console.log(typeof bar); // undefined

    var foo = 'hello',
        bar = function() {
            return 'world';
        };
        
    function foo() {
        return 'hello';
    }
}());

一、为何咱们能在foo声明以前访问它?
回想在VO的建立阶段,咱们知道函数在该阶段就已经被建立在变量对象中。因此在函数开始执行以前,foo已经被定义了。

二、Foo被声明了两次,为何foo显示为函数而不是undefined或字符串?
咱们知道,在建立阶段,函数声明是优先于变量被建立的。并且在变量的建立过程当中,若是发现VO中已经存在相同名称的属性,则不会影响已经存在的属性。
所以,对foo()函数的引用首先被建立在活动对象里,而且当咱们解释到var foo时,咱们看见foo属性名已经存在,因此代码什么都不作并继续执行。

三、为何bar的值是undefined?
bar采用的是函数表达式的方式来定义的,因此bar其实是一个变量,但变量的值是函数,而且咱们知道变量在建立阶段被建立但他们被初始化为undefined,这也是为何函数表达式不会被提高的缘由。

总结:

一、EC分为两个阶段,建立执行上下文和执行代码。
二、每一个EC能够抽象为一个对象,这个对象具备三个属性,分别为:做用域链ScopeVO|AOAOVO只能有一个)以及this
三、函数EC中的AO在进入函数EC时,肯定了Arguments对象的属性;在执行函数EC时,其它变量属性具体化。
四、EC建立的过程是由前后顺序的:参数声明 > 函数声明 > 变量声明

参考

javascript 执行环境,变量对象,做用域链
What is the Execution Context & Stack in JavaScript?
函数MDN

相关文章
相关标签/搜索