深刻理解JavaScirpt的函數調用和"this"

過去不少年裏,我看到過太多關於JavaScript函數調用的混淆。尤爲是,不少人抱怨函數調用中this的語義使人困惑。
在我看來,經過理解核心函數調用原語,而後將其餘全部調用函數的方法視爲在原語之上的語法糖,如此即可澄清不少這類疑惑。事實上,這正是ECMAScript規範對此的見解。在某些方面,這篇文章是規範的簡化,但基本思路是同樣的。javascript

核心原語

首先,咱們先看一下函數調用的核心原語,Function對象的call方法[1]。調用方法方法相對簡單。java

  1. 從參數1到末尾建立一個參數列表(argList)
  2. 第一個參數(參數0)是thisValue
  3. 經過將this的值設爲thisValueargList做爲其參數列表調用函數

舉例:閉包

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

因爲jQuery中大量使用匿名回調函數,所以它在內部使用call方法將這些回調的this值設置爲更有用的值。舉個例子,在全部事件處理程序中(如不進行特殊干預),jQuery不接收window做爲其this值,而是經過把設置事件處理程序的元素做爲它第一個參數在回調函數上調用call
這很是有用,由於匿名回調函數中的默認this的值並非特別有用,除了它給初學者對javascript的一種印象,this一般是一個奇怪的,常常變更至於難以解釋的概念。
若是你理解了將「含糖」函數調用轉換爲「已去糖」的func.call(thisValue, ...args)的基本規則,那麼你應該可以在並非那麼危險的JavaScriptthis水域中航行。

PS:我撒謊的部分

在個別地方,我從規範的確切措辭中略微簡化了事實。可能最嚴重的欺騙是我稱呼func.call爲原語的說法。實際上,規範有一個func.call[obj.]func()都使用的原語(內部稱爲[[Call]])。
然而,仍是看一下func.call的定義吧:

  1. 若是IsCallable(func)值爲false,則拋出TypeError異常
  2. argList爲一個空的List
  3. 若是使用多個參數調用此方法,則從arg1開始,從左往右將每一個參數追加爲argList的最後一個元素
  4. 提供thisArg做爲this的值,並將argList做爲參數列表,返回調用func的內部方法[[Call]]的結果

如你所見,此定義本質上是一種很簡單的JavaScript語義綁定到原語[[Call]]操做。
若是你看一下調用函數的定義,前七個步驟設置thisValueargList,最後一步是:「提供thisArg做爲this的值,並將列表argList做爲參數值,返回調用func的內部方法[[Call]]的結果。」
一旦肯定了argListthisValue,它基本上是相同的措辭。
我在稱call是一個原語時做了一些欺騙,但其含義基本上與我在文章開頭提出的規範和引用的章節是同樣的。
還有一些我沒有在這裏介紹的其餘案例(最值得注意的是with)。

原文地址

相關文章
相關標籤/搜索