歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
Linux教程網 >> Linux編程 >> Linux編程 >> 理解並掌握 JavaScript 中 this 的用法

理解並掌握 JavaScript 中 this 的用法

日期:2017/3/1 9:25:47   编辑:Linux編程

按:本文原文來自 Javascript.isSexy 這個網站。這篇文章和文中提到的另一篇文章解決了我一直以來對 thisapply, call, bind 這三個方法的困惑。我看過很多國內相關的技術文章,沒有一篇能讓我徹底理解這些概念的。因此我決定把它譯過來,不要讓更多的初學者像我一樣在這個問題上糾結太長時間。

(在學習 this 的同時也了解那些 this 被誤解和誤用的場景)

預備知識:JavaScript 基礎知識
閱讀時間:約 40 分鐘

  • JavaScript this 用法基礎
  • 理解 JavaScript this 的關鍵
  • 在全局作用域中使用 this
  • this 最容易被誤解和難以掌握的情景
    • 1. 解決當包含 this 的方法被當做回調函數時遇到的問題
    • 2. 解決當 this 出現在閉包內遇到的問題
    • 3. 解決把一個 this 方法 賦給一個變量時出現的問題
    • 4. 解決當借用方法的時候 this 的值不正確的問題
  • 結語

在 JavaScript 中,this 這個關鍵字常常困擾著初學者甚至一些進階的開發者。這篇文章旨在完完全全闡明 this。當你讀完本文之後,你就再也不會為 this 所困惑了。你將會理解 this 的各種使用場景,包括那些最難懂的情形。

我們使用 this 的方式和在英語或法語中使用代詞的方式十分類似。我們會這樣寫「李華正在飛快地跑著,因為正在趕火車。」注意這裡代詞「他」的用法。我們也可以這樣寫:「李華正在飛快地跑著,因為李華正在趕火車。」我們通常不會把「李華」這個名字像這樣重復使用,因為這樣顯得很神經。類似地,在 JavaScript 中,我們使用 this 作為一種指代。它指代一個對象(object),也就是那個上下文中的主語,或者說運行時的主體。考慮下面這個例子:

var person = {
    firstName: "Penelope",
    lastName: "Barrymore",
    fullName: function () {
        // 注意我們使用「this」關鍵字就像我們在上文中使用「他」一樣
        console.log(this.firstName + " " + this.lastName);
        // 我們也可以這樣寫
        console.log(person.firstName + " " + person.lastName);
    }
}

如果我們使用 person.firstNameperson.lastName 這種寫法的話,我們的代碼就會變得有歧義。假設有一個全局變量(我們或許有意為之,或許根本沒有意識到)的名字也叫 person ,那麼 person.firstName 將會嘗試讀取那個全局變量 person 中的 firstName 屬性,這將可能導致極難調試的錯誤。所以我們使用 this 關鍵字,不僅僅是因為這看起來十分優雅,還因為這樣使用更加准確。使用 this 消除了我們代碼中的歧義,就像在上文中使用「他」讓我們的話顯得更加清晰一樣。它讓我們明白我們想要指代的李華就是句子剛開頭提到的那個李華。

就像代詞「他」用來指代之前提到的人一樣,this 這個關鍵字也是用來指代那個被當前函數(就是使用了 this 的函數)綁定的對象。this 這個關鍵字不僅僅是指代那個對象,並且包含了那個對象的值。這很類似代詞,this 可以被視作是指代「上下文」中對象(也稱為「祖先對象」)的一種便捷的方式(同時也是一種沒有歧義的替換)。我們將在後面學習更多關於「上下文」 的概念。

JavaScript this 用法基礎

首先,我們已經知道在 JavaScript 中,函數和對象一樣都有屬性。而當一個函數執行的時候,它就獲得了 this 這個屬性。而 this 其實就是一個具有調用當前函數的對象的值的變量。

this 這個變量 永遠 指向 一個 對象,並且擁有這個對象的值。雖然 this 可以在全局作用域中出現,但它通常還是會在函數體內或對象的方法內。有一點要注意的是,當我們使用嚴格模式(strict mode)的時候,this 在全局函數中和匿名函數中的值是未定義的(undefined),不指向任何一個對象。

