前言
this 是 JavaScript 中不可不談的一個知識點,它非常重要但又不容易理解。因為 JavaScript 中的 this 不同於其他語言。不同場景下的 this 指向不同(當函式被呼叫執行時會生成變數物件,確定 this 的指向,因此當前函式的 this 是在函式被呼叫執行的時候才確定的,所以導致 this 的指向靈活不確定),而且,在嚴格模式和非嚴格模式下,this 也會有不同的解讀。
為什麼要有 this
先想想如果 JavaScript 中沒有 this 會怎麼樣?比如下面這段程式碼:
function identity(context) {
return context.name.toUpperCase();
}
function speak(context) {
var greeting = `Hello, I am ` + identity(context)
console.log(greeting)
}
var you = {
name: `Reader`
}
var me = {
name: `Stone`
}
identity(you); // READER
speak(me); // Hello, I am Stone
我們給這 identity 和 speak 兩個函式顯示的傳入了一個上下文物件,這似乎看不出什麼,但是一旦你的應用變得越來越複雜,這種顯示傳遞上下文就會讓程式碼越來越混亂,程式碼結構越來越模糊。而使用 this 就可以避免這樣,因為 this 提供了一種更優雅的方式來隱式傳遞物件引用,可以把 API 設計得更加簡潔易用。
this 的繫結規則
全域性物件中的 this
全域性物件的變數物件是一個比較特殊的存在,在全域性物件中,this 指向它本身,比如:
// this 繫結到全域性物件
this.a1 = 10;
// 通過宣告繫結到變數物件,全域性環境中,變數物件就是它本身
var a2 = 20;
// 會隱式繫結到全域性物件
a3 = 30;
console.log(a1); // 10
console.log(a2); // 20
console.log(a3); // 30
函式中的 this
在一個函式的執行上下文中,this 由該函式的呼叫者提供,由函式的呼叫方式來決定 this 的指向。下面這個例子:
function fn() {
console.log(this)
}
fn(); // Window {...}
預設全域性物件就是呼叫者,等價於 window.fn()
(只討論瀏覽器中全域性物件)。但是在非嚴格模式中,this 是指向 undefined 的,比如:
`use strict`;
function fn() {
console.log(this);
}
fn(); // undefined
window.fn(); // Window {...}
這就說明了,如果不指定函式呼叫者,在嚴格模式下回預設繫結到全域性物件,在非嚴格模式下預設指向 undefined 。
函式是獨立呼叫,還是被某個物件所呼叫,是很容易分辨的,比如:
`use strict`;
var a = 20;
function fn() {
var a = 1;
var obj = {
a: 10,
c: this.a + 20
}
return obj.c
}
console.log(window.fn()); // 40
console.log(fn()); // TypeError
物件字面量的形式不會產生自己的作用域,所以 obj 中的 this.a
並不是指向 obj ,而是與函式內部的 this 一樣。因此,當 window.fn()
呼叫時,fn 內部的 this 指向 window 物件,此時 this.a
訪問全域性物件中的 a ;由於是在嚴格模式中,在沒有指明呼叫者的時候,fn 內部預設指向 undefined,所以在單獨呼叫的時候會報錯。
call/apply/bind 顯示繫結 this
JavaScript 中提供了一個方式可以讓我們手動指定函式內部的 this 指向,也就是前面提到的隱式傳遞物件,它們就是call/apply/bind。這三個方法是 Function 的原型方法,所有函式都可以呼叫這三個方法。看下面這個例子:
var a = 20;
var obj = {
a: 40
}
function fn() {
console.log(this.a);
}
fn(); // 20
fn.call(obj); // 40
fn.apply(obj); // 40
當函式呼叫 apply/bind 時,表示執行該函式,並且這個函式內部的 this 指向 apply/bind 的第一個引數。
二者的區別:
- call 的第一個引數是函式內部 this 的指向,後續的引數則是函式執行時所需要的引數,一個一個傳遞
- apply 的第一個引數與 call 相同,而執行函式的引數,則以陣列的形式傳入
bind 方法也能指定函式的 this ,但是它不同於call/apply。bind 方法會返回一個新函式,這個函式與原函式有相同的函式體,但是函式內部的 this 被繫結成 bind 方法的第一個引數,後續引數也是一個一個傳入,並且不會自動執行新函式。
建構函式中的 this
可以把建構函式看成是普通函式,其中的 this 指向是建立的物件例項,之所以稱之為建構函式,是因為我們會藉助 new 操作符來呼叫函式。在用 new 建立物件時,this 指向發生改變是在第二步:
- 建立一個物件例項
- 將建構函式中的 this 指向這個物件
- 執行建構函式中的程式碼
- 返回這個新建立的物件
function Foo() {
this.a = 20
}
var foo = new Foo()
console.log(foo.a) // 20
箭頭函式中的 this
箭頭函式內部是不會繫結 this 的,它會捕獲外層作用域中的 this,作為自己的 this 值。比如:
function Person() {
this.age = 20;
(() => {
this.age++
})()
}
var p = new Person()
console.log(p.age)
DOM事件中的 this
當函式被當做監聽事件處理函式時, 其 this 指向觸發該事件的元素 (針對於addEventListener事件)。比如:
<div class="box">
click
</div>
document.querySelector(`.box`).addEventListener(`click`, function(e) {
console.log(this) // 這個this指向div.box元素
}, false)
總結
this 的使用場景豐富多樣,可以用來實現繼承,實現函式柯里化等,作為開發者應該清楚各種使用方式以及其內部原理。
參考