1. 程式人生 > >完全理解JavaScript中的this關鍵字

完全理解JavaScript中的this關鍵字

前言

原文

王福朋老師的 JavaScript原型和閉包系列 文章看了不下三遍了,最為一個初學者,每次看的時候都會有一種 "大徹大悟" 的感覺,而看完之後卻總是一臉懵逼。原型與閉包 可以說是 JavaScirpt 中理解起來最難的部分了,當然,我也只是瞭解到了一些皮毛,對於 JavaScript OOP 更是缺乏經驗。這裡我想總結一下 Javascript 中的 this 關鍵字,王福朋老師的在文章裡也花了大量的篇幅來講解 this 關鍵字的使用,可以說 this 關鍵字也是值得重視的。

上下文環境

我們都知道,每一個 "程式碼段" 都會執行在某一個 上下文環境 當中,而在每一個程式碼執行之前,都會做一項 "準備工作"

,也就是生成相應的 上下文環境,所以每一個 上下文環境 都可能會不一樣。

上下文環境 是什麼?我們可以去看王福朋老師的文章(連結在文末),講解的很清楚了,這裡不贅述了。

"程式碼段" 可以分為三種:

  • 全域性程式碼
  • 函式體
  • eval 程式碼

與之對應的 上下文環境 就有:

  • 全域性上下文
  • 函式上下文

elav 就不討論了,不推薦使用)

當然,這和 this 又有什麼關係呢?this 的值就是在為程式碼段做 "準備工作" 時賦值的,可以說 this 就是 上下文環境 的一部分,而每一個不同的 上下文環境 可能會有不一樣的 this值。

每次在尋找一個問題的解決方案或總結一個問題的時候,我總會去嘗試將這個問題進行合適的分類,而從不同的方面去思考問題。

所以,這裡我大膽的將 this 關鍵字的使用分為兩種情況:

  1. 全域性上下文的 this

  2. 函式上下文的 this

(你也可以選擇其他的方式分類。當然,這也不重要了)

全域性上下文中的 this

在全域性執行上下文中(在任何函式體外部),this 都指向全域性物件:

// 在瀏覽器中, 全域性物件是 window
console.log(this === window) // true

var a = 'Zavier Tang'
console.log(a) // 'Zavier Tang'
console.log(window.a) // 'Zavier Tang'
console
.log(this.a) // 'Zavier Tang' this.b = 18 console.log(b) // 18 console.log(window.b) // 18 console.log(this.b) // 18 // 在 node 環境中,this 指向global console.log(this === global) // true 複製程式碼

函式上下文中的 this

在函式內部,this 的值取決與函式被呼叫的方式。

this 的值在函式定義的時候是確定不了的,只有函式呼叫的時候才能確定 this 的指向。實際上 this 的最終指向的是那個呼叫它的物件。(也不一定正確)

1. 全域性函式

對於全域性的方法呼叫,this 指向 window 物件(node下為 global ):

var foo = function () {
  return this
}
// 在瀏覽器中
foo() === window // true

// 在 node 中
foo() === global //true
複製程式碼

但值得注意的是,以上程式碼是在 非嚴格模式 下。然而,在 嚴格模式 下,this 的值將保持它進入執行上下文的值:

var foo = function () {
  "use strict"
  return this
}

f2() // undefined
複製程式碼

即在嚴格模式下,如果 this 沒有被執行上下文定義,那它為 undefined

在生成 上下文環境 時:

  • 若方法被 window(或 global )物件呼叫,即執行 window.foo(),那 this 將會被定義為 window(或 global );
  • 若被普通物件呼叫,即執行 obj.foo(),那 this 將會被定義為 obj 物件;(在後面會討論)
  • 但若未被物件呼叫,即直接執行 foo(),在非嚴格模式下,this 的值預設指向全域性物件 window(或 global ),在嚴格模式下,this 將保持為 undefined

通過 this 呼叫全域性變數:

var a = 'global this'

var foo = function () {
  console.log(this.a)
}
foo() // 'global this'
複製程式碼
var a = 'global this'

var foo = function () {
  this.a = 'rename global this' // 修改全域性變數 a
  console.log(this.a)
}
foo() // 'rename global this'
複製程式碼

所以,對於全域性的方法呼叫,this 指向的是全域性物件 window (或global ),即呼叫方法的物件。(注意嚴格模式的不同)

2. 作為物件的方法

當函式作為物件的方法呼叫時,它的 this 值是呼叫該函式的物件。也就是說,函式的 this 值是在函式被呼叫時確定的,在定義函式時確定不了(箭頭函式除外)。

var obj = {
  name: 'Zavier Tang',
  foo: function () {
    console.log(this)
    console.log(this.name)
  }
}

obj.foo() // Object {name: 'Zavier Tang', foo: function}    // 'Zavier Tang'

//foo函式不是作為obj的方法呼叫
var fn = obj.foo // 這裡foo函式並沒有執行
fn() // Window {...}  // undefined
複製程式碼

this 的值同時也只受最靠近的成員引用的影響:

