一、函式的呼叫
全域性環境的this指向全域性物件,在瀏覽器中就是我們熟知的window物件
說到this
的種種情況,就離不開函式的呼叫,一般我們呼叫函式,無外乎以下四種方式:
- 普通呼叫,例如
foo()
。 - 作為物件方法呼叫,例如
obj.foo()
。 - 建構函式呼叫,例如
new foo()
。 - 使用
call
、apply
、bind
等方法。
除箭頭函式外的其他函式被呼叫時,會在其詞法環境上繫結this
的值,我們可以通過一些方法來指定this
的值。
- 使用
call
、apply
、bind
等方法來顯式指定this
的值。function foo() { console.log(this.a) } foo.call({a: 1}) // 輸出: 1 foo.apply({a: 2}) // 輸出: 2 // bind方法返回一個函式,需要手動進行呼叫 foo.bind({a: 3})() // 輸出: 3 複製程式碼
- 當函式作為物件的方法呼叫時,
this
的值將被隱式指定為這個物件。let obj = { a: 4, foo: function() { console.log(this.a) } } obj.foo() // 輸出: 4 複製程式碼
- 當函式配合
new
操作符作為建構函式呼叫時,this
的值將被隱式指定新構造出來的物件。
二、ECMAScript規範解讀this
上面講了幾種比較容易記憶和理解this
的情況,我們來根據ECMAScript規範來簡單分析一下,這裡只說重點,一些規範內具體的實現就不講了,反而容易混淆。
其實當我們呼叫函式時,內部是呼叫函式的一個內建[[Call]](thisArgument, argumentsList)
方法,此方法接收兩個引數,第一個引數提供this
的繫結值,第二個引數就是函式的引數列表。
ECMAScript規範: 嚴格模式時,函式內的
this
繫結嚴格指向傳入的thisArgument
。非嚴格模式時,若傳入的thisArgument
不為undefined
或null
時,函式內的this
繫結指向傳入的thisArgument
;為undefined
或null
時,函式內的this
繫結指向全域性的this
。
所以第一點中講的三種情況都是顯式或隱式的傳入了thisArgument
來作為this
的繫結值。我們來用虛擬碼模擬一下:
function foo() {
console.log(this.a)
}
/* -------顯式指定this------- */
foo.call({a: 1})
foo.apply({a: 1})
foo.bind({a: 1})()
// 內部均執行
foo[[Call]]({a: 1})
/* -------函式構造呼叫------- */
new foo()
// 內部執行
let obj = {}
obj.__proto__ = foo.prototype
foo[[Call]](obj)
// 最後將這個obj返回,關於建構函式的詳細內容可翻閱我之前關於原型和原型鏈的文章
/* -------作為物件方法呼叫------- */
let obj = {
a: 4,
foo: function() {
console.log(this.a)
}
}
obj.foo()
// 內部執行
foo[[Call]]({
a: 1,
foo: Function foo
})
複製程式碼
那麼當函式普通呼叫時,thisArgument
的值並沒有傳入,即為undefined
,根據上面的ECMAScript規範,若非嚴格模式,函式內this
指向全域性this
,在瀏覽器內就是window。
虛擬碼模擬:
window.a = 10
function foo() {
console.log(this.a)
}
foo() // 輸出: 10
foo.call(undefined) // 輸出: 10
// 內部均執行
foo[[Call]](undefined) // 非嚴格模式,this指向全域性物件
foo.call(null) // 輸出: 10
// 內部執行
foo[[Call]](null) // 非嚴格模式,this指向全域性物件
複製程式碼
根據上面的ECMAScript規範,嚴格模式下,函式內的this
繫結嚴格指向傳入的thisArgument
。所以有以下表現。
function foo() {
'use strict'
console.log(this)
}
foo() // 輸出:undefined
foo.call(null) // 輸出:null
複製程式碼
需要注意的是,這裡所說的嚴格模式是函式被建立時是否為嚴格模式,並非函式被呼叫時是否為嚴格模式:
window.a = 10
function foo() {
console.log(this.a)
}
function bar() {
'use strict'
foo()
}
bar() // 輸出:10
複製程式碼
三、箭頭函式中的this
ES6新增的箭頭函式在被呼叫時不會繫結this
,所以它需要去詞法環境鏈上尋找this
。
function foo() {
return () => {
console.log(this)
}
}
const arrowFn1 = foo()
arrowFn1() // 輸出:window
// 箭頭函式沒有this繫結,往外層詞法環境尋找
// 在foo的詞法環境上找到this繫結,指向全域性物件window
// 在foo的詞法環境上找到,並非是在全域性找到的
const arrowFn2 = foo.call({a: 1})
arrowFn2() // 輸出 {a: 1}
複製程式碼
切記,箭頭函式中不會繫結this
,由於JS採用詞法作用域,所以箭頭函式中的this
只取決於其定義時的環境。
window.a = 10
const foo = () => {
console.log(this.a)
}
foo.call({a: 20}) // 輸出: 10
let obj = {
a: 20,
foo: foo
}
obj.foo() // 輸出: 10
function bar() {
foo()
}
bar.call({a: 20}) // 輸出: 10
複製程式碼
四、回撥函式中的this
當函式作為回撥函式時會產生一些怪異的現象:
window.a = 10
let obj = {
a: 20,
foo: function() {
console.log(this.a)
}
}
setTimeout(obj.foo, 0) // 輸出: 10
複製程式碼
我覺得這麼解釋比較好理解:obj.foo
作為回撥函式,我們其實在傳遞函式的具體值,而並非函式名,也就是說回撥函式會記錄傳入的函式的函式體,達到觸發條件後進行執行,虛擬碼如下:
setTimeout(obj.foo, 0)
//等同於,先將傳入回撥函式記錄下來
let callback = obj.foo
// 達到觸發條件後執行回撥
callback()
// 所以foo函式並非作為物件方法呼叫,而是作為函式普通呼叫
複製程式碼
要想避免這種情況,有三種方法,第一種方法是使用bind
返回的指定好this
繫結的函式作為回撥函式傳入:
setTimeout(obj.foo.bind({a: 20}), 0) // 輸出: 20
複製程式碼
第二種方法是儲存我們想要的this值,就是常見的,具體命名視個人習慣而定。
let _this = this
let self = this
let me = this
複製程式碼
第三種方法就是使用箭頭函式
window.a = 10
function foo() {
return () => {
console.log(this.a)
}
}
const arrowFn = foo.call({a: 20})
arrowFn() // 輸出:20
setTimeout(arrowFn, 0) // 輸出:20
複製程式碼
五、總結
- 箭頭函式中沒有
this
繫結,this
的值取決於其建立時所在詞法環境鏈中最近的this
繫結 - 非嚴格模式下,函式普通呼叫,
this
指向全域性物件 - 嚴格模式下,函式普通呼叫,
this
為undefined
- 函式作為物件方法呼叫,
this
指向該物件 - 函式作為建構函式配合
new
呼叫,this
指向構造出的新物件 - 非嚴格模式下,函式通過
call
、apply
、bind
等間接呼叫,this
指向傳入的第一個引數這裡注意兩點:
bind
返回一個函式,需要手動呼叫,call
、apply
會自動呼叫- 傳入的第一個引數若為
undefined
或null
,this
指向全域性物件
- 嚴格模式下函式通過
call
、apply
、bind
等間接呼叫,this
嚴格指向傳入的第一個引數
有時候文字的表述是蒼白無力的,真正理解之後會發現:this
不過如此。
六、小練習
例子來自南波的JavaScript之例題中徹底理解this
// 例1
var name = 'window'
var person1 = {
name: 'person1',
show1: function () {
console.log(this.name)
},
show2: () => console.log(this.name),
show3: function () {
return function () {
console.log(this.name)
}
},
show4: function () {
return () => console.log(this.name)
}
}
var person2 = { name: 'person2' }
person1.show1() // ?
person1.show1.call(person2) // ?
person1.show2() // ?
person1.show2.call(person2) // ?
person1.show3()() // ?
person1.show3().call(person2) // ?
person1.show3.call(person2)() // ?
person1.show4()() // ?
person1.show4().call(person2) // ?
person1.show4.call(person2)() // ?
複製程式碼
選中下方檢視答案:
person1 // 函式作為物件方法呼叫,this指向物件
person2 // 使用call間接呼叫函式,this指向傳入的person2
window // 箭頭函式無this繫結,在全域性環境找到this,指向window
window // 間接呼叫改變this指向對箭頭函式無效
window // person1.show3()返回普通函式,相當於普通函式呼叫,this指向window
person2 // 使用call間接呼叫函式,this指向傳入的person2
window // person1.show3.call(person2)仍然返回普通函式
person1 // person1.show4呼叫物件方法,this指向person1,返回箭頭函式,this在person1.show4呼叫時的詞法環境中找到,指向person1
person1 // 間接呼叫改變this指向對箭頭函式無效
person2 // 改變了person1.show4呼叫時this的指向,所以返回的箭頭函式的內this解析改變
系列文章
歡迎前往閱讀系列文章,如果喜歡或者有所啟發,歡迎 star,對作者也是一種鼓勵。
菜鳥一枚,如果有疑問或者發現錯誤,可以在相應的 issues 進行提問或勘誤,與大家共同進步。