4. 彻底搞懂javascript-函数的运行

1,989 阅读9分钟

上一篇我们了解到了函数在不同情况下是如何被创建的,现在我们来探讨当函数被调用后做了什么?

回忆一下第二章

总结上述,过程我们构建一个JS的运行模型,进入可执行代码,都会走这个运行模型:

可运行代码(Executable Code)

ECMAScript 5 规范,定义了三类可运行代码(Executable Code) ,运行这些代码时候会创建运行上下文(Execution Contexts):

  • global code:就是js整个“程序”,就是源代码文件中所有不在function体中的代码。
  • function code:就是函数体中的代码,除了内嵌函数体中的代码以外
  • eval code : 就是传给内置eval函数的代码字符串

运行模型

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

同时我们也构建运行环境模型:

/**
 * 运行环境模型伪代码
 */

Runtime = {
    executionContextStack: []
};

Runtime.getRunningExecutionContext = function() {
    return this.executionContextStack[this.executionContextStack.length - 1];
}

Runtime.pop = function() {
    this.executionContextStack.pop();
}

Runtime.push = function(newContext) {
    this.executionContextStack.push(newContext);
}

Runtime.getIdentifierVaule = function (name) {

    var env = this.getRunningExecutionContext().LexicalEnvironment;

    while(env){
        var envRec = env.EnvironmentRecord;
        var exists = envRec.isExist(name);
        if(exists) return envRec.getValue(name);
        env = env.outerEnvironmentReference;
    }
}

function ExecutionContext() {
    this.LexicalEnvironment = undefined;
    this.VariableEnvironment =  undefined;
    this.ThisBinding = undefined;
}



function LexicalEnvironment() {
    this.EnvironmentRecord = undefined;
    this.outerEnvironmentReference = undefined;
}

function EnvironmentRecord(obj) {

    if(isObject(obj)) {
        this.bindings = object;
        this.type = 'Object';
    }
    this.bindings = new Map();
    this.type = 'Declarative';
}


EnvironmentRecord.prototype.register = function(name) {
    if (this.type === 'Declarative')
        this.bindings.set(name,undefined)
    this.bindings[name] = undefined;
}

EnvironmentRecord.prototype.initialize = function(name,value) {
      if (this.type === 'Declarative')
        this.bindings.set(name,value);
    this.bindings[name] = value;
}

EnvironmentRecord.prototype.getValue = function(name) {
    if (this.type === 'Declarative')
        return this.bindings.get(name);
    return this.bindings[name];
}


function creatGlobalEnvironment(globalobject) {
	var globalEnvironment = new LexicalEnvironment();
	globalEnvironment.outer = null
	globalEnvironment.EnvironmentRecord = new EnvironmentRecord(globalobject)
	return globalEnvironment;
}

GlobalEnvironment = creatGlobalEnvironment(globalobject)//可以看作是浏览器环境下的window

函数调用的方式

函数调用分为几类:

  1. 作为函数调用:如 functionName();
  2. 作为方法调用:如someObj.method();
  3. 函数表达式调用:其实也是函数调用一种,(function(){})(),(function functionName(){})(); 这类是马上创建函数,马上调用,记得上一篇,我们提到函数表达式在执行语句的时候创建函数对象,()表示调用,所以这类也叫立即调用函数表达式(IIFE)
  4. 作为构造函数调用:new functionName() 方式的调用
  5. functionName.call 和functionName.apply方式

这几种调用方式,有什么不同呢?其实在真正进入函数代码运行之后是一样的,这几种调用方式的不同是在准备进入函数代码运行之前做的准备不一样。

就像大家去影院看电影,在进入影厅之前,有的同学买爆米花,有的同学买汉堡,有点同学买瓶奶茶,带在身上,进入影厅以后大家的流程就相同了,找排号,找座位,坐下。。。。

有没有发现,在影厅里里面,大家在同一个环境,但是每个人带的"食品"不一样。这个影厅里的"食品",就是函数里的"this"。

那这五种调用方式,在进入函数代码运行之前,携带进去的,要作为this的"东西"都是啥呢?

  1. 带undefined 进去的:函数调用functionName();和 立即调用函数表达式(function(){})(),(function functionName(){})();
  2. 带对象进去的:
    • 方法调用:如someObj.method() : 带someObj进去
    • new functionName() 方式的调用:创建一个新对象 newObject,带进去
    • functionName.call和functionName.apply:把call和apply指定thisArg带进去

进入函数代码以后呢,和global过程类似:

运行模型

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

还是这三步。

我们通过分析上一篇开头的代码来说明其过程

函数调用过程

var a = 2;

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

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

bar()//2

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

全局代码运行

  1. 全局运行上下文初始化:
//创建全局运行上下文
var globalExecutionContext = new ExecutionContext();
globalExecutionContext.LexicalEnvironment = creatGlobalEnvironment(globalobject);
globalExecutionContext.VariableEnvironment = creatGlobalEnvironment(globalobject);
globalExecutionContext.ThisBinding = globalobject;

//入栈
Runtime.push(globalExecutionContext);

//这时的Runtime = {
//    executionContextStack: [globalExecutionContext]
//}

