完全理解JavaScript中的this關鍵字

正偉_發表於2019-02-22

this關鍵字

前言

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

作者:正偉

原文連結:this關鍵字

注:原創文章,轉載請註明出處

一個問題

一道很常見的題目:下面程式碼將會輸出的結果是什麼?

const obj1 = {
  a: `a in obj1`,
  foo: () => { console.log(this.a) }
}

const obj2 = {
  a: `a in obj2`,
  bar: obj1.foo
}

const obj3 = {
  a: `a in obj3`
}

obj1.foo()  // 輸出 ??
obj2.bar()  // 輸出 ??
obj2.bar.call(obj3)  // 輸出 ??
複製程式碼

在弄明白 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 都指向全域性物件(window / global)。在物件的內部,也是在全域性上下文,this 同樣指向全域性物件(window / global)

window.a = 10
var obj = {
  x: this.a,
  _this: this
}
obj.x  // 10
obj._this === this  // 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
}

foo() // undefined
複製程式碼

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

在生成 上下文環境 時

  • 若方法被 window(或 global )物件呼叫,即執行 window.foo(),那 this 將會被定義為 window(或 global );
  • 若被普通物件呼叫,即執行 obj.foo(),那 this 將會被定義為 obj 物件;(後面會討論)
  • 但若未被物件呼叫(上面分別是被 window 物件和普通物件 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 ),即呼叫方法的物件。(注意嚴格模式的不同)

函式在全域性上下文中呼叫, foo() 可以看作是 window.foo(),只不過在嚴格模式下有所限制。

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)
  console.log(this === tang)
}

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

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

也可以看作是物件 tang 呼叫了 getInfo 方法,this 指向了 tang。即 this 指向了呼叫它的那個物件。

參考:《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 {...}
複製程式碼

建構函式同樣可以看作是一個普通的函式(只不過函式名稱第一個字母大寫了而已咯),但是在用 new 關鍵字呼叫建構函式建立物件時,它與普通函式的行為不同罷了。

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 f = function () {
  console.log(this.name)
}

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

var g = f.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`
複製程式碼

到這裡,“this 最終指向的是那個呼叫它的物件” 這句話就不通用了,函式呼叫 callapplybind 方法是一個特殊情況。下面還有一種特殊情況:箭頭函式

5. 箭頭函式

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

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

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

其實箭頭函式並沒有自己的 this。所以,呼叫 this 時便和呼叫普通變數一樣在作用域鏈中查詢,獲取到的即是建立此箭頭函式的上下文中的 this。若建立此箭頭函式的上下文中也沒有 this,便繼續沿著作用域鏈往外查詢,直到全域性作用域,這時便指向全域性物件(window / global)。

當箭頭函式在建立其的上下文外部被呼叫時,箭頭函式便是一個閉包,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 值。

“一個問題”的解答

回到開篇的問題上,輸出結果為:

// undefined
// undefined
// undefined
複製程式碼

因為箭頭函式是在物件 obj1 內部建立的,在物件內部屬於全域性上下文(注意只有全域性上下文和函式上下文),this 同樣是指向全域性物件,即箭頭函式的 this 指向全域性物件且無法被修改。

在全域性物件中,沒有定義變數 a,所以便輸出三個了 undefined

const obj1 = {
  a: `a in obj1`,
  foo: () => { console.log(this.a) }
}
複製程式碼

總結

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

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

  2. 在函式內部,this 的取值取決於其所在函式的呼叫方式,也就是說 this 的值是在函式被呼叫的時候確定的,在建立函式時無法確定(詳解:this關鍵字)。以下四種呼叫方式:

    1. 全域性中呼叫:指向全域性物件 window / globalfoo 相當於 window.foo 在嚴格模式下有所不同;

    2. 作為物件的方法屬性:指向呼叫函式的物件,在呼叫繼承的方法時也是如此;

    3. new 關鍵字呼叫:建構函式只不過是一個函式名稱第一個字母大寫的普通函式而已,在用 new 關鍵字呼叫時,this 指向新建立的物件;

    4. call / apply / bindcall/apply/bind 可以修改函式的 this 指向,bind 繫結的 this 指向將無法被修改。

    當然,箭頭函式是個例外,箭頭函式本身不存在 this,而在箭頭函式中使用 this 獲取到的便是建立其的上下文中的 this


參考:

相關文章