你應該要知道的JS中的this

wz102824678發表於2019-02-16

前言

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 指向發生改變是在第二步:

  1. 建立一個物件例項
  2. 將建構函式中的 this 指向這個物件
  3. 執行建構函式中的程式碼
  4. 返回這個新建立的物件
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 的使用場景豐富多樣,可以用來實現繼承,實現函式柯里化等,作為開發者應該清楚各種使用方式以及其內部原理。

參考

你不知道的JavaScript(上卷)

相關文章