//接上面程式碼
var o = {
  name: 'Zavier Tang in object o',
  fn: fn,
  obj: obj
}
o.fn() // Object {name: 'Zavier Tang in object o', fn: fn, obj: obj}  // 'Zavier Tang in object o'
o.obj.foo() // Object {name: 'Zavier Tang', foo: function}    // 'Zavier Tang'
複製程式碼

在原型鏈中,this 的值為當前物件:

var Foo = function () {
  this.name = 'Zavier Tang'
  this.age = 20
}

Foo.prototype.getInfo = function () {
  console.log(this.name)
  console.log(this.age)
}

var tang = new Foo()
tang.getInfo() // "Zavier Tang"  // 20
複製程式碼

雖然這裡呼叫的是一個繼承方法,但 this 所指向的依然是 tang 物件。

參考:《Object-Oriented JavaScript》(Second Edition)

3. 作為建構函式

如果函式作為建構函式,那函式當中的 this 便是建構函式即將 new 出來的物件:

var Foo = function () {
  this.name = 'Zavier Tang',
  this.age = 20,
  this.year = 1998,
  console.log(this)
}

var tang = new Foo()

console.log(tang.name) // 'Zavier Tang'
console.log(tang.age) // 20
console.log(tang.year) // 1998
複製程式碼

Foo 不作為建構函式呼叫時,this 的指向便是前面討論的,指向全域性變數:

// 接上面程式碼
Foo() // window {...}
複製程式碼

4. 函式呼叫 applycallbind

當一個函式在其主體中使用 this 關鍵字時,可以通過使用函式繼承自Function.prototypecallapply 方法將 this 值繫結到呼叫中的特定物件。即 this 的值就取傳入物件的值:

var obj1 = {
  name: 'Zavier1'
}

var obj2 = {
  name: 'Zavier2'
}

var foo = function () {
  console.log(this)
  console.log(this.name)
}
foo.apply(obj1) // Ojbect {name: 'Zavier1'}   //'Zavier1'
foo.call(obj1) // Ojbect {name: 'Zavier1'}   //'Zavier1'

foo.apply(obj2) // Ojbect {name: 'Zavier2'}   //'Zavier2'
foo.call(obj2) // Ojbect {name: 'Zavier2'}   //'Zavier2'
複製程式碼

applycall 不同,使用 bind 會建立一個與 foo 具有相同函式體和作用域的函式。但是,特別要注意的是,在這個新函式中,this 將永久地被繫結到了 bind 的第一個引數,無論之後如何呼叫。

var foo = function () {
  console.log(this.name)
}

var obj1 = {
  name: 'Zavier1'
}
var obj2 = {
  name: 'Zavier2'
}

var g = foo.bind(obj1)
g() // 'Zavier1'

var h = g.bind(ojb2) // bind只生效一次!
h() // 'Zavier1'

var o = {
  name: 'Zavier Tang',
  f:f,
  g:g,
  h:h
}
o.f() // 'Zavier Tang'
o.g() // 'Zavier1'
o.h() // 'Zavier1'
複製程式碼

5. 箭頭函式

箭頭函式是 ES6 語法的新特性,在箭頭函式中,this 的值與建立箭頭函式的上下文的 this 一致。

在全域性程式碼中,this 的值為全域性物件:

var foo = (() => this)
//在瀏覽器中
foo() === window // true
// 在node中
foo() === global // true
複製程式碼

其實箭頭函式並沒有自己的 this。所以,呼叫 this 時便和呼叫普通變數一樣在作用域鏈中查詢,獲取到的即是建立此箭頭函式的上下文中的 this

當箭頭函式在建立其的上下文外部被呼叫時,箭頭函式便是一個閉包,this 的值同樣與原上下文環境中的 this 的值一致。由於箭頭函式本身是不存在 this,通過 callapplybind 修改 this 的指向是無法實現的。

作為物件的方法:

var foo = (() => this)

var obj = {
  foo: foo
}
// 作為物件的方法呼叫
obj.foo() === window // true

// 用apply來設定this
foo.apply(obj) === window // true
// 用bind來設定this
foo = foo.bind(obj)
foo() === window // true
複製程式碼

箭頭函式 foothis 被設定為建立時的上下文(在上面程式碼中,也就是全域性物件)的this 值,而且無法通過其他呼叫方式設定 foothis 值。

與普通函式對比,箭頭函式的 this 值是在函式建立建立確定的,而且無法通過呼叫方式重新設定 this 值。普通函式中的 this 值是在呼叫的時候確定的,可通過不同的呼叫方式設定 this 值。

總結

this 關鍵字的值取決於其所處的位置(上下文環境):

  1. 在全域性環境中,this 的值指向全域性物件( window 或 global )。

  2. 在函式內部,this 的取值取決於其所在函式的呼叫方式,也就是說 this 的值是在函式被呼叫的時候確定的,在建立函式時無法確定。當然,箭頭函式是個例外,箭頭函式本身不存在 this,而在箭頭函式中使用 this 獲取到的便是建立其的上下文中的 this。同時,使用函式的繼承方法 callapplybind 會修改 this 的指向。但值得注意的是,使用 bind 方法會使 this 的值永久的繫結到給定的物件,無法再通過呼叫 callapply 方法修改 this 的值,箭頭函式呼叫 callapplybind 方法無法修改 this

原文連結


參考: