揭祕JavaScript中“神祕”的this關鍵字

Ioodu發表於2019-03-05

揭祕JavaScript中“神祕”的this關鍵字

當我開始學習JavaScript時,花了一些時間來理解JavaScript中的this關鍵字並且能夠快速識別this關鍵字所指向的物件。我發現理解this關鍵字最困難的事情是,您通常會忘記在您已閱讀或觀看過一些JavaScript課程或資源中解釋的不同案例情況。在ES6中引入箭頭函式後,事情變得更加混亂,因為箭頭函式this以不同的方式處理關鍵字。我想寫這篇文章來陳述我學到的東西,並嘗試以一種可以幫助任何正在學習JavaScript並且難以理解this關鍵字的人的方式來解釋它。

您可能知道,執行任何JavaScript行的環境(或scope)稱為“執行上下文”。Javascript執行時維護這些執行上下文的堆疊,並且當前正在執行存在於該堆疊頂部的執行上下文。this變數引用的物件每次更改執行上下文時都會更改。

預設情況下,執行上下文是全域性的,這意味著如果程式碼作為簡單函式呼叫的一部分執行,則該this變數將引用全域性物件。在瀏覽器的情況下,全域性物件是window物件。例如,在Node.js環境中,this值是一個特殊物件global

例如,嘗試以下簡單的函式呼叫:

function foo () {
  console.log("Simple function call");
  console.log(this === window);
}
foo();
複製程式碼

呼叫foo(),得到輸出:

“Simple function call”
true
複製程式碼

證明這裡的this指向全域性物件,此例中為window

注意,如果實在嚴格模式下,this的值將是undefined,因為在嚴格模式下全域性物件指向undefined而不是window

試一下如下示例:

function foo () {
  'use strict';
  console.log("Simple function call");
  console.log(this === window);
}
foo();
複製程式碼

輸出:

“Simple function call”
false
複製程式碼

我們再來試下有建構函式的:

function Person(first_name, last_name) {
    this.first_name = first_name;
    this.last_name = last_name;
  
    this.displayName = function() {
        console.log(`Name: ${this.first_name} ${this.last_name}`);
    };
}
複製程式碼

建立Person例項:

let john = new Person('John', 'Reid');
john.displayName();
複製程式碼

得到結果:

"Name: John Reid"
複製程式碼

這裡發生了什麼?當我們呼叫 new PersonJavaScript會在Person函式內建立一個新物件並把它儲存為this。接著,first_name, last_namedisplayName 屬性會被新增到新建立的this物件上。如下:

揭祕JavaScript中“神祕”的this關鍵字

你會注意到在Person執行上下文中建立了this物件,這個物件有first_name, last_namedisplayName 屬性。希望您能根據上圖理解this物件是如何建立並新增屬性的。

我們已經探討了兩種相關this繫結的普通案例我不得不提出下面這個更加困惑的例子,如下函式:

function simpleFunction () {
    console.log("Simple function call")
    console.log(this === window); 
}
複製程式碼

我們已經知道如果像下面這樣作為簡單函式呼叫,this關鍵字將指向全域性物件,此例中為window物件。

simpleFunction()
複製程式碼

因此,得到輸出:

“Simple function call”
true
複製程式碼

建立一個簡單的user物件:

let user = {
    count: 10,
    simpleFunction: simpleFunction,
    anotherFunction: function() {
        console.log(this === window);
    }
}
複製程式碼

現在,我們有一個simpleFunction屬性指向simpleFunction函式,同樣新增另一個屬性呼叫anotherFunction函式方法。

如果呼叫user.simpleFunction(),得到輸出:

“Simple function call”
false
複製程式碼

為什麼會這樣呢?因為simpleFunction()現在是user物件的一個屬性,所以this指向這個user物件而不是全域性物件。

當我們呼叫user.anotherFunction,也是一樣的結果。this關鍵字指向user物件。所以,console.log(this === window);應該返回false:

false
複製程式碼

再來,以下操作會返回什麼呢?

let myFunction = user.anotherFunction;
myFunction();
複製程式碼

現在,得到結果:

true
複製程式碼

