this
在 OOP 相對單純,都是代表固定 Object,但在 ECMAScript 則是嚴肅課題,主要是 ECMAScript 是以 Function 為核心,OOP 是由 Function 實現,因此 this
觀念大異其趣。
Version
ECMAScript 2015
Execution Context
this
在 ES5 稱為 execution context,也因為是 context,表示 this
並不是代表固定 Object,而會隨環境而變動。
Global
let foo = function() {
return this
}
foo() // ?
若在 global scope 的 function 使用 this
,則 this
為 window
。
Object
let foo = function() {
return this
}
let obj = {
foo
}
obj.foo() // ?
若 function 為掛在 Object 上的 method,則 this
指向 Object。
因為是 foo()
透過 obj
執行,所以 execution context 是 Object。
apply()
let foo = function() {
return this
}
let obj = {}
foo.apply(obj) // ?
apply()
為 Function.prototype.apply()
所提供,因此每個 function 都天生自帶 apply()
。
apply()
的特色是當執行 function 時,可動態傳入 Object 指定 function 的 execution context,也就是 Object 將動態取代 this
。
call()
let foo = function() {
return this
}
let obj = {}
foo.call(obj) // ?
call()
為 Function.prototype.apply()
所提供,因此每個 function 都天生自帶 call()
。
call()
的特色是當執行 function 時,可動態傳入 Object 指定 function 的 execution context,也就是 Object 將動態取代 this
。
若不傳入第二個 argument,則
apply()
與call()
功能完全相同,都是傳入 Object 取代this
,並且執行 function。若傳入第二個 argument,則
apply()
與call()
就不同:
apply()
以 Array 形式傳入 argument,如foo.apply(obj, [1, 2, 3])
call()
以傳統形式傳入 argument,如foo.call(obj, 1, 2, 3)
bind()
let foo = function() {
return this
}
let obj = {}
foo.bind(obj)() // ?
bind()
為 Function.prototype.bind()
所提供,因此每個 function 都天生自帶 bind()
。
bind()
與 apply()
與 call()
很類似,也可動態傳入 Object 指定 function 的 execution context,也就是 object 動態取代 this
。
但 apply()
與 call()
是直接執行 function,而 bind()
是回傳一個新 function。
因此 apply()
之後還要加上 ()
才能執行 function。
New
function Foo(name) {
this.name = name
}
let foo = new Foo('Sam')
foo.name // ?
ES5 為了能如 OOP 使用 new
建立 Object,當 new
遇到普通 function 時,搖身一變成為 constructor function,其 this
代表 new
所建立的 object,如此就能使用 this
存取 Object 的 property。
class Foo {
constructor(name) {
this.name = name
}
}
let foo = new Foo('Sam')
foo.name // ?
ES6 迎來了 class
語法,costructor function 則由 constructor()
method 取代,this
也如同一般 OOP 指向 Object。
Arrow Function
let obj = {
foo: () => this
}
obj.foo()
ES6 迎來新的 arrow function,但其 this
與傳統 function 觀念不一樣。
傳統 function 由執行時的 Object 決定 exection context,也就是 this
會隨環境而變 (global、Object、new),甚至 apply()
、call()
與 bind()
也可動態改變 this
。
Arrow function 的 this
則 維持使用
當時建立 arrow function 的 function 的 execution context,且一旦決定後,就不能再由 apply()
、call()
或 bind()
改變。
func()
在 obj
內以 arrow function 建立,因此其 this
固定不變,且因為 () => this
並不在任何 function 內,因此其 execution context 為 window
,this
不再如普通 function 內代表 obj
。
let obj = {
increment: 1,
foo: function(a) {
return a.map(function(x) {
return x + this.increment
})
}
}
let data = [1, 2, 3]
obj.foo(data) // ?
ECMAScript 是以 function 為核心的語言,強調 first-class function,因此 argument 常是 function。
如著名的 Array.prototype.map()
就要我們傳入 function。
第 4 行
return a.map(function(x) {
return x + this.increment
})
在 map function 內我們希望存取 obj
的 increment
property,直覺會使用 this
。
最後結果為不預期的 NaN
。
因為 map()
傳入一個全新 function,該 function 並非掛在 obj
下,因此其 this
並非指向 obj
,而是 window
。
let obj = {
increment: 1,
foo: function(a) {
var self = this
return a.map(function(x) {
return x + self.increment
})
}
}
let data = [1, 2, 3]
obj.foo(data) // ?
ES5 常常看到 var self = this
,map function 則透過 closure 讀取到 self
,間接也獲得 this
,如此則可存取 obj
的 increment
property。
這種寫法缺點是 map function 的 this
都要改成 self
。
let obj = {
increment: 1,
foo: function(a) {
return a.map(function(x) {
return x + this.increment
}.bind(this))
}
}
let data = [1, 2, 3]
obj.foo(data) // ?
另外一個方式則透過 bind()
,重新將 this
綁定到 map 的 callback。
這種寫法優點是 map function 的 this
不用修改,但 bind()
也很醜。
let obj = {
increment: 1,
foo: function(a) {
return a.map(x => x + this.increment)
}
}
let data = [1, 2, 3]
obj.foo(data) // ?
ES6 的 arrow function 就是為了解決這個問題。
Arrow function 的 this
由定義該 arrow function 的 function 決定,也就是將使用 func()
的 this
,其 this
指向 obj
,因此 map function 的 this
也繼續指向 obj
,可順利讀取到 increment
property。
Arrow function 不僅簡短,且可讀性又佳。
let obj = {
increment: 1,
foo: a => a.map(x => x + this.increment)
}
let data = [1, 2, 3]
obj.foo(data) // ?
但 foo()
不可再用 arrow function!!
因為 arrow function 沒有自己的 this
,而是由定義該 arrow function 之處決定,foo
的 arrow function 為 free function,因此 this
指向 window
,結果再次回到 NaN
。
Conclusion
this
在 ECMAScript 頗為混亂,但仍應對this
有清楚觀念,畢竟this
已經成為 ECMAScript 的 featurethis
對於 FP 而言為 implicit argument,來自於 side effect,且 function 結果會隨 context 而變,也不符合 pure function 要求,因此 FP 不會使用this
- 但
this
在 OOP 是必須的,因為 ECMAScript 就是利用 function 與this
實現 OOP,因此還是必須了解 - 實務上建議 callback 都使用 arrow function,可避免
this
指向window
Reference
Vegard Sandnes, How does the “this” keyword work in JavaScript