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 方式調用: 傳遞新建立的對象
相關文章
相關標籤/搜索