點燈坊

失くすものさえない今が強くなるチャンスよ

ECMAScript 之 this

Sam Xiao's Avatar 2020-11-24

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,則 thiswindow

this000

Object

let foo = function() {
  return this
}

let obj = {
  foo
}

obj.foo() // ?

若 function 為掛在 Object 上的 method,則 this 指向 Object。

因為是 foo() 透過 obj 執行,所以 execution context 是 Object。

this001

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

this002

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)

this003

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。

this005

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。

this011

class Foo {
  constructor(name) {
    this.name = name
  }
}

let foo = new Foo('Sam')
foo.name // ?

ES6 迎來了 class 語法,costructor function 則由 constructor() method 取代,this 也如同一般 OOP 指向 Object。

this012

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 為 windowthis 不再如普通 function 內代表 obj

this004

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 內我們希望存取 objincrement property,直覺會使用 this

this006

最後結果為不預期的 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,如此則可存取 objincrement property。

this008

這種寫法缺點是 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。

this009

這種寫法優點是 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。

this010

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

this007

Conclusion

  • this 在 ECMAScript 頗為混亂,但仍應對 this 有清楚觀念,畢竟 this 已經成為 ECMAScript 的 feature
  • this 對於 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