重學前端(6) JavaScript執行(三):你知道現在有多少種函式嗎?
阿新 • • 發佈:2022-04-12
函式
第一種,普通函式:用 function 關鍵字定義的函式。
第二種,箭頭函式:用 => 運算子定義的函式。
第三種,方法:在 class 中定義的函式。
實際上從執行時的角度來看,this 跟面向物件毫無關聯,它是與函式呼叫時使用的表示式相關。
這個設計來自 JavaScript 早年,通過這樣的方式,巧妙地模仿了 Java 的語法,但是仍然保持了純粹的“無類”執行時設施。
如果,我們把這個例子稍作修改,換成箭頭函式,結果就不一樣了:
示例: class C { foo(){ //code } }
第四種,生成器函式:用 function * 定義的函式。 第五種,類:用 class 定義的類,實際上也是函式。
class Foo { constructor(){ //code } }第六 / 七 / 八種,非同步函式:普通函式、箭頭函式和生成器函式加上 async 關鍵字。
async functionfoo(){ // code } const foo = async () => { // code } async function foo*(){ // code }
ES6 以來,大量加入的新語法極大地方便了我們程式設計的同時,也增加了很多我們理解的心智負擔。要想認識這些函式的執行上下文切換,我們必須要對它們行為上的區別有所瞭解。 對普通變數而言,這些函式並沒有本質區別,都是遵循了“繼承定義時環境”的規則,它們的一個行為差異在於 this 關鍵字。 this 關鍵字的行為 this 是 JavaScript 中的一個關鍵字,它的使用方法類似於一個變數(但是 this 跟變數的行為有很多不同,上一節課我們講了一些普通變數的行為和機制,也就是 var宣告和賦值、let 的內容)。 this 是執行上下文中很重要的一個組成部分。同一個函式呼叫方式不同,得到的 this 值也不同,我們看一個例子:
function在這個例子中,我們定義了函式 showThis,我們把它賦值給一個物件 o 的屬性,然後嘗試分別使用兩個引用來呼叫同一個函式,結果得到了不同的 this 值。 普通函式的 this 值由“呼叫它所使用的引用”決定,其中奧祕就在於:我們獲取函式的表示式,它實際上返回的並非函式本身,而是一個 Reference 型別(記得我們在型別一章講過七種標準型別嗎,正是其中之一)。 Reference 型別由兩部分組成:一個物件和一個屬性值。不難理解 o.showThis 產生的 Reference 型別,即由物件 o 和屬性“showThis”構成。 當做一些算術運算(或者其他運算時),Reference 型別會被解引用,即獲取真正的值(被引用的內容)來參與運算,而類似函式呼叫、delete 等操作,都需要用到Reference 型別中的物件。 在這個例子中,Reference 型別中的物件被當作 this 值,傳入了執行函式時的上下文當中。 至此,我們對 this 的解釋已經非常清晰了:呼叫函式時使用的引用,決定了函式執行時刻的 this 值。showThis(){ console.log(this); } var o = { showThis: showThis } showThis(); // global o.showThis(); // o
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。
"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 取值有明顯區別。