this 在一個函數體內出現的時候(設為函數 A ),它包含了調用函數 A 的那個對象的值。我們需要使用 this 來讀取調用函數 A 的那個對象的方法或是屬性。而這在我們不知道那個對象的名字,甚至有時候那個對象沒有名字的情況下就變得尤為重要。實際上,this 真的僅僅就是對「祖先對象」,或者說調用這個函數的那個對象,的一個便捷的指代而已。

我們用一個例子來展示 JavaScript 中 this 的一些基本用法,也來回顧一下上文的內容:

var person = {
    firstName: "Penelope",
    lastName: "Barrymore",
    // 因為 this 關鍵字在 showFullName 方法中被用到,而 showFullName 在 person 這個對象中被定義,
    // 所以 this 將會具有 person 這個對象的值,因為 person 對象將會調用 showFullName()
    showFullName: function() {
        console.log (this.firstName + " " + this.lastName);
    }
}
​
person.showFullName(); // Penelope Barrymore

再來看看 jQuery 中 this 用法的例子:

// 一段非常普遍的 jQuery 代碼
​
$ ("button").click (function (event) {
    // $(this) 將具有那個 ($("button")) 按鈕對象的值
    // 因為那個按鈕對象調用了 click() 方法
    console.log ($(this).prop("name"));
});

我來解釋一下上面的這個 jQuery 示例:$(this) 是 jQuery 中與 JavaScript 中 this 類同的語法,它被用在一個匿名函數中,而這個匿名函數在一個按鈕的 click() 方法中被執行。$(this) 之所以具有這個按鈕對象的值是因為 jQuery 庫把 $(this) 和那個調用了 click 方法的對象手動 綁定 (bind)在一起了。 因此,即使 $(this) 是在一個匿名函數中被定義,並且自身不能讀取外部函數中的 this 變量,它仍然能夠具有那個 jQuery 按鈕對象 ($("button")) 的值。

注意,按鈕(button)是一個 HTML 頁面上的 DOM 元素,同時也是一個對象;在上面這個例子中的按鈕是一個 jQuery 對象,因為我們把它包裝在 jQuery 的 $() 函數中了。

理解 JavaScript this 的關鍵

如果你理解了 JavaScript this 的以下這個原則的話,那你對 this 這個關鍵字就會有一個清晰的認識了:只有一個對象調用了包含 this 的函數的時候,this 才會被賦值。我們不妨把包含 this 的函數稱作 this 函數

在一個對象方法中定義的 this 看起來好像指向了這個對象本身,但仍然只有在某個對象調用了這個 this 函數 的時候它才被賦值。並且被賦的那個值 只依賴於 調用了 this 函數 的那個對象。雖然在大多數情況下, this 都是那個調用了 this 函數 的那個對象,但也有一些情況不是這樣的。我將會在後文中講到這一點。

在全局作用域中使用 this

在全局作用域中,當代碼在浏覽器中執行的時候,所有的全局變量和函數都被定義在 window 對象上。因此,當我們在全局函數中使用 this 的時候,它會指向全局 window 對象並且擁有它的值(除非在嚴格模式下),此時的 this 就成了整個 JavaScript 應用程序或者說整個網頁的主容器。

所以:

var firstName = "Peter",
    lastName = "Ally";
​
function showFullName () {
    // 在這個函數中,this 將會擁有 window 對象的值
    // 因為 showFullName() 函數,和 firstName, lastName 一樣是定義在全局作用域的
    console.log (this.firstName + " " + this.lastName);
}
​
var person = {
    firstName: "Penelope",
    lastName: "Barrymore",
    showFullName:function () {
        // 下面這行中的 this 指代 person 對象,因為 showFullName 這個函數將會被 person 對象調用
        console.log (this.firstName + " " + this.lastName);
    }
}
​
showFullName (); // Peter Ally​
​
// 所有的全局變量和函數都定義在 window 對象上面,所以:
window.showFullName (); // Peter Ally​
​
// 在 person 對象中定義的 showFullName() 函數中的 this 仍然指向 person 對象,所以:
person.showFullName (); // Penelope Barrymore

this 最容易被誤解和難以掌握的情景

