4. 完全搞懂javascript-函数的运行

上一篇咱们了解到了函数在不一样状况下是如何被建立的,如今咱们来探讨当函数被调用后作了什么?javascript

回忆一下第二章java

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

可运行代码(Executable Code)

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

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

运行模型

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

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

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

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方式

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

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

有没有发现,在影厅里里面,你们在同一个环境,可是每一个人带的"食品"不同。这个影厅里的"食品",就是函数里的"this"。ui

那这五种调用方式,在进入函数代码运行以前,携带进去的,要做为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 方式调用: 传递新建立的对象
相关文章
相关标签/搜索