JavaScript基础系列---执行环境与做用域链

问题

今天看笔记发现本身以前记了一个关于同名标识符优先级的内容,具体是下面这样的:javascript

  • 形参优先级高于当前函数名,低于内部函数名
  • 形参优先级高于arguments
  • 形参优先级高于只声明却未赋值的局部变量,可是低于声明且赋值的局部变量
  • 函数和变量都会声明提高,函数名和变量名同名时,函数名的优先级要高。执行代码时,同名函数会覆盖只声明却未赋值的变量,可是它不能覆盖声明且赋值的变量
  • 局部变量也会声明提高,能够先使用后声明,不影响外部同名变量

而后我就想,为何会有这样的优先级呢,规定的?可是好像没有这个规定,因而开始查阅资料,就有了下文html

初识Execution Context

Execution ContextJavascript中一个抽象概念,它定义了变量或函数有权访问的其余数据,决定了它们各自的行为。为了便于理解,咱们能够近似将其等同于执行当前代码的环境,JavaScript的可执行代码包括前端

  • 全局代码
  • 函数代码
  • eval()代码

每当执行流转到这些可执行代码时,就会“新建”一个Execution Context并进入该Execution Contextjava

clipboard.png

在上图中,共有4个Execution Context,其中有一个是Global Execution Context(有且仅有一个),还有三个Function Execution Contextsegmentfault

再识Execution Context Stack

浏览器中的JavaScript解释器是单线程的,每次建立并进入一个新的Execution Context时,这个Execution Context就会被推(push)进一个环境栈中,这个栈称为Execution Context Stack,当当前Execution Context的代码执行完以后,栈又会将其弹(pop)出,并销毁这个Execution Context,保存在其中的变量及函数定义也随之被销毁,而后把控制权返回给以前的Execution ContextGlobal Execution Context例外,它要等到应用程序退出后 —— 如关闭网页或浏览器 —— 才会被销毁)浏览器

JavaScript的执行流就是由这个机制控制的,如下面的代码为例说明:闭包

var sayHello = 'Hello';
function name(){
    var fisrtName = 'Cao',
        lastName = 'Cshine';
    function getFirstName(){
        return fisrtName;
    }
    function getLatName(){
        return lastName;
    }
    console.log(sayHello + getFirstName() + ' ' + getLastName());
}
name();

clipboard.png

  • 当浏览器第一次加载script的时候,默认会进入Global Execution Context,因此Global Execution Context永远是在栈的最下面。
  • 而后遇到函数调用name(),此时新建并进入Function Execution Context nameFunction Execution Context name入栈;
  • 继续执行遇到函数调用getFirstName(),因而新建并进入Function Execution Context getFirstNameFunction Execution Context getFirstName入栈,因为该函数内部不会再新建其余Execution Context,因此直接执行完毕,而后出栈,控制权交给Function Execution Context name
  • 再往下执行遇到函数调用getLastName(),因而新建并进入Function Execution Context getLastNameFunction Execution Context getLastName入栈,因为该函数内部不会再新建其余Execution Context,因此直接执行完毕,而后出栈,控制权交给Function Execution Context name
  • 执行完console后,函数name也执行完毕,因而出栈,控制权交给Function Execution Context name,至此栈中又只有Global Execution Context
  • 关于Execution Context Stack有5个关键点:异步

    • 单线程
    • 同步执行(非异步)
    • 1个Global Execution Context
    • 无限制的函数Function Execution Context
    • 每一个函数调用都会建立新的Execution Context,即便是本身调用本身,以下面的代码:函数

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

      Execution Context Stack的状况以下图所示:ui

      clipboard.png

亲密接触Execution Context