this 關鍵字在以下場景中常常被誤解:當我們借用一個使用了 this 的方法的時候;當我們把一個只用了 this 的方法賦給一個變量的時候;當一個使用了 this 的方法被當作回調函數傳入的時候;當 this 在閉包中使用的時候。我們能過舉例來詳細地解釋在上面的每一種情形中如何使 this 擁有合適的值。

一點重要的提示

在接下去講之前,我們先來談談「上下文」(Context)這個概念

在 JavaScript 中,上下文的概念和一個英文句子中主語的概念相類似:「John is the winner who returned the money.」這句話中的主語是 John ,我們可以說這句話的語境(上下文)是 John ,因為這句話此時的關注點在 John 身上。代詞「who」也是指代先行詞 John。正如我們可以使用分號來切換句子的主語一樣,我們可以通過讓另一個對象去調用本對象的方法的方式來切換上下文。

用代碼可以這樣描述

var person = {
    firstName: "Penelope",
    lastName: "Barrymore",
    showFullName: function() {
        // 「上下文」
        console.log(this.firstName + " " + this.lastName);
    }
}

// 當我們在 person 對象上調用 showFullName() 方法的時候,「上下文」是 persion 對象。
// 這時在 showFullName() 方法裡面使用的 this 就擁有了 person 對象的值
person.showFullName(); // Penelope Barrymore

// 當我們使用另一個對象來調用 showFullName 的時候
var anotherPerson = {
    firstName: "Rohit", 
    lastName: "Khan"
};

// 我們可以使用 apply 方法來顯式地設置 this 的值。關於 apply() 方法,我們將在後文中詳細解釋
// this 得到的永遠是調用它的那個對象的值,因此:
person.showFullName.apply(anotherPerson); // Rohit Khan

// 所以現在上下文就變成了 anotherPerson ,因為是 anotherPerson 使用 apply() 方法調用了 person.showFullName() 方法

在下面這些情景中,this 關鍵字可能會變得十分難以理解。我們在示例中同時給出了解決有關 this 使用錯誤的方案。

1. 解決當包含 this 的方法被當做回調函數時遇到的問題

當我們把含有 this 的方法當做回調函數的時候代碼往往變得十分難以理解。比如:

// 我們有一個簡單的對象,它有一個 clickHandler 方法,我們想要使當頁面上的一個按鈕被點擊時它被調用
var user = {
    data: [
        {name: "T. Woods", age: 37},
        {name: "P. Mickelson", age: 43}
    ],
    clickHandler: function(event) {
        var randomNum = ((Math.random() * 2 | 0) + 1) - 1; // 產生 0 到 1 之間的隨機數

        // 下面這行會隨機打印出一個 data 數組中的人的姓名和年齡 
        console.log(this.data[randomNum].name + " " + this.data[randomNum].age);
    }
}

// 這個 button 被 jQuery 的 $ 包裝起來了,所以它變成了一個 jQuery 對象
// 下面這行會輸出 undefined 因為 button 對象沒有 data 屬性
$("button").click(user.clickHandler);

在上面的代碼中,按鈕 ($("button")) 是一個對象,我們把 user.clickHandler 傳入它的 click() 方法作為一個回調函數,這時候我們就明白 user.clickHandler 方法裡面的 this 已經不再指向 user 這個對象了。因為 this 是定義在 user.clickHandler 方法裡的,所以它現在指向那個調用了 user.clickHandler 的對象。而那個對象就是 button 對象。也就是說,user.clickHandler 將會在 button 對象的 click 方法中被執行。

注意在調用 clickHandler() 時,我們雖然寫成了 user.clickHander 的形式(事實上我們必須這麼寫,因為 clickHandler 是在 user 對象中被定義的),但 clickHandler 還會在 button 對象的上下文中被執行,this 也因而指向了 button 對象。

講到這裡,我們應該發現當上下文發生變化的時候,換句話說就是當我們在別的對象中調用了本對象內定義的方法的時候,this 關鍵字就不再指向定義 this 時的那個對象了,而是指向了調用了那個 this 所在方法的對象。

解決 this 方法被當作回調函數傳遞時指向錯誤的方法:

因為我們確實想要讓 this.data 指向 user 對象的 data 屬性,我們可以使用 bind(), apply(), call() 這三個方法來顯式地設置 this 的值。

