jeremygo

jeremygo

我是把下一颗珍珠串在绳子上的人

從五個規則來介紹 this

在文章開始以前我們先做一道題:

window.name = "window";

function User(name) {
  this.name = name;
  this.greet1 = function() {
    console.log(this.name);
  };
  this.greet2 = function() {
    return function() {
      console.log(this.name);
    };
  };
  this.greet3 = function() {
    return () => console.log(this.name);
  };
}

const UserA = new User("UserA");
const UserB = new User("UserB");

UserA.greet1();
UserA.greet1.call(UserB);

UserA.greet2()();
UserA.greet2.call(UserB)();

UserA.greet3()();
UserA.greet3.call(UserB)();

你可以自己先試著寫出答案,如果你想檢驗自己的答案可以直接移到文末,我們也會在最後進行解析。

本文將會從以下五個規則來介紹 JavaScript 中的 this :

  • 隱式綁定
  • 顯式綁定
  • new 綁定
  • 詞法綁定
  • 默認綁定

隱式綁定#

首先讓我們看一段代碼:

const user = {
    name: 'Jeremy',
    greet () {
        console.log(`My name is ${this.name}`)
    }
}

讓我們調用 user 對象中的 greet 方法:

user.greet();  // My name is Jeremy

我們可以看到,當我們通過 user 對象來調用它的方法 greet 時,greet 中的 this 指向的就是 user 對象,這就是隱式綁定的關鍵: 當函數引用有上下文對象時,隱式綁定會把函數調用中的 this 綁定到這個上下文對象,因此這裡的 this.name 等同於 user.name

讓我們稍微擴展一下:

const user = {
    name: 'Jeremy',
    greet () {
        console.log(`My name is ${this.name}`)
    },
    son: {
        name: 'lap',
        greet () {
            console.log(`My name is ${this.name}`)
        }
    }
}

調用 user.son.greet() 的結果是否符合你的預期呢?

現在讓我們改寫一下代碼:

function greet () {
    console.log(`My name is ${this.name}`)
}

const user = {
    name: 'Jeremy'
}

我們將 greet 拆成了獨立的函數,現在我們該怎麼做讓 greet 中的 this 指向 user 對象呢?

顯式綁定#

在 JavaScript 中,每一個函數都有一個方法可以讓你實現這個功能 (即改變 this 的指向) ,這就是 call:

call() 方法調用一個具有給定 this 值的函數,以及分別提供的參數 (參數的列表)。

因此我們可以這樣調用:

greet.call(user)

這就是 顯式綁定 的含義,我們顯示地 (使用 .call ) 指定了 this 的指向。

如果我們想給 greet 傳入一些參數,這就需要用到 call 方法的其餘參數:

function greet (l1, l2, l3) {
    console.log(`My name is ${this.name} and I know ${l1}, ${l2} and ${3}`)
}

const user = {
    name: 'Jeremy'
}

const languages = ['JavaScript', 'Java', 'PHP']

greet.call(user, languages[0], languages[1], languages[2]) // My name is Jeremy and I know JavaScript, Java and PHP

當我們實際實踐這些代碼的時候就會發現把 languages 陣列一個一個傳進去是很煩人的,在這種情況下我們有一個更好的選擇 .apply:

apply() 方法調用一個具有給定 this 值的函數,以及作為一個陣列(或類似陣列對象)提供的參數。

.apply.call 唯一的區別就是傳入參數的方式,因此我們可以這樣調用:

greet.apply(user, languages) // My name is Jeremy and I know JavaScript, Java and PHP

最後介紹的方法是 .bind :

**bind() ** 方法創建一個新的函數,在調用時設置 this 關鍵字為提供的值。並在調用新函數時,將給定參數列表作為原函數的參數序列的前若干項。

.bind.call 類似,區別在於 .call 是立即調用,.bind 會返回一個新函數可以讓你之後再調用:

const newFn = greet.bind(user, languages[0], languages[1], languages[2])
newFn() // My name is Jeremy and I know JavaScript, Java and PHP

new 綁定#

讓我們看新的一段代碼:

function User (name) {
    this.name = name
}

const me = new User('Jeremy')
console.log(me.name) // Jeremy

使用 new 來調用 User 時,我們會構造一個新對象並把它綁定到 User 調用中的 this 上。

JavaScript 中的 new 使用起來跟傳統面向類的語言一樣,但內部機制是完全不一樣的,當我們使用 new 來調用函數,或者說發生構造函數調用時,會執行以下操作:

  • 創建一個新對象
  • 將這個對象鏈接到原型上
  • 將這個對象綁定到函數調用的 this
  • 如果該函數沒有返回其它對象,那麼就返回這個新對象

詞法綁定#

上述介紹的四種規則已經可以包含所有的正常函數,但是在 ES6 中介紹了一種特殊的函數:箭頭函數。

箭頭函數表達式的語法比函數表達式更短,並且沒有自己的 thisargumentssuper或 new.target 。這些函數表達式更適用於那些本來需要匿名函數的地方,並且它們不能用作構造函數。