看起来是这样的:

  1. var声明和函数声明扫描scan:
  • 扫描var 声明:“var a = 2;”

    var currentEnvironment = Runtime.getRunningExecutionContext().VariableEnvironment;
    currentEnvironment.EnvironmentRecord.register('a');
    
    
  • 扫描到函数声明:“function foo() {console.log(a)}”

        //获取当前运行上下文的词法环境
        var currentEnvironment = Runtime.getRunningExecutionContext().VariableEnvironment;
        //创建函数
        var fo = FunctionCreate([],"console.log(a)",currentEnvironment,false)//详细过程看上一篇
        currentEnvironment.EnvironmentRecord.initialize('foo',fo);
        
    
  • 扫描到函数声明:"function bar(){ var a = 5;foo()}"

        //获取当前运行上下文的词法环境
        var currentEnvironment = Runtime.getRunningExecutionContext().VariableEnvironment;
        //创建函数
        var fo = FunctionCreate([]," var a = 5;foo()",currentEnvironment,false)//详细过程看上一篇
        currentEnvironment.EnvironmentRecord.initialize('bar',fo);
    

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

  1. 执行语句

    • 执行语句“a = 2;”
        var currentEnvironment = Runtime.getRunningExecutionContext().LexicalEnvironment;
        currentEnvironment.EnvironmentRecord.initialize('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);

    模型伪代码如下:

        //创建新的运行上下文
        var barExecutionContext = new ExecutionContext();
        
        //创建一个新的词法环境(Lexical Enviroment)
        var localEnviroment = new LexicalEnvironment();
            //创建新的EnvironmentRecord
        var barEnvironmentRecord = new EnvironmentRecord();
        
        localEnviroment.EnvironmentRecord = barEnvironmentRecord
        localEnviroment.outer = [[scope]] of bar function object
        
        barExecutionContext.LexicalEnvironment = localEnviroment;
        barExecutionContext.VariableEnvironment = localEnviroment;
        barExecutionContext.ThisBinding = globalobject;//此例子中thisArg是undefined,且不是strict,所以设置为 globalobject
        
        //把函数的运行上下文入栈:
        
        Runtime.push(barExecutionContext);
        
    

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

    整个过程简化来来是说:用函数自身创建时候携带的词法环境为“父”,创建一个函数自己的词法环境。

    图中虚线的意思,就是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语句的值

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

```
//foo()运行完毕,回到bar的运行上下文
Runtime.pop();


//bar运行完毕,回到global 运行上下文
  Runtime.pop();
//global 运行上下文 已经无其他语句,弹出global全局上下文
 Runtime.pop();
```

最终把运行栈清空:

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

函数创建-函数运行

从上面的分析,我们知道函数的运行时的环境和函数创建时候的环境紧密相连,而和函数被调用时的环境没关系。这就是静态词法环境的意思(可认为就是静态作用域,因为还没谈到作用域的概念,所以用此法环境的说法)。

上篇我们提到的一种特殊情况,那就是new Function()方式创建的函数,这种方法创建的函数,函数对象中的[[scope]],永远是global词法环境。所以,不管new Function()在什么样的的环境中创建函数,其函数运行时的都是全局环境+自己函数内部的词法环境。

这就是这段代码中innerTwo()会输出1的原因:

var a = 1;

function foo() {

    var a = 2;
    function innerOne(){
        console.log(a)
    }
    
    var innerTwo = new Function("console.log(a)")
    
    var innerTree =  function (){
        console.log(a)
    }

    innerOne();
    innerTwo();
    innerTree();
    }
    foo();//2 1 2 

思考

之前笔者看帖子有小伙伴提到:

只有当整个应用程序结束的时候,ECStack 才会被清空,所以程序结束之前, ECStack >最底部永远有个 globalContext:

这时有个小伙伴针对这句话提问:

ECStack可以理解为执行栈,但是JS在处理定时器、DOM事件监听等异步事件时,会将其放入Event Table,满足触发条件后会发送到消息队列,这时候只有检测到调用栈为空的时候,才会把队列中事件放到栈中执行。你这里的意思是在整个执行过程中globalContext是一直存在的吗?那这里的矛盾应该如何解释,求教,谢谢。

意思就是既然说全局运行栈在栈底,而且程序结束的时候,全局运行栈也会被清空,而且只有运行栈为空了,事件函数才能入栈,那这时 globalContext 都不见了,事件函数里面是怎么找到全局变量的呢?

结合本篇和上篇函数的创建和调用过程,你能回答这个问题吗?

请试着解释如下代码:


var onGlobal = 'on Global ';

setTimeout(function(){
    console.log(onGlobal)
},1000);

setTimeout的回调,只有在运行栈为空时(后续我们聊到event loop 会谈到这个),才会被推入运行栈,那这时候全局运行栈不在了,回调函数如何找到onGlobal这个变量的值的呢?

总结

  1. 不同的函数调用方式会给函数传递不同的thisArg值:
    • 普通函数调用(包括立即调用函数):传递undefined
    • 对象方法:传递对象
    • new 方式调用: 传递新创建的对象