揭祕JavaScript中「神祕」的this關鍵字

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

您可能知道,執行任何JavaScript行的環境(或scope)稱爲「執行上下文」。Javascript運行時維護這些執行上下文的堆棧,而且當前正在執行存在於該堆棧頂部的執行上下文。this變量引用的對象每次更改執行上下文時都會更改。瀏覽器

默認狀況下,執行上下文是全局的,這意味着若是代碼做爲簡單函數調用的一部分執行,則該this變量將引用全局對象。在瀏覽器的狀況下,全局對象是window對象。例如,在Node.js環境中,this值是一個特殊對象globalbash

例如,嘗試如下簡單的函數調用:閉包

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

調用foo(),獲得輸出:app

「Simple function call」
true
複製代碼

證實這裏的this指向全局對象,此例中爲window函數

注意,若是實在嚴格模式下,this的值將是undefined,由於在嚴格模式下全局對象指向undefined而不是windowpost

試一下以下示例:學習

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

輸出:ui

「Simple function call」
false
複製代碼

咱們再來試下有構造函數的:this

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對象上。以下:

你會注意到在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關鍵字綁定主題時保持堅實的基礎。若是您有任何問題,建議或改進,我老是樂於學習更多知識並與全部知名開發人員交流知識。請隨時寫評論,或給我留言!

相關文章
相關標籤/搜索