完全理解JavaScript中的this關鍵字
前言
王福朋老師的 JavaScript原型和閉包系列 文章看了不下三遍了,最為一個初學者,每次看的時候都會有一種 "大徹大悟" 的感覺,而看完之後卻總是一臉懵逼。原型與閉包 可以說是 JavaScirpt 中理解起來最難的部分了,當然,我也只是瞭解到了一些皮毛,對於 JavaScript OOP 更是缺乏經驗。這裡我想總結一下 Javascript 中的 this
關鍵字,王福朋老師的在文章裡也花了大量的篇幅來講解 this
關鍵字的使用,可以說 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
在函式內部,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. 函式呼叫 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 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
,通過 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
值。
總結
this
關鍵字的值取決於其所處的位置(上下文環境):
-
在全域性環境中,
this
的值指向全域性物件( window 或 global )。 -
在函式內部,
this
的取值取決於其所在函式的呼叫方式,也就是說this
的值是在函式被呼叫的時候確定的,在建立函式時無法確定。當然,箭頭函式是個例外,箭頭函式本身不存在this
,而在箭頭函式中使用this
獲取到的便是建立其的上下文中的this
。同時,使用函式的繼承方法call
、apply
和bind
會修改this
的指向。但值得注意的是,使用bind
方法會使this
的值永久的繫結到給定的物件,無法再通過呼叫call
和apply
方法修改this
的值,箭頭函式呼叫call
、apply
或bind
方法無法修改this
。
參考: