多年以來,我看到了許多人對於JavaScript函式喚起有很多困惑。特別是許多人會抱怨,”this”在函式喚起中的語意是令人疑惑的。
在我看來,透過理解核心的函式喚起的原始模型,並且去看一下在此基礎之上的其他方式的函式喚起(對原始喚起的思想的抽取)可以消除這些困惑。實際上,ECMAScript 標準也是這麼考慮的。在某些地方來看,這篇文章是標準的簡化,但是二者的基本思想是一致的。
核心的原始函式喚起方法
首先,讓我們來看一下核心的函式喚起原始模型,一個Function的call方法[1]。call方法相對比較直接。
1、取引數的第一個到最後一個組成一個引數串列(argList);
2、第一個引數是thisValue;
3、把this設定為thisValue同時argList作為它的引數串列來喚起函式。
比如:
function hello(thing) {
console.log(this + ” says hello ” + thing);
}
hello.call(“Yehuda”, “world”) //=> Yehuda says hello world
正如你所見,我們喚起了hello函式,把this設定為”Yehuda” 並傳入了一個引數”world”。這是JavaScript函式喚起的主要原始形式。你可以把所有其他的函式喚起作為這個原始樣式的運用來考慮。(要“運用”原始模型來喚起其他函式就要用更便利的語法並依據一個更基本的主要原始模型)
註:[1]在ES5標準中,call方法的描述基於其他的,更低水平的基元,但是它是在那個基元基礎上的非常簡單的包裹,因此我在這裡將其簡化了。想瞭解更多可以參考這篇文章後面的資訊。
簡單的函式喚起
很明顯,總是用call來喚起函式是令人難以忍受的。JavaScript允許我們用括號語法來直接喚起函式(hello(“world”))。當我們這麼做的時候,喚起是這樣的:
function hello(thing) {
console.log(“Hello ” +thing);
}
// this:
hello(“world”)
// desugars to:
hello.call(window, “world”);
在ECMAScript 5 中,在嚴格樣式下這個行為已經發生了變化[2]:
// this:
hello(“world”)
// desugars to:
hello.call(undefined, “world”);
簡短的一個版本說明是:一個函式喚起比如:fn(…args)與fn.call(window [ES5-strict: undefined], …args)是一樣的。
註意,對於行內的函式宣告(function() {})() 與(function() {}).call(window [ES5-strict: undefined)也是一樣的。
註:[2] 實際上,我撒了點謊。ECMAScript 5 標準說undefined(幾乎)總是被傳入,當不在嚴格樣式下時,被喚起的函式應該改變this的值為全域性物件。這允許嚴格樣式的喚起者避免打破已經存在的非嚴格樣式庫。
成員函式
下麵一種非常常用的函式喚起方式是函式作為一個物件的方法成員來喚起(person.hello())。這種情況下函式喚起像這樣:
var person = {
name: “Brendan Eich”,
hello: function(thing) {
console.log(this + ” says hello ” + thing);
}
}
// this:
person.hello(“world”)
// desugars to this:
person.hello.call(person, “world”);
註意,這和hello方法以這種形式附加到物件之後會變得怎樣是無關的。記住,我們之前定義hello為一個獨立的函式。讓我們來看看動態的把函式附加到物件上發生了什麼:
function hello(thing) {
console.log(this + ” says hello ” + thing);
}
person = { name: “Brendan Eich” }
person.hello =hello;
person.hello(“world”) // still desugars to person.hello.call(person, “world”)
hello(“world”) // “[object DOMWindow]world”
註意,函式並沒有”this”的一個持久的概念。他總是在被喚起的時候基於喚起者喚起它的方式被設定。
應用Function.prototype.bind
由於對一個擁有持久的this的值的函式的取用有時候是非常方便的,歷史上人們用了一個閉包把戲把一個函式轉化為了擁有不變的this值:
var person = {
name: “Brendan Eich”,
hello: function(thing) {
console.log(this.name + ” says hello ” + thing);
}
}
var boundHello = function(thing) { return person.hello.call(person, thing); }
boundHello(“world”);
儘管我們的boundHello 方法仍然可以改寫為boundHello.call(window, “world”) ,我們轉換了一個角度,應用我們的基元call方法來改變this為我們期望的值。
我們可以用自製體系來使得這個竅門有一般用途:
var bind = function(func, thisValue) {
return function() {
return func.apply(thisValue, arguments);
}
}
var boundHello = bind(person.hello, person);
boundHello(“world”) // “Brendan Eich says hello world”
為了理解上面的程式碼,你只需要兩個額外的資訊。首先,arguments是一個類陣列物件,它擁有傳到函式裡的所有引數的取用。第二,apply方法的工作機制和基元call是完全一樣的,唯一的不同是它採用的一個類陣列的物件來作為引數,而不是用引數串列。
我們的 bind方法簡單的傳回一個新函式。當它被喚起的時候,我們的新函式簡單的喚起傳進來的原始函式,設定原始值為this。它也遍歷引數。
因為this在某種程度上是一個常見的習語,ES5引入了一個新的bind方法給所有的Function物件來實現下麵的行為:
var boundHello = person.hello.bind(person);
boundHello(“world”) // “Brendan Eich says hello world”
當你需要一個未加工的函式作為回呼函式的時候這是非常有用的:
var person = {
name: “Alex Russell”,
hello: function() { console.log(this.name + ” says hello world”); }
}
$(“#some-div”).click(person.hello.bind(person));
// when the div is clicked, “Alex Russell says hello world” is printed
當然,這個實現有點笨重,而且TC39(負責ECMAScript下一個版本的委員會)正在實現一個更加優雅的且向後相容的解決方案。
jQuery裡面的bind
因為jQuery裡面大量的應用匿名回呼函式,它內部使用call方法來設定那些回呼函式的this值為更有用的值。比如,在所有的事件處理器函式中,jQuery沒有接收window作為this的值(如果你沒有特殊的幹預),而是對元素喚起call方法,並將事件處理器函式作為第一個引數。
這極其有用,因為在匿名函式內部的this的預設值並不是特別有用,但是它會給JavaScript初學者一個這樣的感覺:this一般是很奇怪的,並且是難以推測的經常變化的一個概念。
如果你理解了從一個有語法糖的函式喚起到抽取出了“糖分”的函式喚起func.call(thisValue, …args)的基本轉換規則,你應該就能操縱這個並不是十分“陰險”的 JavaScript this 值這一領域。
附:我有所‘欺騙’
在幾個地方,對於規範的措辭我有所簡化。或許最重要的‘欺騙’是我將func.call稱為一個基元(”primitive”)。實際上,這個規範有一個基元(在內部被稱為[[Call]])為func.call和obj.]func()所共有。
然而,讓我們來看一下func.call的定義:
1、如果IsCallable(func) 結果為false,那麼就丟擲一個型別異常;
2、讓 argList 為一個空串列;
3、如果這個方法被喚起的時候引數不止一個,那麼從左到右開始將arg1追加每一個引數作為 argList 的最新元素;
4、傳回喚起func的內部方法[[Call]]的執行結果,提供thisArg作為this的值,argList作為引數的串列。
正如你所見,這個定義本質上是一個很簡單的JavaScript的語言系結到基元[[Call]]運運算元。
如果你看一下函式喚起的定義,前七步是設定thisValue和argList,最後一步是:“傳回 喚起func的內部方法 [[Call]]的結果值,提供thisArg作為this的值,argList作為引數的串列”。
一旦thisValue和argList的值被確定,func.call的定義和函式喚起的定義本質上是相同的字眼。
我在稱call為一個基元上做了一點欺騙,但是在本質上他們意思還是一樣的,我在文章開頭拿出規範且做了取用。
還有很多案例(大多數文章會明顯的包含with)我沒有在文章中進行討論。
英文出處: Yehuda Katz
譯文出處: 伯樂線上 – abell123