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
關鍵字的使用分為兩種情況:
- 全域性上下文的
this
- 函式上下文的
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. 函式呼叫 apply
、call
、 bind
時
當一個函式在其主體中使用 this
關鍵字時,可以通過使用函式繼承自Function.prototype
的 call
或 apply
方法將 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`
複製程式碼
與 apply
、call
不同,使用 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
最終指向的是那個呼叫它的物件” 這句話就不通用了,函式呼叫call
、apply
、bind
方法是一個特殊情況。下面還有一種特殊情況:箭頭函式。
5. 箭頭函式
箭頭函式是 ES6 語法的新特性,在箭頭函式中,this
的值與建立箭頭函式的上下文的 this
一致。
在全域性程式碼中,this
的值為全域性物件:
var foo = (() => this)
//在瀏覽器中
foo() === window // true
// 在node中
foo() === global // true
複製程式碼
其實箭頭函式並沒有自己的 this
。所以,呼叫 this
時便和呼叫普通變數一樣在作用域鏈中查詢,獲取到的即是建立此箭頭函式的上下文中的 this
。若建立此箭頭函式的上下文中也沒有 this
,便繼續沿著作用域鏈往外查詢,直到全域性作用域,這時便指向全域性物件(window
/ global
)。
當箭頭函式在建立其的上下文外部被呼叫時,箭頭函式便是一個閉包,this
的值同樣與原上下文環境中的 this
的值一致。由於箭頭函式本身是不存在 this
,通過 call
、 apply
或 bind
修改 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
複製程式碼
箭頭函式 foo
的 this
被設定為建立時的上下文(在上面程式碼中,也就是全域性物件)的 this
值,而且無法通過其他呼叫方式設定 foo
的 this
值。
與普通函式對比,箭頭函式的 this
值是在函式建立時確定的,而且無法通過呼叫方式重新設定 this
值。普通函式中的 this
值是在呼叫的時候確定的,可通過不同的呼叫方式設定 this
值。
“一個問題”的解答
回到開篇的問題上,輸出結果為:
// undefined
// undefined
// undefined
複製程式碼
因為箭頭函式是在物件 obj1
內部建立的,在物件內部屬於全域性上下文(注意只有全域性上下文和函式上下文),this
同樣是指向全域性物件,即箭頭函式的 this
指向全域性物件且無法被修改。
在全域性物件中,沒有定義變數 a
,所以便輸出三個了 undefined
。
const obj1 = {
a: `a in obj1`,
foo: () => { console.log(this.a) }
}
複製程式碼
總結
this
關鍵字的值取決於其所處的位置(上下文環境):
-
在全域性環境中,
this
的值指向全域性物件(window
或global
)。 -
在函式內部,
this
的取值取決於其所在函式的呼叫方式,也就是說this
的值是在函式被呼叫的時候確定的,在建立函式時無法確定(詳解:this關鍵字)。以下四種呼叫方式:-
全域性中呼叫:指向全域性物件
window
/global
,foo
相當於window.foo
在嚴格模式下有所不同; -
作為物件的方法屬性:指向呼叫函式的物件,在呼叫繼承的方法時也是如此;
-
new 關鍵字呼叫:建構函式只不過是一個函式名稱第一個字母大寫的普通函式而已,在用
new
關鍵字呼叫時,this 指向新建立的物件; -
call / apply / bind:
call
/apply
/bind
可以修改函式的this
指向,bind
繫結的this
指向將無法被修改。
當然,箭頭函式是個例外,箭頭函式本身不存在
this
,而在箭頭函式中使用this
獲取到的便是建立其的上下文中的this
。 -
參考: