當我開始學習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 Person
,JavaScript
會在Person
函式內建立一個新物件並把它儲存為this
。接著,first_name
, last_name
和 displayName
屬性會被新增到新建立的this
物件上。如下:
你會注意到在Person
的執行上下文中建立了this
物件,這個物件有first_name
, last_name
和 displayName
屬性。希望您能根據上圖理解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
複製程式碼
call
,apply
唯一的區別就是引數的傳遞形式,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
關鍵字能訪問到color
和position
屬性因為它指向box
物件。在clickMe
方法內部,this
關鍵字能訪問到color
和position
屬性因為它也指向box
物件。但是,clickMe
方法為querySelector
方法定義了一個回撥函式,然後這個回撥函式以普通函式的形式呼叫,所以this
指向全域性物件而非box
物件。當然,全域性物件沒有定義color
和position
屬性,所以這就是為什麼我們得到了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
物件,因此,color
和position
屬性將是有正確的green
和1
值。
再來一個:
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
關鍵字。在本例中它被包裹的箭頭函式clickMe
,clickMe
箭頭函式的this
關鍵字指向全域性物件,本例中是window
物件。所以this.color
和this.position
將會是undefined
因為window
物件沒有position
和color
屬性。
我想再給你看個在很多情況下都會有幫助的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.firstName
undefined。現在,我們試著修復這個情況。
- 在
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);
}
複製程式碼
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
指向這個物件。
- 建立
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
關鍵字繫結主題時保持堅實的基礎。如果您有任何問題,建議或改進,我總是樂於學習更多知識並與所有知名開發人員交流知識。請隨時寫評論,或給我留言!