過去不少年裏,我看到過太多關於JavaScript函數調用的混淆。尤爲是,不少人抱怨函數調用中this的語義使人困惑。
在我看來,經過理解核心函數調用原語,而後將其餘全部調用函數的方法視爲在原語之上的語法糖,如此即可澄清不少這類疑惑。事實上,這正是ECMAScript規範對此的見解。在某些方面,這篇文章是規範的簡化,但基本思路是同樣的。javascript
首先,咱們先看一下函數調用的核心原語,Function對象的call
方法[1]。調用方法方法相對簡單。java
argList
)thisValue
this
的值設爲thisValue
和argList
做爲其參數列表調用函數舉例:閉包
function hello(thing) { console.log(this + " says hello " + thing); } hello.call("Yehuda", "world") //=> Yehuda says hello world
如你所見,咱們經過將this
設置爲「Yehuda」
和單個參數「world」
來調用hello
方法。這正是JavaScript中函數調用的核心原語。你能夠認爲全部其餘方式的函數調用均可」去糖「獲得這個原語。(「去糖」是指採用一種方便的語法並用更基本的核心原語來描述它)。 app
[1]在ES5規範中,call
方法是用另外一個更底層的原語來描述的,但它是在那個原語之上的簡單封裝,因此我在這裏簡化了一下。有關更多信息,請參閱本文末尾。函數
顯而易見,一直用call
調用函數將會很是煩人。JavaScript容許咱們直接使用括號語法hello("world")
來調用函數。當咱們這樣作時,調用「去糖」以下:this
function hello(thing) { console.log("Hello " + thing); } // this: hello("world") // desugars to: hello.call(window, "world");
僅在使用嚴格模式[2]的ECMAScript 5中,此行爲將改變:spa
// this: hello("world") // desugars to: hello.call(undefined, "world");
簡短版本的說法是:像fn(...args)
這樣的函數調用和fn.call(window [ES5-strict: undefined], ...args)
是如出一轍的。
注意,對於行內聲明的函數(function() {})()
也是成立的:(function() {})()
和(function() {}).call(window [ES5-strict: undefined)
是如出一轍的。prototype
[2]事實上,我撒了一點小謊。ECMAScript 5規範說undefined
(幾乎)老是被傳遞,但不在嚴格模式下時被調用函數應該將其thisValue
更改成全局對象。這容許嚴格模式下調用者避免破壞現有的非嚴格模式庫。code
調用方法的下一個很是廣泛的方式是做爲一個對象的一個成員 (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
是一個類Array對象,它表示傳遞給函數的全部參數。其次,apply
方法的工做原理和call
原語除了它採用類Array對象而不是一次列出一個參數以外徹底同樣。
咱們的bind
方法簡單地返回一個新函數。當它被調用時,咱們的新函數只是調用傳入的原始函數,並將原始值設置爲其this
值,固然它也傳遞參數。
由於這是一個有點常見的習慣用法,ES5在全部Function
對象上引入了一個新方法bind
,實現了此行爲:
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中大量使用匿名回調函數,所以它在內部使用call
方法將這些回調的this
值設置爲更有用的值。舉個例子,在全部事件處理程序中(如不進行特殊干預),jQuery不接收window
做爲其this
值,而是經過把設置事件處理程序的元素做爲它第一個參數在回調函數上調用call
。
這很是有用,由於匿名回調函數中的默認this
的值並非特別有用,除了它給初學者對javascript的一種印象,this
一般是一個奇怪的,常常變更至於難以解釋的概念。
若是你理解了將「含糖」函數調用轉換爲「已去糖」的func.call(thisValue, ...args)
的基本規則,那麼你應該可以在並非那麼危險的JavaScriptthis
水域中航行。
在個別地方,我從規範的確切措辭中略微簡化了事實。可能最嚴重的欺騙是我稱呼func.call
爲原語的說法。實際上,規範有一個func.call
和[obj.]func()
都使用的原語(內部稱爲[[Call]]
)。
然而,仍是看一下func.call
的定義吧:
IsCallable(func)
值爲false
,則拋出TypeError異常argList
爲一個空的ListargList
的最後一個元素thisArg
做爲this
的值,並將argList
做爲參數列表,返回調用func的內部方法[[Call]]
的結果如你所見,此定義本質上是一種很簡單的JavaScript語義綁定到原語[[Call]]
操做。
若是你看一下調用函數的定義,前七個步驟設置thisValue
和argList
,最後一步是:「提供thisArg
做爲this
的值,並將列表argList
做爲參數值,返回調用func的內部方法[[Call]]
的結果。」
一旦肯定了argList
和thisValue
,它基本上是相同的措辭。
我在稱call
是一個原語時做了一些欺騙,但其含義基本上與我在文章開頭提出的規範和引用的章節是同樣的。
還有一些我沒有在這裏介紹的其餘案例(最值得注意的是with
)。