原標題:Understanding JavaScript Function Invocation and "this",出自Yehuda的這篇部落格,是在Typescript的中文教程裡看到的。
JS的函式呼叫一直以來給不少人帶來疑惑,其中this
的語義是人們抱怨的最多的。
在我看來,首先理解了函式呼叫的原始核心語法,然後弄清楚其他呼叫函式的語法糖,這些疑惑就能解決了。實際上這正式ECMA規範所設計的思路。在某種程度上,這篇文章是ECMA規範的簡化版,不過基本理念都是一樣的。
核心原始碼
首先來看JS函式呼叫的核心,Function
類的call
方法【1】。call
方法的邏輯很直白:
- 把從第二個起的所有引數放進一個引數列表,如
argList
中 - 把第一個引數定為
thisValue
- 執行
function
,把this
指向thisValue
,argList
作為引數列表
例如:
function hello(thing) {
console.log(this + " says hello " + thing);
}
hello.call("Yehuda", "world") //=> Yehuda says hello world
複製程式碼
可以看到,在執行hello
函式時我們把this
指向"Yehuda"
,傳入單個引數"world"
。這就是JS函式呼叫的核心原始碼。你可以把其他的函式呼叫的語法都看成是這個原始碼的語法糖。
【1】在ES5規範中,
call
還是另一個更低層次的原始碼的語法糖,但包裝得並不複雜,所以在這裡直接簡化了。文末有更多資料。
簡單的函式呼叫
顯然每次都用call
來呼叫函式太累贅了。JS允許我們直接使用括號來呼叫函式,如hello("world")
,這個就是一個語法糖了:
function hello(thing) {
console.log("Hello " + thing);
}
// this:
hello("world")
// desugars to:
hello.call(window, "world");
複製程式碼
在ES5的嚴格模式(strict mode)下,有一點小小的改動:【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】實際上原作者說他撒了個小謊。ES5規範說的是給
thisValue
所繫結的幾乎都是undefined
(The ECMAScript 5 spec says that undefined is (almost) always passed),但他認為不在嚴格模式時thisValue
應該繫結到global
物件。This allows strict mode callers to avoid breaking existing non-strict-mode libraries.
成員函式
另一個常見的場景是呼叫一個物件的成員函式,如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
函式即可。現在來看下把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
指向,開發者會使用閉包來實現這個目的:
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("world")
最終會解析成boundHello.call(window, "world")
,但之前的操作已經把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
函式簡單返回一個新的函式。在呼叫bind()
時,它又會呼叫之前傳參進去的函式,並且把後者的this
繫結到第二個引數。
因為這種用法也很常見,所以ES5引入了一個新的方法bind
,適用於所有Function
類物件,效果如下:
var boundHello = person.hello.bind(person);
boundHello("world") // "Brendan Eich says hello world"
複製程式碼
如果你需要寫一個(帶有this
,但其指向有特定需要的)回撥函式,這種寫法就很有用:
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
本段不做翻譯。
後記(編者作)
有好幾處的描述我對原本的規範描述做了簡化,其中最關鍵的一點是我把func.call
稱為原始碼(primitive)。實際上在規範裡,function.call
和[obj.]func()
還有更深一層的原原始碼。
但來看一下ES5標準中func.call
的宣告(譯者:即ES5標準裡Function.prototype.call (thisArg [ , arg1 [ , arg2, … ] ] )
的描述):
- 如果
IsCallable(func)
為false
,丟擲TypeError
錯誤 - 將
argList
初始化為空陣列 - 如果這個函式傳入了多個引數,將這些引數從左往右加入到
argList
,從arg1
開始標記 - 返回撥用
func
內建方法[[call]]
的結果,呼叫時把thisArg
賦給this
,傳入argList
作為引數佇列
可以看到,這只是一段很簡單的繫結到[[call]]
操作的JS程式碼。
如果你去看呼叫函式的定義,頭7步都是在初始化thisValue
和argList
,最後一步是“返回撥用func
內建方法[[call]]
的結果,呼叫時把thisArg
賦給this
,傳入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.)”
一旦argList
和thisValue
準備好之後,之後的工作原理都是一樣的了。
所以我偷了個小懶,但我把ES5規範裡的描述都拎出來了,他們的意義都是一樣的。
還有一些其他用法,比如跟with
相關的用法,在這裡我沒有涉及。