1. 程式人生 > 其它 >重學前端(6) JavaScript執行(三):你知道現在有多少種函式嗎?

重學前端(6) JavaScript執行(三):你知道現在有多少種函式嗎?

函式   第一種,普通函式:用 function 關鍵字定義的函式。   第二種,箭頭函式:用 => 運算子定義的函式。   第三種,方法:在 class 中定義的函式。
示例:
class C {
    foo(){
    //code
    }
}

 

第四種,生成器函式:用 function * 定義的函式。   第五種,類:用 class 定義的類,實際上也是函式。
class Foo {
    constructor(){
        //code
    }
}
  第六 / 七 / 八種,非同步函式:普通函式、箭頭函式和生成器函式加上 async 關鍵字。  
async function
foo(){ // code } const foo = async () => { // code } async function foo*(){ // code }

 

  ES6 以來,大量加入的新語法極大地方便了我們程式設計的同時,也增加了很多我們理解的心智負擔。要想認識這些函式的執行上下文切換,我們必須要對它們行為上的區別有所瞭解。     對普通變數而言,這些函式並沒有本質區別,都是遵循了“繼承定義時環境”的規則,它們的一個行為差異在於 this 關鍵字。   this 關鍵字的行為   this 是 JavaScript 中的一個關鍵字,它的使用方法類似於一個變數(但是 this 跟變數的行為有很多不同,上一節課我們講了一些普通變數的行為和機制,也就是 var宣告和賦值、let 的內容)。   this 是執行上下文中很重要的一個組成部分。同一個函式呼叫方式不同,得到的 this 值也不同,我們看一個例子:
function
showThis(){ console.log(this); } var o = { showThis: showThis } showThis(); // global o.showThis(); // o
  在這個例子中,我們定義了函式 showThis,我們把它賦值給一個物件 o 的屬性,然後嘗試分別使用兩個引用來呼叫同一個函式,結果得到了不同的 this 值。   普通函式的 this 值由“呼叫它所使用的引用”決定,其中奧祕就在於:我們獲取函式的表示式,它實際上返回的並非函式本身,而是一個 Reference 型別(記得我們在型別一章講過七種標準型別嗎,正是其中之一)。   Reference 型別由兩部分組成:一個物件和一個屬性值。不難理解 o.showThis 產生的 Reference 型別,即由物件 o 和屬性“showThis”構成。   當做一些算術運算(或者其他運算時),Reference 型別會被解引用,即獲取真正的值(被引用的內容)來參與運算,而類似函式呼叫、delete 等操作,都需要用到Reference 型別中的物件。   在這個例子中,Reference 型別中的物件被當作 this 值,傳入了執行函式時的上下文當中。   至此,我們對 this 的解釋已經非常清晰了:呼叫函式時使用的引用,決定了函式執行時刻的 this 值。
實際上從執行時的角度來看,this 跟面向物件毫無關聯,它是與函式呼叫時使用的表示式相關。 這個設計來自 JavaScript 早年,通過這樣的方式,巧妙地模仿了 Java 的語法,但是仍然保持了純粹的“無類”執行時設施。 如果,我們把這個例子稍作修改,換成箭頭函式,結果就不一樣了:
const showThis = () => {
    console.log(this);
}
var o = {
    showThis: showThis
}
showThis(); // global
o.showThis(); // global
我們看到,改為箭頭函式後,不論用什麼引用來呼叫它,都不影響它的 this 值。   接下來我們看看“方法”,它的行為又不一樣了:
class C {
    showThis() {
        console.log(this);
    }
}
var o = new C();
var showThis = o.showThis;
showThis(); // undefined
o.showThis(); // o
  這裡我們建立了一個類 C,並且例項化出物件 o,再把 o 的方法賦值給了變數 showThis。   這時候,我們使用 showThis 這個引用去呼叫方法時,得到了 undefined。   所以,在方法中,我們看到 this 的行為也不太一樣,它得到了 undefined 的結果。   按照我們上面的方法,不難驗證出:生成器函式、非同步生成器函式和非同步普通函式跟普通函式行為是一致的,非同步箭頭函式與箭頭函式行為是一致的this 關鍵字的機制   說完了 this 行為,我們再來簡單談談在 JavaScript 內部,實現 this 這些行為的機制,讓你對這部分知識有一個大概的認知。   函式能夠引用定義時的變數,如上文分析,函式也能記住定義時的 this,因此,函式內部必定有一個機制來儲存這些資訊。   在 JavaScript 標準中,為函式規定了用來儲存定義時上下文的私有屬性 [[Environment]]。   當一個函式執行時,會建立一條新的執行環境記錄,記錄的外層詞法環境(outer lexical environment)會被設定成函式的 [[Environment]]。   這個動作就是切換上下文了,我們假設有這樣的程式碼:
var a = 1;
foo();
在別處定義了 foo:
var b = 2;
function foo(){
    console.log(b); // 2
    console.log(a); // error
}
  這裡的 foo 能夠訪問 b(定義時詞法環境),卻不能訪問 a(執行時的詞法環境),這就是執行上下文的切換機制了。   JavaScript 用一個棧來管理執行上下文,這個棧中的每一項又包含一個連結串列。如下圖所示:   

 

 

  當函式呼叫時,會入棧一個新的執行上下文,函式呼叫結束時,執行上下文被出棧。   而 this 則是一個更為複雜的機制,JavaScript 標準定義了 [[thisMode]] 私有屬性。   [[thisMode]] 私有屬性有三個取值。
  • lexical:表示從上下文中找 this,這對應了箭頭函式。
  • global:表示當 this 為 undefined 時,取全域性物件,對應了普通函式。
  • strict:當嚴格模式時使用,this 嚴格按照呼叫時傳入的值,可能為 null 或者 undefined。
非常有意思的是,方法的行為跟普通函式有差異,恰恰是因為 class 設計成了預設按 strict 模式執行。 我們可以用 strict 達成與上一節中方法的例子一樣的效果:
"use strict"
function showThis(){
    console.log(this);
}
var o = {
    showThis: showThis
}
showThis(); // undefined
o.showThis(); // o
  函式建立新的執行上下文中的詞法環境記錄時,會根據 [[thisMode]] 來標記新紀錄的 [[ThisBindingStatus]] 私有屬性。   程式碼執行遇到 this 時,會逐層檢查當前詞法環境記錄中的 [[ThisBindingStatus]],當找到有 this 的環境記錄時獲取 this 的值。   這樣的規則的實際效果是,巢狀的箭頭函式中的程式碼都指向外層 this,例如:
var o = {}
o.foo = function foo(){
    console.log(this);
    return () => {
        console.log(this);
        return () => console.log(this);
    }
}
o.foo()()(); // o, o, o
這個例子中,我們定義了三層巢狀的函式,最外層為普通函式,兩層都是箭頭函式。 這裡呼叫三個函式,獲得的 this 值是一致的,都是物件 o。   JavaScript 還提供了一系列函式的內建方法來操縱 this 值,下面我們來了解一下。操作 this 的內建函式   Function.prototype.call 和 Function.prototype.apply 可以指定函式呼叫時傳入的 this 值,示例如下:
function foo(a, b, c){
    console.log(this);
    console.log(a, b, c);
}
foo.call({}, 1, 2, 3);
foo.apply({}, [1, 2, 3]);
  這裡 call 和 apply 作用是一樣的,只是傳參方式有區別。   此外,還有 Function.prototype.bind 它可以生成一個繫結過的函式,這個函式的 this 值固定了引數:
function foo(a, b, c){
    console.log(this);
    console.log(a, b, c);
}
foo.bind({}, 1, 2, 3)();
  有趣的是,call、bind 和 apply 用於不接受 this 的函式型別如箭頭、class 都不會報錯。   這時候,它們無法實現改變 this 的能力,但是可以實現傳參。 new 與 this 我們在之前的物件部分已經講過 new 的執行過程,我們再來看一下:   以構造器的 prototype 屬性(注意與私有欄位 [[prototype]] 的區分)為原型,建立新物件;   將 this 和呼叫引數傳給構造器,執行;   如果構造器返回的是物件,則返回,否則返回第一步建立的物件。   

 

 

顯然,通過 new 呼叫函式,跟直接呼叫的 this 取值有明顯區別。