我還寫了另一篇文章,Javascript 進階:Apply, Call 和 Bind 方法詳解 來詳細解釋這三種方法的用法,包括如何使用它們在各種容易出錯情景下正確地設置 this 的值。我就不在這裡貼出整篇文章了,推薦讀者詳細地閱讀整篇文章,因為我認為要想成為 JavaScript 的高級開發者,和這三種方法打交道是不可避免的。

為了解決上面例子提到的那種問題,我們可以使用 bind 方法:

我們把下面這行:

$("button").click(user.clickHandler);

改正為下面這樣,把 clickHandleruser 綁定起來:

$("button").click(user.clickHandler.bind(user));

查看 JSBin 上的在線示例

2. 解決當 this 出現在閉包內遇到的問題

另一個 this 常常被誤解的情景是當我們使用閉包的時候。一個非常值得注意的地方是,閉包不能直接通過使用 this 來訪問外層函數的 this 變量,因為 this 變量只有當前函數本身可以訪問,而其內層函數是訪問不到的。舉個例子:

var user = {
    tournament: "The Masters",
    data: [
        {name: "T. Woods", age: 37},
        {name: "P. Mickelson", age: 43}
    ],

    clickHandler: function() {
        // 在這裡使用 this.data 是可以的,因為 this 指向 user 對象,而 data 是 user 對象的一個屬性
        this.data.forEach(function(person)) {
            // 但是在內層匿名函數中(就是我們傳給 forEach 方法的函數),this 不再指向 user 對象了
            // 這個內層函數不能訪問外層函數的 this 變量了
            console.log("What is This referring to? " + this); //[Object Window]
            console.log(person.name + " is playing at " + this.tournament);
            // T. Woods is playing at undefined
            // P. Mickelson is playing at undefined
        });
    }
}

user.clickHandler(); // 現在 this 指向什麼?[object Window]

在匿名函數內部的 this 不能獲得外層函數 this 的值,所以當沒有使用嚴格模式的時候,它就被綁定在了全局 window 對象上了。

在內層函數中維持 this 的值的方法:

為了解決傳入 forEach 的匿名函數中 this 值不正確的問題,我們使用一個常用的解決辦法,即當我們進入 forEach 的時候,提前把 this 的值存到另一個變量中去。

var user = {
    tournament: "The Masters",
    data: [
        {name: "T. Woods", age: 37},
        {name: "P. Mickelson", age: 43}
    ],

    clickHandler: function(event) {
        // 為了當 this 還指向 user 對象的時候把它的值保存下來,我們把它存到另一個變量中
        // 我們把 this 保存到 theUserObj 變量中去,這樣我們就可以在之後使用了
        var theUserObj = this;
        this.data.forEach(function(person) {
            // 我們將 this.tournament 替換成 theUserObj.tournament
            console.log(person.name + " is playing at " + theUserObj.tournament);
        });
    }
}

user.clickHandler();
// T. Woods is playing at The Masters
// P. Mickelson is playing at The Masters

值得注意的是,許多 JavaScript 開發者喜歡把 this 存在一個叫做 that 的變量中(就像下面的代碼那樣)。我覺得用 that 來命名使用的時候十分不方便,所以盡量使用一個合適的名詞來描述 this 所指向的對象,所以我在上述代碼中使用了 var theUserObj = this

// 一種十分常見的寫法
var that = this;

查看 JSBin 上的在線示例

3. 解決把一個 this 方法 賦給一個變量時出現的問題

當我們把一個使用了 this 的方法賦給一個變量的時候,this 的值很可能出乎我們的意料,指向了其他的對象。我們來看一個例子:

// 這個 data 變量是一個全局變量
var data = [
    {name: "Samantha", age: 12},
    {name: "Alexis", age: 14}
];

var user = {
    // 這個 data 變量是 user 對象的一個屬性
    data: [
        {name: "T. Woods", age: 37},
        {name: "P. Mickelson", age: 43}
    ],
    showData: function(event) {
        var randomNum = ((Math.random() * 2 | 0) + 1) - 1; // 0 和 1 之間的隨機數

        // 下面這行隨機打印一個 data 數組中的人的信息
        console.log(this.data[randomNum].name + " " + this.data[randomNum].age);
    }
}