所以這又發生了什麼?在這個例子中,我們發起普通函式呼叫。正如之前所知,如果一個方法以普通函式方式執行,那麼this關鍵字將指向全域性物件(在這個例子中是window物件)。所以console.log(this === window);輸出true

再看一個例子:

var john = {
    name: 'john',
    yearOfBirth: 1990,
    calculateAge: function() {
        console.log(this);
        console.log(2016 - this.yearOfBirth);
        function innerFunction() {
            console.log(this);
        }
        innerFunction();
    }
}
複製程式碼

呼叫john.calculateAge()會發生什麼呢?

{name: "john", yearOfBirth: 1990, calculateAge: ƒ}
26
Window {postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, parent: Window, …}
複製程式碼

calculateAge函式內部, this 指向 john物件,但是,在innerFunction函式內部,this指向全域性物件(本例中為window),有些人認為這是JS的bug,但是規則告訴我們無論何時一個普通函式被呼叫時,那麼this將指向全域性物件。

...

我所學的JavaScript函式也是一種特殊的物件,每個函式都有call, apply, bind方法。這些方法被用來設定函式的執行上下文的this值。

function Person(firstName, lastName) {
    this.firstName = firstName;
    this.lastName = lastName;
    this.displayName = function() {
        console.log(`Name: ${this.firstName} ${this.lastName}`);
    }
}
複製程式碼

建立兩個例項:

let person = new Person("John", "Reed");
let person2 = new Person("Paul", "Adams");
複製程式碼

呼叫:

person.displayName();
person2.displayName();
複製程式碼

結果:

Name: John Reed
Name: Paul Adams
複製程式碼

call:

person.displayName.call(person2);
複製程式碼

上面所做的事情就是設定this的值為person2物件。因此,

Name: Paul Adams
複製程式碼

apply:

person.displayName.apply([person2]);
複製程式碼

得到:

Name: Paul Adams
複製程式碼

callapply唯一的區別就是引數的傳遞形式,apply應該傳遞一個陣列,call則應該單獨傳遞引數。

我們用bind來做同樣的事情,bind返回一個新的方法,這個方法中的this指向傳遞的第一個引數。

let person2Display = person.displayName.bind(person2);
複製程式碼

呼叫person2Display,得到Name: Paul Adams結果。

...

箭頭函式

ES6中,有一個新方法定義函式。如下:

let displayName = (firstName, lastName) => {
    console.log(Name: ${firstName} ${lastName});
};
複製程式碼

不像通常的函式,箭頭函式沒有他們自身的this關鍵字。他們只是簡單的使用寫在函式裡的this關鍵字。他們有一個this詞法變數。

ES5:

var box = {
    color: 'green', // 1
    position: 1, // 2
    clickMe: function() { // 3
        document.querySelector('body').addEventListener('click', function() {
            var str = 'This is box number ' + this.position + ' and it is ' + this.color; // 4
            alert(str);
        });
    }
}
複製程式碼

如果呼叫:

box.clickMe()
複製程式碼

彈出框內容將是This is box number undefined and it is undefined'.

我們一步一步來分析是怎麼回事。在//1//2行,this關鍵字能訪問到colorposition屬性因為它指向box物件。在clickMe方法內部,this關鍵字能訪問到colorposition屬性因為它也指向box物件。但是,clickMe方法為querySelector方法定義了一個回撥函式,然後這個回撥函式以普通函式的形式呼叫,所以this指向全域性物件而非box物件。當然,全域性物件沒有定義colorposition屬性,所以這就是為什麼我們得到了undefined值。

我們可以用ES5的方法來修復這個問題:

var box = {
    color: 'green',
    position: 1,
    clickMe: function() {
        var self = this;
        document.querySelector('body').addEventListener('click', function() {
            var str = 'This is box number ' + self.position + ' and it is ' + self.color;
            alert(str);
        });
    }
}
複製程式碼

新增 var self = this,建立了一個可以使用指向box物件的this關鍵字的閉包函式的工作區。我們僅僅只需要在回撥函式內使用self變數。

呼叫:

box.clickMe();
複製程式碼

彈出框內容This is box number 1 and it is green

怎麼使用箭頭函式能夠達到上述效果呢?我們將用箭頭函式替換點選函式的回撥函式。