每一个Execution Context在概念上能够当作由下面三者组成:

  • 变量对象(Variable object,简称VO
  • 做用域链(Scope Chain
  • this

变量对象(Variable object

该对象与Execution Context相关联,保存着Execution Context中定义的全部变量、函数声明以及函数形参,这个对象咱们没法访问,可是解析器在后台处理数据是用到它(注意函数表达式以及没用var/let/const声明的变量不在VO中)

Global Execution Context中的变量对象VO根据宿主环境的不一样而不一样,在浏览器中为window对象,所以全部的全局变量和函数都是做为window对象的属性和方法建立的。

对于Function Execution Context,变量对象VO为函数的活动对象,活动对象是在进入Function Execution Context时建立的,它经过函数的arguments属性初始化,也就是最初只包含arguments这一个属性。

JavaScript解释器内部,每次调用Execution Context都会经历下面两个阶段:

  • 建立阶段(发生在函数调用时,可是内部代码执行前,这将解释声明提高现象)

    • 建立做用域链(做用域链见下文)
    • 建立变量对象VO
    • 肯定this的值
  • 激活/代码执行阶段

    • 变量赋值、执行代码

其中建立阶段的第二步建立变量对象VO的过程能够理解成下面这样:

  • Global Execution Context中没有这一步) 建立arguments对象,扫描函数的全部形参,并将形参名称 和对应值组成的键值对做为变量对象VO的属性。若是没有传递对应的实参,将undefined做为对应值。若是形参名为arguments,将覆盖arguments对象
  • 扫描Execution Context中全部的函数声明(注意是函数声明,函数表达式不算)

    • 将函数名和对应值(指向内存中该函数的引用指针)组成组成的键值对做为变量对象VO的属性
    • 若是变量对象VO已经存在同名的属性,则覆盖这个属性
  • 扫描Execution Context中全部的变量声明

    • 由变量名和对应值(此时为undefined) 组成,做为变量对象的属性
    • 若是变量名与已经声明的形参或函数相同,此时什么都不会发生,变量声明不会干扰已经存在的这个同名属性。

好~~如今咱们来看代码捋一遍:

function foo(num) {
    console.log(num);// 66
    console.log(a);// undefined
    console.log(b);// undefined
    console.log(fc);// f function fc() {}
    var a = 'hello';
    var b = function fb() {};
    function fc() {}
}
foo(66);
  • 当调用foo(66)时,建立阶段时,Execution Context能够理解成下面这个样子

    fooExecutionContext = {
        scopeChain: { ... },
        variableObject: {
            arguments: {
                0: 66,
                length: 1
            },
            num: 66,
            fc: pointer to function fc()
            a: undefined,
            b: undefined
        },
        this: { ... }
    }
  • 当建立阶段完成之后,执行流进入函数内部,激活执行阶段,而后代码完成执行,Execution Context能够理解成下面这个样子:

    fooExecutionContext = {
        scopeChain: { ... },
        variableObject: {
            arguments: {
                0: 66,
                length: 1
            },
            num: 66,
            fc: pointer to function fc()
            a: 'hello',
            b: pointer to function fb()
        },
        this: { ... }
    }

做用域链(Scope Chain

当代码在一个Execution Context中执行时,就会建立变量对象的一个做用域链,做用域链的用途是保证对执行环境有权访问的全部变量和函数的有序访问

Global Execution Context中的做用域链只有Global Execution Context的变量对象(也就是window对象),而Function Execution Context中的做用域链还会有“父”Execution Context的变量对象,这里就会要牵扯到[[Scopes]]属性,能够将函数做用域链理解为---- 当前Function Execution Context的变量对象VO(也就是该函数的活动对象AO) + [[Scopes]],怎么理解呢,咱们继续往下看

[[Scopes]]属性

[[Scopes]]这个属性与函数的做用域链有着密不可分的关系,JavaScript中每一个函数都表示为一个函数对象,[[Scopes]]是函数对象的一个内部属性,只有JavaScript引擎能够访问。

结合函数的生命周期:

  • 函数定义

    • [[Scopes]]属性在函数定义时被存储,保持不变,直至函数被销毁
    • [[Scopes]]属性连接到定义该函数的做用域链上,因此他保存的是全部包含该函数的 “父/祖父/曾祖父...” Execution Context的变量对象(OV),咱们将其称为全部父变量对象(All POV
    • !!!特别注意 [[Scopes]]是在定义一个函数的时候决定的
  • 函数调用

    • 函数调用时,会建立并进入一个新的Function Execution Context,根据前面讨论过的调用Function Execution Context的两个阶段可知:先建立做用域链,这个建立过程会将该函数对象的[[Scopes]]属性加入到其中
    • 而后会建立该函数的活动对象AO(做为该Function Execution Context的变量对象VO),并将建立的这个活动对象AO加到做用域链的最前端
    • 而后肯定this的值
    • 正式执行函数内的代码

经过上面的过程咱们大概能够理解:做用域链 = 当前Function Execution Context的变量对象VO(也就是该函数的活动对象AO) + [[Scopes]],有了这个做用域链, 在发生标识符解析的时候, 就会沿着做用域链一级一级地搜索标识符,最开始是搜索当前Function Execution Context的变量对象VO,若是没有找到,就会根据[[Scopes]]找到父变量对象,而后继续搜索该父变量对象中是否有该标识符;若是仍没有找到,便会找到祖父变量对象并搜索其中是否有该标识符;如此一级级的搜索,直至找到标识符为止(若是直到最后也找不到,通常会报未定义的错误);注意:对于thisarguments,只会搜到其自己的变量(活动)对象为止,而不会继续按着做用域链搜素。

如今再结合例子来捋一遍:

var a = 10;
function foo(d) {
    var b = 20;
    function bar() {
        var c = 30;
        console.log(a +  b + c + d); // 110
        //这里能够访问a,b,c,d
    }
    //这里能够访问a,b,d 可是不能访问c
    bar();
}
//这里只能访问a
foo(50);
  • 当浏览器第一次加载script的时候,默认会进入Global Execution Context的建立阶段

    • 建立Scope Chain(做用域链)
    • 建立变量对象,此处为window对象。而后会扫描全部的全局函数声明,再扫描全局变量声明。以后该变量对象会加到Scope Chain
    • 肯定this的值
    • 此时Global Execution Context能够表示为:

      globalEC = {
          scopeChain: {
              pointer to globalEC.VO
          },
          VO: {
              a: undefined,
              foo: pointer to function foo(),
              (其余window属性)
          },
          this: { ... }
      }
  • 接着进入Global Execution Context的执行阶段

    • 遇到赋值语句var a = 10,因而globalEC.VO.a = 10

      globalEC = {
          scopeChain: {
              pointer to globalEC.VO
          },
          VO: {
              a: 10,
              foo: pointer to function foo(),
              (其余window属性)
          },
          this: { ... }
      }
    • 遇到foo函数定义语句,进入foo函数的定义阶段,foo[[Scopes]]属性被肯定

      foo.[[Scopes]] = {
          pointer to globalEC.VO
      }
    • 遇到foo(50)调用语句,进入foo函数调用阶段,此时进入Function Execution Context foo的建立阶段

      • 建立Scope Chain(做用域链)
      • 建立变量对象,此处为foo的活动对象。先建立arguments对象,而后扫描函数的全部形参,以后会扫描foo函数内全部的函数声明,再扫描foo函数内的变量声明。以后该变量对象会加到Scope Chain
      • 肯定this的值
      • 此时Function Execution Context foo能够表示为

        fooEC = {
            scopeChain: {
                pointer to fooEC.VO,
                foo.[[Scopes]]
            },
            VO: {
                arguments: {
                    0: 66,
                    length: 1
                },
                b: undefined,
                d: 50,
                bar: pointer to function bar(),
            },
            this: { ... }
        }
    • 接着进入Function Execution Context foo的执行阶段

      • 遇到赋值语句var b = 20;,因而fooEC .VO.b = 20

        fooEC = {
            scopeChain: {
                pointer to fooEC.VO,
                foo.[[Scopes]]
            },
            VO: {
                arguments: {
                    0: 66,
                    length: 1
                },
                b: 20,
                d: 50,
                bar: pointer to function bar(),
            },
            this: { ... }
        }
      • 遇到bar函数定义语句,进入bar函数的定义阶段,bar[[Scopes]]`属性被肯定

        bar.[[Scopes]] = {
            pointer to fooEC.VO,
            pointer to globalEC.VO
        }
      • 遇到bar()调用语句,进入bar函数调用阶段,此时进入Function Execution Context bar的建立阶段

        • 建立Scope Chain(做用域链)
        • 建立变量对象,此处为bar的活动对象。先建立arguments对象,而后扫描函数的全部形参,以后会扫描foo函数内全部的函数声明,再扫描bar函数内的变量声明。以后该变量对象会加到Scope Chain
        • 肯定this的值
        • 此时Function Execution Context bar能够表示为

          barEC = {
             scopeChain: {
                 pointer to barEC.VO,
                 bar.[[Scopes]]
             },
             VO: {
                 arguments: {
                     length: 0
                 },
                 c: undefined
             },
             this: { ... }
          }
      • 接着进入Function Execution Context bar的执行阶段

        • 遇到赋值语句var c = 30,因而barEC.VO.c = 30

          barEC = {
              scopeChain: {
                  pointer to barEC.VO,
                  bar.[[Scopes]]
              },
              VO: {
                  arguments: {
                      length: 0
                  },
                  c: 30
              },
              this: { ... }
          }
        • 遇到打印语句console.log(a + b + c + d);,须要访问变量a,b,c,d

          • 经过bar.[[Scopes]].globalEC.VO.a访问获得a=10
          • 经过bar.[[Scopes]].fooEC.VO.b,bar.[[Scopes]].fooEC.VO.d访问获得b=20,d=50
          • 经过barEC.VO.c访问获得c=30
          • 经过运算得出结果110
      • bar函数执行完毕,Function Execution Context bar销毁,变量c也随之销毁
    • foo函数执行完毕,Function Execution Context foo销毁,b,d,bar也随之销毁
  • 全部代码执行完毕,等到该网页被关闭或者浏览器被关闭,Global Execution Context才销毁,a,foo才会销毁

经过上面的例子,相信对Execution Context和做用域链的理解也更清楚了,下面简单总结一下做用域链:

  • 做用域链的前端始终是当前执行的代码所在Execution Context的变量对象;
  • 下一个变量对象来自其包含Execution Context,以此类推;
  • 最后一个变量对象始终是Global Execution Context的变量对象;
  • 内部Execution Context可经过做用域链访问外部Execution Context反之不能够
  • 标识符解析是沿着做用域链一级一级地搜索标识符的过程。搜索过程始终从做用域链的前端开始,而后逐级的向后回溯,直到找到标识符为止(若是找不到,一般会致使错误);
  • 做用域链的本质是一个指向变量对象的指针列表,只引用而不实际包含变量对象。

延长做用域链

下面两种语句能够在做用域链的前端临时增长一个变量对象以延长做用域链,该变量对象会在代码执行后被移除

  • try-catch语句的catch
    建立一个新的变量对象,其中包含的是被抛出的错误对象的声明
  • with语句
    将指定的对象添加到做用域链中

    function buildUrl(){
        var qs = "?debug=true";
        with(location){
            var url = href + qs;
        }
        //console.log(href) 将会报href is not defined的错误,由于with语句执行完with建立的变量对象就被移除了
        return url;
    }

    with语句接收window.location对象,所以其变量对象就包含了window.location对象的全部属性,而这个变量对象被添加到做用域链的前端。因此在with语句里面使用href至关于window.location.href

解答问题

如今咱们来解答最开始的优先级问题

  • 形参优先级高于当前函数名,低于内部函数名

    function fn(fn){
        console.log(fn);// cc
    }
    fn('cc');

    函数fn属于Global Execution Context,而形参fn属于Function Execution Context fn,此时做用域的前端是Function Execution Context fn的变量对象,因此console.log(fn)为形参的值

    function fa(fb){
        console.log(fb);// ƒ fb(){}
        function fb(){}
        console.log(fb);// ƒ fb(){}
    }
    fa('aaa');

    调用fa函数时,进入Function Execution Context fa的建立阶段,根据前面所说的变量对象建立过程:

    先建立arguments对象,而后扫描函数的全部形参,以后会扫描函数内全部的函数声明,再扫描函数内的变量声明;
    扫描函数声明时,若是变量对象 VO中已经存在同名的属性,则覆盖这个属性

    咱们能够获得fa的变量对象表示为:

    fa.VO = {
        arguments: {
            0:'aaa',
            length: 1
        },
        fb: pointer to function fb(),
    }

    因此console.log(fb)获得的是fa.VO.fb的值ƒ fb(){}

  • 形参优先级高于arguments

    function fn(aa){
        console.log(arguments);// Arguments ["hello world"]
    }
    fn('hello world');
    
    function fn(arguments){
        console.log(arguments);// hello world
    }
    fn('hello world');

    调用fn函数时,进入Function Execution Context fn的建立阶段,根据前面所说的变量对象建立过程:

    先建立arguments对象,而后扫描函数的全部形参,以后会扫描函数内全部的函数声明,再扫描函数内的变量声明;
    先建立arguments对象,后扫描函数形参,若是形参名为arguments,将会覆盖arguments对象

    因此当形参名为arguments时,console.log(arguments)为形参的值hello world

  • 形参优先级高于只声明却未赋值的局部变量,可是低于声明且赋值的局部变量

    function fa(aa){
        console.log(aa);//aaaaa
        var aa;
        console.log(aa);//aaaaa
    }
    fa('aaaaa');

    调用fa函数时,进入Function Execution Context fa的建立阶段,根据前面所说的变量对象建立过程:

    先建立arguments对象,而后扫描函数的全部形参,以后会扫描函数内全部的函数声明,再扫描函数内的变量声明;
    扫描函数内的变量声明时,若是变量名与已经声明的形参或函数相同,此时什么都不会发生,变量声明不会干扰已经存在的这个同名属性

    因此建立阶段以后Function Execution Context fa的变量对象表示为:

    fa.VO = {
        arguments: {
            0:'aaaaa',
            length: 1
        },
        aa:'aaaaa',
    }

    以后进入Function Execution Context fa的执行阶段:console.log(aa);打印出fa.VO.aa(形参aa)的值aaaaa;因为var aa;仅声明而未赋值,因此不会改变fa.VO.aa的值,因此下一个console.log(aa);打印出的仍然是fa.VO.aa(形参aa)的值aaaaa

    function fb(bb){
        console.log(bb);//bbbbb
        var bb = 'BBBBB';
        console.log(bb);//BBBBB
    }
    fb('bbbbb');

    调用fb函数时,进入Function Execution Context fb的建立阶段,根据前面所说的变量对象建立过程:

    先建立arguments对象,而后扫描函数的全部形参,以后会扫描函数内全部的函数声明,再扫描函数内的变量声明;
    扫描函数内的变量声明时,若是变量名与已经声明的形参或函数相同,此时什么都不会发生,变量声明不会干扰已经存在的这个同名属性

    因此建立阶段以后Function Execution Context fb的变量对象表示为:

    fb.VO = {
        arguments: {
            0:'bbbbb',
            length: 1
        },
        bb:'bbbbb',
    }

    以后进入Function Execution Context fb的执行阶段:console.log(bb);打印出fb.VO.bb(形参bb)的值'bbbbb';遇到var bb = 'BBBBB';fb.VO.bb的值将被赋为BBBBB,因此下一个console.log(bb);打印出fb.VO.bb(局部变量bb)的值BBBBB

  • 函数和变量都会声明提高,函数名和变量名同名时,函数名的优先级要高。

    console.log(cc);//ƒ cc(){}
    var cc = 1;
    function cc(){}

    根据Global Execution Context的建立阶段中建立变量对象的过程:是先扫描函数声明,再扫描变量声明,且变量声明不会影响已存在的同名属性。因此在遇到var cc = 1;这个声明语句以前,global.VO.ccƒ cc(){}

  • 执行代码时,同名函数会覆盖只声明却未赋值的变量,可是它不能覆盖声明且赋值的变量

    var cc = 1;
    var dd;
    function cc(){}
    function dd(){}
    console.log(cc);//1
    console.log(dd);//ƒ dd(){}

    Global Execution Context的建立阶段以后,Global Execution Context的变量对象能够表示为:

    global.VO = {
        cc:pointer to function cc(),
        dd:pointer to function dd()
    }

    而后进入Global Execution Context的执行阶段,遇到var cc = 1;这个声明赋值语句后, global.VO.cc将被赋值为1;而后再遇到var dd这个声明语句,因为仅声明未赋值,因此不改变global.VO.dd的值;因此console.log(cc);打印出1console.log(dd);打印出ƒ dd(){}

  • 局部变量也会声明提高,能够先使用后声明,不影响外部同名变量

每一个Execution Context都会有变量建立这个过程,因此会有声明提高;根据做用域链,若是局部变量与外部变量同名,那么最早找到的是局部变量,影响不到外部同名变量

相关资料

JavaScript基础系列---变量及其值类型
Understanding Scope in JavaScript
What is the Execution Context & Stack in JavaScript?
深刻探讨JavaScript的执行环境和栈
做用域原理
JavaScript执行环境 + 变量对象 + 做用域链 + 闭包

相关文章
相关标签/搜索