本文由尤慕譯自Understanding JavaScript Function Invocation and "this".
多年來腥光,我都有看到大家對 js 函數(shù)調(diào)用的一些困惑雅采。尤其是克蚂,許多人都會抱怨函數(shù)調(diào)用中的this語義含糊不清。
依我的觀點(diǎn)旋廷,只要理解了核心的函數(shù)調(diào)用原語(the core function invocation primitive),把其它各種類型的函數(shù)調(diào)用看作建立在該原語之上的語法糖急前,
這些困惑就能迎刃而解。實(shí)際上宅广,這正是ECMAScript規(guī)范思考的方式葫掉。本文是對規(guī)范的一種簡化描述,但基礎(chǔ)思想是一致的跟狱。
The Core Primitive: 核心原語
首先俭厚,我們來看核心的函數(shù)調(diào)用原語:Function 對象的call方法。該方法比較直觀(譯者注,call 即調(diào)用)驶臊。
- 從入?yún)⒌牡?位(從0開始)到最后挪挤,構(gòu)造出一個參數(shù)列表(argList)
- 第0個入?yún)⑹?em>thisValue
- 將函數(shù)的this綁定到thisValue,函數(shù)的參數(shù)綁定到argList,然后調(diào)用該函數(shù)
例如:
function hello(thing) {
console.log(this + " says hello " + thing);
}
hello.call("Yehuda", "world") //=> Yehuda says hello world
如你所見叼丑,調(diào)用hello方法時,this被綁定到 "Yehuda" ,入?yún)⑹?/em>"world"扛门。這便是 js 函數(shù)調(diào)用的核心原語鸠信。你可以認(rèn)為,其它類型的方法調(diào)用都會轉(zhuǎn)換成這種原語(即desugar
: 將方便的語法轉(zhuǎn)換成使用原語描述的形式)论寨。
Simple Function Invocation:簡單的函數(shù)調(diào)用
顯然星立,調(diào)用函數(shù)時總是使用call是很煩人的一件事。js 允許我們直接通過括號語法進(jìn)行函數(shù)調(diào)用(如:hello("world")
)葬凳。我們看它是如何轉(zhuǎn)換成原語的:
function hello(thing) {
console.log("Hello " + thing);
}
// 我們這樣寫:
hello("world")
// 會被轉(zhuǎn)換成:
hello.call(window, "world");
在ECMAScript 5中绰垂,這種行為在strict mode作了一些變化:
// 我們這樣寫:
hello("world")
// 會被轉(zhuǎn)換成:
hello.call(undefined, "world");
簡而言之就是,像fn(...args)
這樣的函數(shù)調(diào)用和fn.call(window [ES5-strict: undefined], ...args)
是互通的火焰。
注意這同樣適用于內(nèi)聯(lián)函數(shù): (function() {})()
和(function() {}).call(window [ES5-strict: undefined)
是一樣的劲装。
Member Functions:成員函數(shù)(方法)
另一種常見的函數(shù)調(diào)用,是調(diào)用的函數(shù)是作為對象的成員而存在(person.hello()
)荐健。這種情況下酱畅,轉(zhuǎn)換為原語描述:
var person = {
name: "Brendan Eich",
hello: function(thing) {
console.log(this + " says hello " + thing);
}
}
// 我們這樣寫:
person.hello("world")
// 會轉(zhuǎn)換成:
person.hello.call(person, "world");
注意,我們不用考慮此類調(diào)用中hello方法是如何綁定到object對象上的(編譯器會幫我們處理)江场。上一例的hello是被定義為一個獨(dú)立的函數(shù)纺酸。接下來我們看看如果將hello動態(tài)的綁定到 對象上會發(fā)生什么:
function hello(thing) {
console.log(this + " says hello " + thing);
}
person = { name: "Brendan Eich" }
person.hello = hello;
person.hello("world") // 仍然會被轉(zhuǎn)換為 person.hello.call(person, "world")
hello("world") // "[object DOMWindow]world"
注意到?jīng)],函數(shù)的this并非是個固定不變的值址否,而是在運(yùn)行時由調(diào)用者所決定餐蔬。
Using Function.prototype.bind:使用Function.prototype.bind
有時候需要保持函數(shù)的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的調(diào)用仍然會轉(zhuǎn)換為boundHello.call(window, "world")
,我們其實(shí)是繞了個彎樊诺,用原始的call方法將this設(shè)定為我們需要的值。
我們可以將這種技巧抽象的更具普適性:
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"
要理解這段代碼音同,需要另外知道兩點(diǎn)信息词爬。其一,arguments是一個類數(shù)組對象权均,用來裝載傳遞給一個函數(shù)的所有參數(shù)顿膨。其二,apply方法和call方法工作方式幾乎相同叽赊,不同點(diǎn)是前者的參數(shù)列表是一個類數(shù)組對象恋沃,后者的參數(shù)列表是一個參數(shù)一個位置。
我們的bind方法只是簡單的返回一個新的函數(shù)必指。調(diào)用這個返回的函數(shù)時囊咏,該函數(shù)只是簡單的調(diào)用原來傳入的函數(shù),并將第二個傳的參數(shù)thisValue設(shè)為該函數(shù)的this。當(dāng)然梅割,它還會將bind剩余的參數(shù)傳入給調(diào)用bind所返回的那個函數(shù)霜第。
由于該技巧在 js 中已經(jīng)成為習(xí)語,ES5引入了一個新方法bind,讓所有函數(shù)對象都擁有此方法:
var boundHello = person.hello.bind(person);
boundHello("world") // "Brendan Eich says hello world"
當(dāng)需要將一個函數(shù)作為回調(diào)傳遞時户辞,bind尤其有用:
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
總是bind bind bind的庶诡,難免顯得笨拙。TC39(負(fù)責(zé)ECMAScript下一版的委員會)咆课,正在開發(fā)一個更優(yōu)雅的、向后兼容的解決方案(譯者注(2016-11-08): 目前有兩種方式解決這種問題扯俱,一是es6的arrow functions以及es7的function bind operator)书蚪。
On jQuery:說說 jQuery
jQuery大量使用了匿名回調(diào)函數(shù),它在內(nèi)部會使用call來將回調(diào)的this綁定到一個更有用的值上迅栅。例如殊校,事件處理器的this不是指向window,jQuery會通過call將其設(shè)定到事件處理器所綁定的元素。
這是極其有用的读存,因為匿名回調(diào)函數(shù)的默認(rèn)this往往沒什么用處为流,還會給js 初學(xué)者這樣一種印象,即让簿,this通常是一個怪異的敬察、可變的、難以推理的概念尔当。
如果你掌握了如何將一個普通函數(shù)轉(zhuǎn)換成原語描述的形式(func.call(thisValue, ...args)
),你應(yīng)該能順利走出 js this 的迷宮了莲祸。
PS: I Cheated ==> 附言: 我撒了謊
文中的幾處,我對規(guī)范中的瑣言碎語進(jìn)行了簡化椭迎∪裰模可能最大的欺騙之處即是我將func.call稱作"原語(primitive)"。實(shí)際上畜号,規(guī)范中是到的原語叫[[Call]]
, 不管是func.call還是 [obj.]func() ,都會使用該原語缴阎。
但是,我們看看規(guī)范中對func.call的定義(譯者注:規(guī)范的語言用英文讀更容易懂一些简软,這里不再進(jìn)行中文翻譯):
- If
IsCallable(func)
is false, then throw a TypeError exception. - Let
argList
be an empty List. - If this method was called with more than one argument then in left to right order starting with arg1 append each argument as the last element of
argList
- Return the result of calling the
[[Call]]
internal method of func, providingthisArg
as the this value and argList as the list of arguments.
如上所述蛮拔,這個定義本質(zhì)上就是js語言對原語[[Call]]的綁定說明。
如果你看了函數(shù)調(diào)用的說明替饿,前7步是設(shè)定thisValue和argList,最后一步是:
Return the result of calling the [[Call]] internal method on func, providing thisValue as the this value and providing the list argList as the argument values.
(對 func 調(diào)用內(nèi)部的[[Call]]
方法语泽,將this指向thisValue,參數(shù)指向argList)"
規(guī)范中的語言十分繁瑣,主是處理好argList和thisValue视卢。
關(guān)于call作為原語踱卵,我撒了點(diǎn)謊,但是它和規(guī)范的本質(zhì)是相同的。
注意,對于一些額外的情況(比如with
),本文沒有涉及惋砂。