var box = {
    color: 'green',
    position: 1,
    clickMe: function() {
        document.querySelector('body').addEventListener('click', () => {
            var str = 'This is box number ' + this.position + ' and it is ' + this.color;
            alert(str);
        });
    }
}
複製程式碼

箭頭函式的神奇之處就是共享包裹它的this詞法關鍵字。所以,本例中外層函式的this共享給箭頭函式,這個外層函式的this關鍵字指向box物件,因此,colorposition屬性將是有正確的green1值。

再來一個:

var box = {
    color: 'green',
    position: 1,
    clickMe: () => {
        document.querySelector('body').addEventListener('click', () => {
            var str = 'This is box number ' + this.position + ' and it is ' + this.color;
            alert(str);
        });
    }
}
複製程式碼

oh!現在又彈出了‘This is box number undefined and it is undefined’.。為什麼?

click事件監聽函式閉包的this關鍵字共享了包裹它的this關鍵字。在本例中它被包裹的箭頭函式clickMeclickMe箭頭函式的this關鍵字指向全域性物件,本例中是window物件。所以this.colorthis.position將會是undefined因為window物件沒有positioncolor屬性。

我想再給你看個在很多情況下都會有幫助的map函式,我們定義一個Person建構函式方法如下:

function Person(firstName, lastName) {
    this.firstName = firstName;
    this.lastName = lastName;
    this.displayName = function() {
        console.log(`Name: ${this.firstName} ${this.lastName}`);
    }
}
複製程式碼

Person的原型上新增myFriends方法:

Person.prototype.myFriends = function(friends) {
    var arr = friends.map(function(friend) {
        return this.firstName + ' is friends with ' + friend;
    });
    console.log(arr);
}
複製程式碼

建立一個例項:

let john = new Person("John", "Watson");
複製程式碼

呼叫john.myFriends(["Emma", "Tom"]),結果:

["undefined is friends with Emma", "undefined is friends with Tom"]
複製程式碼

本例與之前的例子非常相似。myFriends函式體內有this關鍵字指向回撥物件。但是,map閉包函式內是一個普通函式呼叫。所以map閉包函式內this指向全域性物件,本例中為window物件,因此this.firstNameundefined。現在,我們試著修復這個情況。

  1. myFriends函式體內指定this為其它變數如self,以便map函式內閉包使用它。
Person.prototype.myFriends = function(friends) {
    // 'this' keyword maps to the calling object
    var self = this;
    var arr = friends.map(function(friend) {
        // 'this' keyword maps to the global object
        // here, 'this.firstName' is undefined.
        return self.firstName + ' is friends with ' + friend;
    });
    console.log(arr);
}
複製程式碼
  1. map閉包函式使用bind
Person.prototype.myFriends = function(friends) {
    // 'this' keyword maps to the calling object
    var arr = friends.map(function(friend) {
        // 'this' keyword maps to the global object
        // here, 'this.firstName' is undefined.
        return this.firstName + ' is friends with ' + friend;
    }.bind(this));
    console.log(arr);
}
複製程式碼

呼叫bind會返回一個map回撥函式的副本,this關鍵字對映到外層的this關鍵字,也就是是呼叫myFriends方法,this指向這個物件。

  1. 建立map回撥函式為箭頭函式。
Person.prototype.myFriends = function(friends) {
    var arr = friends.map(friend => `${this.firstName} is friends with ${friend}`);
    console.log(arr);
}
複製程式碼

現在,箭頭函式內的this關鍵字將共享未曾包裹它的詞法作用域,也就是說例項myFriends

所有以上解決方案都將輸出結果:

["John is friends with Emma", "John is friends with Tom"]
複製程式碼

...

在這一點上,我希望我已經設法使this關鍵字概念對您來說有點平易近人。在本文中,我分享了我遇到的一些常見情況以及如何處理它們,但當然,在構建更多專案時,您將面臨更多情況。我希望我的解釋可以幫助您在接近this關鍵字繫結主題時保持堅實的基礎。如果您有任何問題,建議或改進,我總是樂於學習更多知識並與所有知名開發人員交流知識。請隨時寫評論,或給我留言!

相關文章