JS词法环境 - 用一个实例讲清楚词法环境和执行上下文

函数调用过程分析

var a = 2;

function foo() {
    console.log(a)
}

function bar(){
    var a = 5;
    foo()
}

bar()   //2

我们通过分析函数调用过程,来看看,为什么foo() 引用的是全局的a而不是bar里的a。

全局代码运行

  1. 全局运行上下文初始化:

    看起来是这样的:

  2. var声明和函数声明扫描scan:

    • 扫描var 声明:“var a = 2;”
    • 扫描到函数声明:“function foo() {console.log(a)}”
    • 扫描到函数声明:"function bar(){ var a = 5;foo()}"


      这时候整个环境看起来是这样的:

  3. 执行语句
  • 执行语句“a = 2;”

  • 执行调用语句:bar()

    bar()运行以后,上述讲到,会携带undefined作为thisArg,开始进入函数代码的运行。

进入函数代码

函数代码的执行和global的执行类似,也遵循我们的运行模型:

运行模型

运行代码 = 运行上下文初始化 + var声明和函数声明扫描scan + 执行语句;
  1. 初始化函数的运行上下:

    • 创建一个新的词法环境(Lexical Enviroment):localEnviroment

      • 使localEnviroment的outer为函数的'先天作用域'----函数对象的[[scope]]的值。
    • 创建一个新的运行上下文(Execution Context): barExecutionContext
    • 使得barExecutionContextt的LexicalEnvironment和VariableEnvironment 为localEnviroment
    • 判断携带进来的thisArg的值:

      • 如果是strict,使barExecutionContext.ThisBinding = thisArg;
      • 不是strict

        *   如果thisArg是undefined,使barExecutionContext.ThisBinding = globalobject;
        *   如果thisArg不是undefined,使barExecutionContext.ThisBinding = toObject(thisArg);
        
    • 这时整个环境看起来是这样的:

      • 整个过程简化来来是说:用函数自身创建时候携带的词法环境为“父”,创建一个函数自己的词法环境。
      • 图中虚线的意思,就是outer的实际的指向。函数运行时候的词法环境的outer指向了函数创建时的词法环境。而我们知道bar函数在全局运行上下文上创建的,创建时的词法环境为全局词法环境(GlobalEnvironment)。因此outer实际是指向全局词法环境。
      • 所以这里你应该清楚了,函数运行时的词法环境由两部分组成:“先天” + “后天”,先天就是函数创建时的词法环境,后天就是运行时新创建的词法环境,两个链在一块:

        我为什么一直强调"函数创建时的词法环境",因为这个很重要:就是函数运行时的词法环境和它被调用时那一刹那的词法环境无关,而只与它被创建时的词法环境相关。

        好了,bar的运行上下文创建完了,接着开始扫码函数里的代码。

  2. var声明和函数声明扫描scan:

    • 扫描到var声明:“var a = 5;”

      • 把a登记到当前的词法环境
      //注意:此次在栈顶的是bar的运行上下文
      //所以getRunningExecutionContext().LexicalEnvironment返回的是bar函数的词法环境
      var currentEnvironment = Runtime.getRunningExecutionContext().LexicalEnvironment;
      currentEnvironment.EnvironmentRecord.initialize('a',2);

      这时图上看是这样的:

      bar里面只有一个声明,接着执行语句。

  3. 执行语句

    • 执行语句:a = 5;

    • 执行函数调用foo(): 和执行bar过程类似,不再赘述,创建一个新的运行上下文,并进入栈顶

      从图中,我们看一看出,foo运行时的词法环境和foo刚刚被调用那时刻的词法环境没关系。只和它创建时的词法环境相关。

      当foo中执行语句:“console.log(a)”时候,会去当前的词法环境查找a,图中可以看出,当前词法环境是空的,因此就找当前词法环境的outer---也就是函数创建时的词法环境(保存在函数内部属性[[scope]]中),也就是全局词法环境,找到了a:2,因此打印2。

函数运行完返回的动作

函数运行完毕的返回值,分两种情况:

  • new 调用:

    • 无return语句或者有return语句但返回值不是对象:返回新创建的对象
    • 有return语句且返回值是对象:返回指定的值
  • 其他调用方式

    • 如果函数无return 语句,返回undefined

      • 有return语句,则返回return语句的值

返回后,把函数的运行上下文出栈。

最终把运行栈清空:

看图中的对象结构,已经没代码引用它们。它们孤零零的存在内存中,后续就会被js引擎的垃圾回收机制给清除。


作者:G哥讲码堂
来源:掘金

你可能感兴趣的:(javascript,作用域,作用域链)