箭頭函數沒有自己的 this ,根據外層 (函數或者全局) 作用域來決定 this

讓我們再來改寫一下代碼:

const user = {
    name: 'Jeremy',
    languages: ['JavaScript', 'Java', 'PHP'],
    greet () {
        return function () {
            console.log(this.name);
        }
    }
}

我們在 greet 方法中返回了一個函數,當我們試著調用 user.greet()() 時,返回的是 undefined

出現這個的原因是我們調用返回的函數時沒有綁定的上下文對象 (默認就變成了 window ),因此很直接的一種想法就是我們運用 顯式綁定 ,更改代碼如下:

const user = {
    name: 'Jeremy',
    languages: ['JavaScript', 'Java', 'PHP'],
    greet () {
        return function () {
            console.log(this.name);
        }.bind(this)
    }
}

user.greet()() // Jeremy

那我們如果用 箭頭函數 來改寫呢?

const user = {
    name: 'Jeremy',
    languages: ['JavaScript', 'Java', 'PHP'],
    greet () {
        return () => {
            console.log(this.name);
        }
    }
}

user.greet()() // Jeremy

箭頭函數 this 的查找規則其實與 變量查找 類似,在 ES6 以前我們就在使用一種幾乎等效的模式:

var user = {
    name: 'Jeremy',
    languages: ['JavaScript', 'Java', 'PHP'],
    greet () {
        var self = this;
        return function () {
            console.log(self.name);
        }
    }
}

user.greet()() // Jeremy

在同一個函數或者同一個程序中最好不要混用這兩種風格,否則代碼會更難編寫與維護。

默認綁定#

最後讓我們重新看這一段代碼:

function greet () {
    console.log(`My name is ${this.name}`)
}

const user = {
    name: 'Jeremy'
}

如果我們直接調用 greet 會發生什麼?

greet() // My name is undefined

這就引出了我們最後一個規則,如果我們沒有 隱式綁定 (對象調用),也沒有 顯式綁定 (.call.apply.bind) 或是 new 綁定,那麼 JavaScript 會默認將 this 指向 window 對象 (因此默認綁定也稱為 window 綁定):

window.name = 'window'

function greet () {
    console.log(`My name is ${this.name}`)
}

const user = {
    name: 'Jeremy'
}

greet() // window

在 ES5 中,如果你啟動了嚴格模式,那麼 JavaScript 會將 this 保持為 undefined

總結#

我們來總結一套判斷 this 指向的流程:

  • 首先看函數在哪裡被調用。
  • 函數是通過對象來調用 (. 左邊是一個對象) 嗎?如果是,this 指向這個對象,如果不是,繼續。
  • 函數是通過 .call.apply 或者.bind 來調用嗎?如果是,this 指向指定的上下文對象,如果不是,繼續。
  • 函數是通過 new 關鍵字來調用嗎?如果是,this 指向新創建的對象,如果不是,繼續。
  • 函數是一個箭頭函數嗎?如果是,this 指向箭頭函數向外第一個非箭頭函數的函數,如果不是,繼續。
  • 運行環境是嚴格模式嗎?如果是,thisundefined,如果不是,繼續。
  • this 指向 window 對象。

最後回到文章開始的題目,先給出運行的答案:

window.name = "window";

function User(name) {
  this.name = name;
  this.greet1 = function() {
    console.log(this.name);
  };
  this.greet2 = function() {
    return function() {
      console.log(this.name);
    };
  };
  this.greet3 = function() {
    return () => console.log(this.name);
  };
}

const UserA = new User("UserA");
const UserB = new User("UserB");

UserA.greet1();  // UserA
UserA.greet1.call(UserB); // UserB

UserA.greet2()();  // window
UserA.greet2.call(UserB)(); // window

UserA.greet3()();  // UserA
UserA.greet3.call(UserB)();  // UserB

UserAUserB 分別通過 new 構造出來,則對應的 name 分別為 UserAUserB

  • UserA.greet1() : 首先 greet1UserA 調用,則 greet1 內的 this 指向 UserA,所以輸出 UserA
  • UserA.greet1.call(UserB)greet1 通過 .call 調用,指定的對象是 UserB,所以輸出 UserB
  • UserA.greet2()():首先 greet2 通過 UserA 調用,返回了一個沒有綁定上下文對象的函數,所以此時輸出為 window
  • User.greet2.call(UserB)():這裡 gree2 通過 .call 指定 UserB 調用,但是同樣返回了一個沒有綁定上下文對象的函數,所以輸出依然為 window
  • UserA.greet3()():這裡返回的是詞法綁定的箭頭函數,綁定的上下文對象為 UserA,所以輸出 UserA
  • UserA.gree3.call(UserB)():這裡同樣返回了箭頭函數,綁定的上下文對象為通過 .call 指定的 UserB,所以輸出 UserB

參考鏈接:

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。