js解惑-函式執行順序

村口墫一郎發表於2018-08-10

背景

JS基礎知識溫習。

函式解析原理

分3個階段:

  1. 準備(Hoisting)
  2. 裝載(填充資料)
  3. 執行(逐行處理)

準備

本階段就是書本中所說的Hoisting,包括:形參變數建立函式體內變數提升建立函式申明建立

就是先把函式中所有的變數或者宣告的函式名都先定義好,空間都開闢好。

關於準備階段的特別說明:

  1. 如果變數已經定義過,則不會重新定義(比如:形參中有個引數a,並且呼叫函式時傳了值進來,這時候函式中還有個變數a,那麼在這一階段,函式中的變數a是不會重新定義,形參中的值也不會被覆蓋。);

裝載

這裡裝載填充的資料包括:形參申明的函式體

也許你要問了,為什麼一般的變數只是拿到前面定義好,此時值是 undefined,填充資料需要等到執行那一行才進行,而 形參申明的函式 在程式碼執行前就要裝載好呢?

答: 我個人的理解是(有更專業的解釋,歡迎批評指正):

  • 形參 是外部傳進來的,是函式在執行前就已經知道的資料,所以直接就裝載上;而對於函式中普通的變數,受限於JS解析順序的機制影響,只能等到具體執行到那一行時才能知道。
  • 函式申明 為什麼要放到前面去呢?這應該也是JS的策略吧,不然函式表示式(var xxx=fn)為啥就沒這個待遇呢?

關於裝載的特別說明:

  1. 資料裝載的資料為:函式形參 > 函式申明
  2. 對於函式宣告裝載時,如果已經有相同的函式名的話,會覆蓋前面的(比如:形參中有個引數a,並且外部給他賦了值,這時候函式中如果有個函式申明a,那麼在這一階段,形參中的a就會被覆蓋為這個函式)。

執行

通過上面的2個階段,大家就知道,當函式真正一行行開始執行的時候,其實有些值已經存在了,並不是大家想象中的全部為 undefined

本階段就是純粹的執行程式碼了,執行就包括了:變數賦值、物件呼叫等等。

但是本階段其實JS引擎還做了另外一件事情,就是:程式碼檢查。如果報錯了,會直接中斷程式,除非使用 try/catch 捕獲。

示例一

function test() {
    console.log('1、a=',a);
    var a = b = 123;
    console.log('2、a=',a);
    console.log('3、b=',b);
};
test();
console.log('4、b=',b);
console.log('5、a=',a);
複製程式碼

分析

第一步:準備

變數提升(Hoisting),這一步執行後,實際的程式碼變為:

function test() {
    var a;
    console.log('1、a=',a);
    b=123;
    a=b;
    console.log('2、a=',a);
    console.log('3、b=',b);
};
test();
console.log('4、b=',b);
console.log('5、a=',a);
複製程式碼

補充說明: 關於 var a = b = 123 的解釋,請移步: JS解惑-連等表示式

第二步:裝載

由於函式沒有形參和函式申明,所以該步直接跳過。

第三步:執行

//1 var a;
//2 console.log('1、a=',a);
//3 b=123;
//4 a=b;
//5 console.log('2、a=',a);
//6 console.log('3、b=',b);
複製程式碼
  • //1 變數定義:a,預設值:undefined
  • //2 列印變數:a,輸出:1、a= undefined
  • //3 給變數b賦值為123,由於變數b在test函式中未定義,所以js引擎就會預設在全域性物件window所對應的物件下面建立屬性b,並且為其賦值為123,window.b=123;
  • //4 將b的值賦給a,這時候a就等於123,a=123;
  • //5 列印變數:a,輸出:2、a= 123
  • //6 列印變數:b,輸出:3、b= 123

最後:函式執行完後,區域性變數立即銷燬,全域性變數仍然保留。

//7 console.log('4、b=',b);
//8 console.log('5、a=',a);
複製程式碼
  • //7 由於b為全域性變數,執行完函式後未被銷燬,所以輸出:4、b= 123
  • //8 由於a在函式執行後已經銷燬,而全域性變數又沒有a,所以列印時就報錯了;

最終結果

js解惑-函式執行順序

補充說明: 錯誤型別為 ReferenceError 引用錯誤,也就是說系統根本不知道哪個物件或者函式下面的屬性a,所以會報這個錯誤。如果這時候你列印 window.a,那麼結果將是 undefined 而不會報錯。

示例二

function test(a) {
    console.log('1、a=',a);
    var a=123;
    console.log('2、a=',a);
    function a(){};
    console.log('3、a=',a);
};
test(1);
複製程式碼

分析

第一步:準備

定義變數a,程式碼變為:

function test(a) {
    var a;
    console.log('1、a=',a);
    a=123;
    console.log('2、a=',a);
    function a(){};
    console.log('3、a=',a);
};
複製程式碼

第二步:裝載

  1. 先將形參的值1賦值給a,a=1;
  2. 將函式申明賦值給a,a=function(){};

裝載完畢後,程式碼變為:

function test(a) {
    var a = function(){};
    console.log('1、a=',a);
    a=123;
    console.log('2、a=',a);
    console.log('3、a=',a);
};
複製程式碼

第三步:執行

看到第二步裝載完畢後的程式碼,那麼結果也就很清楚了。

js解惑-函式執行順序

最終結果

js解惑-函式執行順序

示例三

function test(a,b){
    console.log('1、a=',a);
    c=0;
    var c;
    console.log('2、c=',c);
    a=3;
    b=2;
    console.log('3、b=',b);
    function b(){};
    function d(){};
    console.log('4、b=',b);
};
test(1);
複製程式碼

