【譯】理解JavaScript中函式呼叫和this
過去幾年,我經常聽到很多人對JavaScript 函式呼叫的談論,尤其是對其中this指向是困惑的。
在我看來,通過深入瞭解函式呼叫的核心概念,這些困惑都是可以消除的,其他形式的呼叫都是其核心的語法糖。事實上,ECMAScript規範就是這樣認為的。這篇部落格在很多地方其實就是簡單的規範而已,不過基本概念都是相通的。
The Core Primitive(核心原始地呼叫)
首先,來看一下核心原始地呼叫:函式call的方法[1]
。call的方法相對直接明瞭。來看一下過程:
thisValue
例如:
function hello(thing) { console.log(this + " says hello " + thing); } hello.call("Yehuda", "world") //=> Yehuda says hello world 複製程式碼
如你所見,呼叫hello方法是把this
指向"Yehuda"
,同時給它傳遞了一個簡單"world"
的引數。這就是核心原始的函式呼叫的形式。你可以認為其它所有的函式呼叫其原理都是通過call的形式來實現的(其它形式都是call的語法糖,語法糖是指用一個更方便語法和一個更基本的核心原生術語描述它)
[1] 在 ECMAScript 5規範中,call方法用另外一種更加底層的原生的方法描述。但是它真是一個非常輕的包裝, 所以我在這裡簡化了一點。想了解更多資訊請看文章末尾。
簡單的函式呼叫
明顯地,每次都用call方法呼叫函式有點煩人。所有JavaScript允許我們通過這種模式的語法hello("world")
呼叫函式。當我們這樣呼叫時,它內部語法還是用call的形式
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: 內部實現,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.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" 複製程式碼
為了理解this,你只需要知道這兩個點。首先arguments
是一個類陣列的物件,它代表了傳遞給函式的所有引數。第二,apply的方法準確地說像call的底層實現,只是把那個類陣列物件用一個接一個引數代替。
我們bind
的方法簡單地返回了一個新的函式。當它被呼叫的時候,新的函式又呼叫原始函式,並把this指向原始值,它也可以傳遞引數。
因為這個方法是常用的,故 ES5 給所有Function
物件定義了一個新的方法bind
,實現瞭如下呼叫:
var boundHello = person.hello.bind(person); boundHello("world") // "Brendan Eich says hello world"this都是指向person的 複製程式碼
這是非常實用的,當你把一個原始函式作為一個回撥的時候:
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的所有事件操作中(假設你沒有進行特殊的操作),DOM呼叫回撥函式,this總是指向那個DOM元素。
這是非常有用的,因為在匿名函式中預設的this是沒有什麼特殊作用的。但這使得那些剛剛學習JavaScript的人將難以理解this。
如果你明白將函式呼叫改寫為func.call(thisValue, ...args)
這樣簡單的方法(去除語法糖),將不會在確定JavaScript中this值的過程中迷失。
PS(附言): 我撒謊了
在很多地方,我稍微簡化了規範裡面一些確切的點。最明顯的地方就是我將func.call的方法當作一個原生函式呼叫方法。事實上,規範裡面明確了func.call
和[obj.]func()
都是通過一個叫[[Call]]
的原始方法實現的。
當然,看看fun.call(thisArg, arg1, arg2, ...)
的定義:
- 如果不能當做函式呼叫,則丟擲型別錯誤。
- 引數列表為空
- 如果傳了不止一個引數,則按從左到右的arg1, arg2的順序把這些值傳遞給函式作為引數列表(除了this)
- 使用呼叫者提供的this值和引數呼叫該函式的返回值。this指向thisArg,arguments指向後面的引數組成的list
如上所述,這個定義本質就是一個簡單的JavaScript繫結原始[[Call]]
的操作。
你可以回顧一下這個函式呼叫的定義,首先幾步是建立thisValue
和argList
,把this指向thisValue
,把arguments指向了argList
,最後一步,呼叫內部函式,返回結果。這和那個原始的最初的呼叫是基本相同的。
我撒謊稱call是最原始的函式呼叫方式,但它的內部呼叫實現基本和規範裡面說的原始呼叫形式是相同的。
另外還有一些額外的this指向的例子沒有說明,像with。
額外的
看到了JavaScript設計模式與開發實踐的總結:
除去不常用的with和eval的情況,具體到實際應用中,this的指向大致可以分為以下4種
- 作為物件的方法呼叫:指向函式直接所在的那個物件
- 作為普通函式呼叫(this就是改寫為call的第一個引數)
- 構造器呼叫(new):指向新生成的那個物件例項
- Function.prototype.call 或 Function.prototype.apply 呼叫
此外,還有箭頭函式中,this就是箭頭函式相鄰外層的那個this(也是一個引數)
如果有錯誤或者不嚴謹的地方,請務必給予指正,十分感謝!