理解JavaScript的函式呼叫和this

edithfang發表於2014-06-12

多年以來,我看到了許多人對於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

譯文連結: http://blog.jobbole.com/70745/

評論(0)

相關文章