// 把 user.showData 賦值給一個變量
var showUserData = user.showData;

// 當我們執行 showUserData 函數的時候,打印在 console 中的值來自於全局的 data 數組,而不是 user 對象的 data 屬性
showUserData(); // Samantha 12 (來自全局 data 數組)

當把含有 this 的方法賦值給一個變量時維持 this 的值的方法

我們可以使用 bind 方法來顯式地設置 this 的值來解決這個問題:

// 把 showData 方法和 user 對象綁定起來
var showUserData = user.showData.bind(user);

// 現在我們可以從 user 對象中獲取值了,因為 this 關鍵字和 user 對象綁定在一起了
showUserData(); // P. Mickelson 43

4. 解決當借用方法的時候 this 的值不正確的問題

在 JavaScript 開發中,借用方法(borrow methods)是一個很常見的用法,作為一個 JavaScript 開發者,我們肯定會在實踐中不斷地遇到這個問題。而且每次我們也樂於使用這種節約時間的方法。如果你想了解更多關於方法借用的問題,請閱讀我的這篇詳細解析的文章,Javascript 進階:Apply, Call 和 Bind 方法詳解。

讓我們來看看當處於借用方法這樣的上下文的時候,this 的相關表現:

// 我們有兩個對象。其中一個有一個叫做 avg() 的方法,而另一個沒有
// 所以我們想借用一下 (avg()) 這個方法
var gameController = {
    scores: [20, 34, 55, 46, 77],
    avgScore: null,
    players: [
        {name: "Tommy", playerID: 987, age: 23},
        {name: "Pau", playerID: 87, age: 33}
    ]
}

var appController = {
    scores: [900, 845, 809, 950],
    avgScore: null,
    avg: function() {
        var sumOfScores = this.scores.reduce(function(prev, cur, index, array) {
            return prev + cur;
        });

        this.avgScore = sumOfScores / this.scores.length;
    }
}

// 如果我們執行下面的代碼,
// gameController.avgScore 屬性將會被設置為 appController 對象的 scores 數組的平均數

// 不要執行下面這行代碼,這只是用來說明的,而我們現在想讓 appController.avgScore 保持 null 值
gameController.avgScore = appController.avg();

avg 方法中的 this 不會指向 gameController 對象,而會指向 appController 對象,因為它是被 appController 對象所調用的。

解決當借用方法時 this 指向出錯的問題

要解決這個問題,我們只要確保在 appController.avg() 中的 this 指向 gameController 就可以了。我們可以使用 apply() 方法來實現:

// 注意我們使用的是 apply() 方法,所以第二個參數必須是一個數組,這個數組中包含了要傳入 appController.avg() 的參數
appController.avg.apply(gameController, gameController.scores);

// 即使我們從 appController 對象中借用了 avg() 方法,gameController 的 avgScore 屬性仍被成功地設置了
console.log(gameController.avgScore); // 46.4

// appController.avgScore 的值仍然是 null。它沒有被更新,只有 gameController.avgScore 被更新了
console.log(appController.avgScore); // null

gameController 對象借用了 appControlleravg() 方法。在 appController.avg() 中的 this 的值會被設置成 gameController 對象,因為我們把 gameController 作為第一個參數傳入了 apply() 方法中。傳入 apply() 方法的第一個參數會被顯式地設置為 this 的值。

查看 JSBin 上的在線示例

結語

我希望你對 JavaScript 中的 this 關鍵字已經理解了。現在你有了必需的工具(bind, apply, call 方法,和把 this 賦給一個變量)來幫你解決在各種情形下關於 this 的問題了。

正如我們在上文中看到的,this 在有些情況下可能會變得很難以處理,比如原始的上下文(就是 this 定義的地方)發生改變的時候,尤其是在回調函數中,或者被另一個對象調用的時候,再或者是當方法借用的時候。但是只要記住 this 永遠具有那個調用 this 函數 的對象的值,就不會出錯。

JavaScript高級程序設計(第3版)高清完整PDF中文+英文+源碼 http://www.linuxidc.com/Linux/2014-09/107426.htm

享受生活,享受代碼。

原文鏈接

Copyright © Linux教程網 All Rights Reserved