分析

準備

找到所有的區域性變數,注意包含形參中的,包括:

  • a,來自:形參
  • b,來自:形參、函式申明
  • c,來自:區域性變數
  • d,來自:函式申明

於是原函式就變為:

function test(a,b){
    var a;
    var b;
    var c;
    var d;

    console.log('1、a=',a);
    c=0;
    console.log('2、c=',c);
    a=3;
    b=2;
    console.log('3、b=',b);
    function b(){};
    function d(){};
    console.log('4、b=',b);
};
test(1);
複製程式碼

裝載

注意裝載的順序:形參先裝載,其次是函式宣告,而且函式申明會覆蓋已定義的變數。

於是函式就變成為:

function test(){
    var a=1;
    var b=function(){};//實參並沒有傳第2個引數,預設為undefined;但後來又被函式申明覆蓋了。
    var c=undefined;//沒有地方為其賦值
    var d=function(){};

    console.log('1、a=',a);
    c=0;
    console.log('2、c=',c);
    a=3;
    b=2;
    console.log('3、b=',b);
    console.log('4、b=',b);
};
test(1);
複製程式碼

執行

先看看裝載後的函式的執行結果:

js解惑-函式執行順序

所以,其實這類題目最難的就是分析階段,包括:(準備、裝載),一旦這2個階段處理好,執行階段基本就是直接列印結果了。

最終結果

js解惑-函式執行順序

示例四

最後一道帶一些邏輯,可能會影響到分析階段的。

function test(a,b){
    console.log('2、b=',b);
    if(a){
        var b=100;
    };
    console.log('3、b=',b);
    c=456;
    console.log('4、c=',c);
};
var a;
console.log('1、a=',a);
test();
a=10;
console.log('5、a=',a);
console.log('6、c=',c);
複製程式碼

分析

準備

這類題目,先不用管函式體外的程式碼,因為函式準備和裝載,跟外部程式碼怎麼執行沒關係。

  • a,來自形參、區域性變數;
  • b,來自形參、區域性變數;(注意:函式的準備階段,是不用管是否有if的,只要看到var b,就一定會提前)

對於變數前沒有var申明的,說明是全域性變數,不用理會。

於是程式碼變為:

function test(a,b){
    var a;
    var b;

    console.log('2、b=',b);
    if(a){
        b=100;
    };
    console.log('3、b=',b);
    c=456;
    console.log('4、c=',c);
};
var a;
console.log('1、a=',a);
test();
a=10;
console.log('5、a=',a);
console.log('6、c=',c);
複製程式碼

裝載

裝載階段依然只管函式體內。

function test(){
    var a=undefined;
    var b=undefined;

    console.log('2、b=',b);
    if(a){
        b=100;
    };
    console.log('3、b=',b);
    c=456;
    console.log('4、c=',c);
};
var a;
console.log('1、a=',a);
test();
a=10;
console.log('5、a=',a);
console.log('6、c=',c);
複製程式碼

執行

先增加一個行號:

function test(){
      var a=undefined;
      var b=undefined;

//7   console.log('2、b=',b);
      if(a){
         b=100;
      };
//8   console.log('3、b=',b);
      c=456;
//9   console.log('4、c=',c);
};
//1 var a;
//2 console.log('1、a=',a);
//3 test();
//4 a=10;
//5 console.log('5、a=',a);
//6 console.log('6、c=',c);
複製程式碼
  • 第1行、第2行,由於沒有test參與,所以結果直接就列印:a=undefined;
  • 第3行開始進入函式體內;
  • 第7行,b=undefined;
  • 第8行,因為上方if(a)條件為假,所以b並沒有賦值100,所以:b=undefined;
  • 第9行,由於區域性並沒有變數c,於是就找全域性變數c,這時候恰恰是上方賦值的c=456,所以這時候:c=456;
  • 函式體內執行完畢,第4行,給外層函式的區域性變數a賦值為10
  • 第5行,a=10;
  • 第6行,由於在函式體內賦值了一個全域性變數c=456,函式執行完並沒有銷燬,所以這裡:c=456;

最終結果

js解惑-函式執行順序

彩蛋

通過以上講解,中間穿插了一些基礎知識,這裡跟大家簡要總結分享下。

函式的定義

函式的定義有三種形式:

  • 函式申明

    function fun1(){};
    複製程式碼
  • 函式表示式

    var fun2 = function(){};
    複製程式碼
  • 建構函式Function

    var fun3 = new Function("a", "b", "return a * b");
    複製程式碼

變數提升(Hoisting)

就是將函式體內的 區域性變數函式申明 放到函式的最前面定義。

連等表示式

請移步: JS解惑-連等表示式

什麼是形參、實參

function fun4(var1,var2){//函式結構體括號內的變數,就叫做形參。
    //TODO
};
fun4('abc',123);//呼叫函式時,實際傳的值就叫做實參。
複製程式碼

GO和VO物件是什麼

  • VO,Variable Object,變數物件。這是一個偽物件,是用在函式體內,在函式 準備 階段時,用來存放準備的資料的,這些資料包括3類:形參、變數和函式宣告。
    • VO物件,不能直接在函式中訪問,因為其實它只是一種說法而已,用來表示資料的儲存行為的。
    • 所以,上文中就沒提這個概念,你要用的時候,直接就訪問函式的變數了,跟這個物件沒啥關係,大家也可以不用太關心,只需要瞭解即可。
  • GO,Global Object,全域性物件。這個概念是跟VO有點對立,就是全域性存在的一個物件,這個物件一般指的是 window 物件。

參考

(全文完)

相關文章