者|Austin Tackaberry
譯者|無明
出處丨前端之巔
這篇文章通過簡單的術語和一個真實的例子解釋了 this 是什么以及為什么說它很有用。
我發現,很多教程在解釋 JavaScript 的 this 時,通常會假設你擁有 Java、C++ 或 Python 等面向對象編程語言的背景。這篇文章主要面向那些對 this 沒有先入之見的人。我將嘗試解釋什么是 this 以及為什么它很有用。
或許你遲遲不肯深入探究 this,因為它看起來很奇怪,讓你心生畏懼。你之所以使用它,有可能僅僅是因為 StackOverflow 說你需要在 React 用它來完成一些事情。
在我們深入了解它的真正含義以及為什么要使用它之前,我們首先需要了解函數式編程和面向對象編程之間的區別。
你可能知道也可能不知道,JavaScript 具有函數和面向對象的構造,你可以選擇關注其中一個或兩者兼而有之。
在我的 JavaScript 之旅的早期,我一方面擁抱函數式編程,一方面像避免瘟疫一樣排斥面向對象編程。我對面向對象關鍵字 this 不甚了解。其中的一個原因是我不明白它存在的必要性。在我看來,完全可以不依賴 this 就可以完成所有的事情。
在某種程度上,我的看法是對的。
你可能只關注其中一種范式而從來不去了解另外一種,作為一名 JavaScript 開發者,你的局限性就體現在這里。為了說明函數式編程和面向對象編程之間的差別,我將使用一組 Facebook 好友數據作為示例。
假設你正在構建一個用戶登錄 Facebook 的 Web 應用,在登錄后顯示一些 Facebook 好友的數據。你需要訪問 Facebook 端點來獲取好友的數據,可能包含一些信息,例如 firstName、lastName、username、numFriends、friendData、birthday 和 lastTenPosts。
const data = [ { firstName: 'Bob', lastName: 'Ross', username: 'bob.ross', numFriends: 125, birthday: '2/23/1985', lastTenPosts: ['What a nice day', 'I love Kanye West', ...], }, ... ]
你從(臆造的)Facebook API 獲得上面的數據。現在,你需要轉換它們,讓它們符合項目需要的格式。假設你要為每個用戶的朋友顯示以下內容:
如果使用函數式方法,就是將整個數組或數組的每個元素傳給一個返回所需操作數據的函數:
const fullNames = getFullNames(data) // ['Ross, Bob', 'Smith, Joanna', ...]
你從原始數據開始(來自 Facebook API),為了將它們轉換為對你有用的數據,你將數據傳給一個函數,這個函數將輸出你可以在應用程序中顯示給用戶的數據。
你也可以通過類似的方式獲取三個隨機帖子并計算朋友生日至今的天數。
函數式方法就是指接受原始數據,將數據傳給一個或多個函數,并輸出對你有用的數據。
對于那些剛接觸編程和學習 JavaScript 的人來說,面向對象方法可能會更難掌握。面向對象是指你將每個朋友轉換為對象,對象包含了用于生成你所需內容的一切。
你可以創建包含 fullName 屬性的對象,以及 getThreeRandomPosts 和 getDaysUntilBirthday 函數。
function initializeFriend(data) { return { fullName: `${data.firstName} ${data.lastName}`, getThreeRandomPosts: function() { // get three random posts from data.lastTenPosts }, getDaysUntilBirthday: function() { // use data.birthday to get the num days until birthday } }; } const objectFriends = data.map(initializeFriend) objectFriends[0].getThreeRandomPosts() // Gets three of Bob Ross's posts
面向對象方法是為你的數據創建對象,這些對象包含了狀態和用于生成對你和你的項目有用的數據的信息。
你可能沒有想過會寫出類似 initializeFriend 這樣的東西,你可能會認為它很有用。你可能還會注意到,它其實并非真正的面向對象。
getThreeRandomPosts 或 getDaysUntilBirthday 方法之所以有用,主要是因為閉包。因為使用了閉包,所以在 initializeFriend 返回之后,它們仍然可以訪問 data。
假設你寫了另一個方法,叫 greeting。請注意,在 JavaScript 中,方法只是對象的一個屬性,這個屬性的值是一個函數。我們希望 greeting 可以做這些事情:
function initializeFriend(data) { return { fullName: `${data.firstName} ${data.lastName}`, getThreeRandomPosts: function() { // get three random posts from data.lastTenPosts }, getDaysUntilBirthday: function() { // use data.birthday to get the num days until birthday }, greeting: function() { return `Hello, this is ${fullName}'s data!` } }; }
這樣可以嗎?
不行!
新創建對象的所有東西都可以訪問 initializeFriend 的變量,但對象本身的屬性或方法不行。當然,你可能會問:
難道你不能用 data.firstName 和 data.lastName 來返回 greeting 嗎?
當然可以。但如果我們還想在 greeting 中包含朋友生日至今的天數,該怎么辦?我們必須以某種方式從 greeting 中調用 getDaysUntilBirthday 方法。
是時候讓 this 上場了!
在不同的情況下,this 代表的東西也不一樣。默認情況下,this 指向全局對象(在瀏覽器中,就是 window 對象)。但光知道這點對我們并沒有太大幫助,對我來說有用的是 this 的這條規則:
如果 this 被用在一個對象的方法中,并且這個方法在對象的上下文中調用,那么 this 就指向這個對象本身。
你會問:“在對象的上下文中調用……這又是什么意思”?
別擔心,稍后我們會解釋這個。
因此,如果我們想在 greeting 中調用 getDaysUntilBirthday,可以直接調用 this.getDaysUntilBirthday,因為在這種情況下,this 指向對象本身。
注意:不要在全局作用域或在另一個函數作用域內的常規 ole 函數中使用 this!this 是一個面向對象的構造。因此,它只在對象(或類)的上下文中有意義!
讓我們重構 initializeFriend,讓它使用 this:
function initializeFriend(data) { return { lastTenPosts: data.lastTenPosts, birthday: data.birthday, fullName: `${data.firstName} ${data.lastName}`, getThreeRandomPosts: function() { // get three random posts from this.lastTenPosts }, getDaysUntilBirthday: function() { // use this.birthday to get the num days until birthday }, greeting: function() { const numDays = this.getDaysUntilBirthday() return `Hello, this is ${this.fullName}'s data! It is ${numDays} until ${this.fullName}'s birthday!` } }; }
現在,在執行完 intializeFriend 后,這個對象的所有東西都限定在對象本身。我們的方法不再依賴于閉包,它們將使用對象本身包含的信息。
這是 this 的一種使用方式,現在回到之前的問題:為什么說 this 因上下文不同而不同?
有時候,你希望 this 可以指向不一樣的東西,比如事件處理程序就是一個很好的例子。假設我們想在用戶點擊鏈接時打開朋友的 Facebook 頁面。我們可能會在對象中添加一個 onClick 方法:
function initializeFriend(data) { return { lastTenPosts: data.lastTenPosts, birthday: data.birthday, username: data.username, fullName: `${data.firstName} ${data.lastName}`, getThreeRandomPosts: function() { // get three random posts from this.lastTenPosts }, getDaysUntilBirthday: function() { // use this.birthday to get the num days until birthday }, greeting: function() { const numDays = this.getDaysUntilBirthday() return `Hello, this is ${this.fullName}'s data! It is ${numDays} until ${this.fullName}'s birthday!` }, onFriendClick: function() { window.open(`https://facebook.com/${this.username}`) } }; }
請注意,我們向對象添加了 username,讓 onFriendClick 可以訪問它,這樣我們就可以在新窗口中打開朋友的 Facebook 頁面。現在編寫 HTML:
<button id="Bob_Ross"> <!-- A bunch of info associated with Bob Ross --> </button>
然后是 JavaScript:
const bobRossObj = initializeFriend(data[0]) const bobRossDOMEl = document.getElementById('Bob_Ross') bobRossDOMEl.addEventListener("onclick", bobRossObj.onFriendClick)
在上面的代碼中,我們為 Bob Ross 創建了一個對象。我們獲得與 Bob Ross 相關的 DOM 元素。現在我們想要調用 onFriendClick 方法來打開 Bob 的 Facebook 頁面。應該沒問題吧?
不行!
什么地方出了問題?
請注意,我們為 onclick 處理程序選擇的函數是 bobRossObj.onFriendClick。看到問題所在了嗎?如果我們像這樣重寫它:
bobRossDOMEl.addEventListener("onclick", function() { window.open(`https://facebook.com/${this.username}`) })
現在你看到問題所在了嗎?當我們將 onclick 處理程序指定為 bobRossObj.onFriendClick 時,我們實際上是將 bobRossObj.onFriendClick 的函數作為參數傳給了處理程序。它不再“屬于”bobRossObj,也就是說 this 不再指向 bobRossObj。這個時候 this 實際上指向的是全局對象,所以 this.username 是 undefined 的。
是時候讓 bind 上場了!
我們需要做的是將 this 顯式綁定到 bobRossObj。我們可以使用 bind 來實現:
const bobRossObj = initializeFriend(data[0]) const bobRossDOMEl = document.getElementById('Bob_Ross') bobRossObj.onFriendClick = bobRossObj.onFriendClick.bind(bobRossObj) bobRossDOMEl.addEventListener("onclick", bobRossObj.onFriendClick)
之前,this 是基于默認規則設置的。通過使用 bind,我們在 bobRossObj.onFriendClick 中將 this 的值顯式設置為對象本身,也就是 bobRossObj。
到目前為止,我們已經知道為什么 this 很有用以及為什么有時候需要顯式綁定 this。接下來我們要討論的最后一個主題是箭頭函數。
你可能已經注意到,箭頭函數像是一個時髦的新事物。人們似乎很喜歡它們,因為它們簡潔而優雅。你可能已經知道它們與一般函數略有不同,但不一定非常清楚這些區別究竟是什么。
或許箭頭函數的不同之處在于:
在箭頭函數內部,無論 this 處于什么位置,它指的都是相同的東西。
讓我們用 initializeFriend 示例解釋一下。假設我們想在 greeting 中添加一個輔助函數:
function initializeFriend(data) { return { lastTenPosts: data.lastTenPosts, birthday: data.birthday, username: data.username, fullName: `${data.firstName} ${data.lastName}`, getThreeRandomPosts: function() { // get three random posts from this.lastTenPosts }, getDaysUntilBirthday: function() { // use this.birthday to get the num days until birthday }, greeting: function() { function getLastPost() { return this.lastTenPosts[0] } const lastPost = getLastPost() return `Hello, this is ${this.fullName}'s data! ${this.fullName}'s last post was ${lastPost}.` }, onFriendClick: function() { window.open(`https://facebook.com/${this.username}`) } }; }
這樣可以嗎?如果不行,要怎樣修改才行?
這樣當然是不行的。因為 getLastPost 不是在對象的上下文中調用的,所以 getLastPost 中的 this 會回退到默認規則,即指向全局對象。
“在對象的上下文中調用”可能是一個比較含糊的概念。要確定一個函數是否是在“對象的上下文中”被調用,最好的辦法是看一下函數是如何被調用的,以及是否有對象“附加”在函數上。
讓我們來看看執行 bobRossObj.onFriendClick() 時會發生什么:“找到 bobRossObj 對象的 onFriendClick 屬性,調用分配給這個屬性的函數”。
再讓我們來看看執行 getLastPost() 時會發生什么:”調用一個叫作 getLastPost 的函數”。有沒有注意到,這里并沒有提及任何對象?
現在來測試一下。假設有一個叫作 functionCaller 的函數,它所做的事情就是調用其他函數:
functionCaller(fn) { fn() }
如果我們這樣做會怎樣:functionCaller(bobRossObj.onFriendClick)?可不可以說 onFriendClick 是“在對象的上下文中”被調用的?this.username 的定義存在嗎?
讓我們來看一下:“找到 bobRossObj 對象的 onFriendClick 屬性。找到這個屬性的值(恰好是一個函數),將它傳給 functionCaller,并命名為 fn。現在,執行名為 fn 的函數”。請注意,函數在被調用之前已經從 bobRossObj 對象中“分離”,因此不是“在對象 bobRossObj 的上下文中”調用,所以 this.username 是 undefined 的。
讓箭頭函數來救場:
function initializeFriend(data) { return { lastTenPosts: data.lastTenPosts, birthday: data.birthday, username: data.username, fullName: `${data.firstName} ${data.lastName}`, getThreeRandomPosts: function() { // get three random posts from this.lastTenPosts }, getDaysUntilBirthday: function() { // use this.birthday to get the num days until birthday }, greeting: function() { const getLastPost = () => { return this.lastTenPosts[0] } const lastPost = getLastPost() return `Hello, this is ${this.fullName}'s data! ${this.fullName}'s last post was ${lastPost}.` }, onFriendClick: function() { window.open(`https://facebook.com/${this.username}`) } }; }
箭頭函數是在 greeting 中聲明的。我們知道,當我們在 greeting 中使用 this 時,它指向對象本身。因此,箭頭函數中的 this 指向的對象就是我們想要的。
英文原文:
https://medium.freecodecamp.org/a-deep-dive-into-this-in-javascript-why-its-critical-to-writing-good-code-7dca7eb489e7
his理解:
1在函數/方法里邊this代表調用該函數/方法的當前對象
2 在事件中, 代表元素節點對象
divnode.onclick = function(){
alert(this.value);
}
3 代表window
4 可以任意代表其他對象
在call和apply使用的時候, 可以任意設置被執行函數內部this的代表
全局變量是window對象的屬性
var a = 100;
var b = 200;
var c = 300;
var d = 400;
alert(window.a)
alert(window.b)
alert(window.c)
alert(window.d)
函數的上下文(context)
所謂的上下文就是指函數里面的this是誰。就說"函數的上下文是什么"
函數中的this是誰, 看這個函數是如何調用的, 而不是看這個函數如何定義的。
規則1:函數直接圓括號調用, IIFE調用, 上下文是window對象; 對象打點調用obj.fn(), 此時this是obj
直接兩個字, 表示這個函數代號之前, 沒有任何標識符, 沒有小圓點, 沒有方括號。通常從數組、對象中"提"出函數的操作(把函數賦給變量):
在obj對象中定義一個函數,叫fun, 這個是obj的屬性:
var a = 888;
var obj = {
a : 100,
fun : function(){
alert(this.a);
}
}
如直接對象打點調用函數:此時彈出100, 說明函數上下文是obj對象本身。
obj.fun();
但如果這個函數被一個變量接收(讓變量直接指向這個對象的方法)
var fn = obj.fun;
fn(); //這個叫()直接運行
然后調用, 函數的this將是window對象, this.a就是訪問全局的a變量是888
注意: 所有IIFE, 都屬于直接調用范圍, 里面的this都是window。
不管IIFE寫的有多深, 不管所在的環境多復雜, 上下文一律是window對象。
比如下面obj對象中的b, 是IIFE:
var a = 888;
var obj = {
a : 100,
b : (function(){
alert(this.a);
})() //IIFE 立即調用 代表this window對象
}
obj.b; //888
var xiaoming = {
name :"小明",
age : 23,
chengwei: (function(){
console.log(this.age); //undefined 代表window對象
return this.age >= 18 ? "先生" : "小朋友"
})() //IIFE 立即調用
}
alert(xiaoming.name + xiaoming.chengwei)
規則2:定時器直接調用函數, 上下文是window對象
var a = 100;
function fn(){
console.log(this.a++);
}
setInterval(fn,1000)
注意臨門一腳誰踢的, 是誰最終調用那個函數, 比如:
var a = 100;
var obj = {
a : 200,
fn : function(){
console.log(this.a++); //100 102 103 104
}
}
setInterval(obj.fn, 1000); //obj.fn沒有()執行,是定時器調用的, 上下文依然是window對象
var a = 100;
var obj = {
a : 200,
fn : function(){
console.log(this.a++); //200 201 202 203 204
}
}
setInterval(function(){
obj.fn(); //obj.fn()直接調用,上下文的this是obj
}, 1000);
規則3:DOM事件處理函數的this, 指的是觸發事件的這個元素
<div id="box"></div>
var box = document.getElementById("box");
box.onclick = function(){
var self = this; //備份this
setTimeout(function(){
//這里this指向window,所以先在外面備份this,再用
self.style.background = 'red';
},1000)
}
規則4:call()和apply()設置函數的上下文
<div id="box"></div>
var oBox = document.getElementById('box');
function fun(){
console.log(this);
this.style.backgroundColor = 'red';
}
//call和apply作用都一樣,有兩個作用:
//1、執行fun函數
//2、改變fun函數的this指向div
fun.call(oBox)
fun.apply(oBox)
規則5:從對象或數組中枚舉的函數, 上下文是這個對象或數組
來看一個最基本的模型, 就是對象中的方法, 方法中出現this。
如果調用是:對象.方法(), 此時函數中this就是指向這個對象。
var obj = {
a : 100,
b : function(){
alert(this.a)
}
}
obj.b(); //100
數組也一樣,如果一樣函數是從數組中枚舉的,加圓括號執行,數組[0](); 此時上下文就是數組
var arr = [
"A",
"B",
function(){
alert(this.length)
}
]
arr[2](); //輸出3,這寫法是從數組中枚舉出來的,所以是數組在調用函數。
var f = arr[2];
f(); //0 全局沒有length長度
console.log(arr)
規則6:用new調用函數, new fun(), 此時this是秘密新創建的空白對象
function fun(){
alert("我調用");
}
var obj = new fun();
console.log(obj)
function People(name,age,sex){
//構造函數,可以稱為一個“類”,描述的是一個類對象需要擁有的屬性
this.name = name; //this執行new實例化新對象
this.age = age;
this.sex = sex;
}
//構造函數的實例,也可以稱為“類的實例”,就相當于按照類的要求,實例化了一個個人
var xiaoming = new People("小明",12,"男");
var xiaohong = new People("小紅",13,"女");
var xiaogangpao = new People("小鋼炮",16,"女");
console.log(xiaoming)
console.log(xiaohong)
console.log(xiaogangpao)
new是一個動詞, 表示產生"新"的, 會發現, 的確這個函數產生了新的對象。
結論:當用new調用一個函數時, 會發生4四件事(4步走)
1) 函數內部會創建一個新的空對象"{}"
2) 將構造函數的作用域賦值給新對象(因此this就指向這個新的空對象)
3) 執行構造函數中的代碼(為這個新的空對象添加屬性)
4) 函數執行完畢后, 將這個對象返回(return)到外面被接收。(函數將把自己的上下文返回給這個對象)
函數的調用方式 上下文
fun() window
obj.fun() obj
box.onclick = fun box
setInterval(fun,1000) window
setTimeout(fun,1000) window
array[8]() array
new fun() 秘密創建的新對象
規則1:直接圓括號調用fn(), 此時this是window
規則2:對象打點調用obj.fn(),此時this是obj
規則3:數組中枚舉函數調用arr[3](), 此時this是arr
規則4:定時器調用函數setInterval(fn , 10), 此時this是window
規則5:按鈕的事件監聽oBtn.onclick = fn, 此時this是oBtn
規則6:call和allpay可以指定,fn.call(obj), 此時this是obj
規則7:用new調用函數,new fn(), 此時this是秘密新創建的空白對象。
實例1:
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<title>無標題文檔</title>
<script>
var arr=[1,2,3,4];
arr.a=12;
arr.show=function ()
{
alert(this.a); //12, this指的是arr數組
};
/* 類似于以下用法
var oDiv = document.getElementById("op");
oDiv.onclick=function ()
{
alert(this);
};
*/
arr.show();
</script>
</head>
<body>
<div id="op">層內容</div>
</body>
</html>
實例2:
實際上自定義的函數是屬于window對象下的成員方法
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<title>無標題文檔</title>
<script>
window.show=function ()
{
alert(this);
};
show();
</script>
</head>
<body>
</body>
</html>
注意:不能在系統對象中隨意附加方法、屬性,否則會覆蓋已有方法、屬性
實例3:
下來,筆者將按照以下目錄對this進行闡述:
this是JavaScript的一個關鍵字,但它時常蒙著面紗讓人無法捉摸,許多對this不明就里的同學,常常會有這樣的錯誤認知:
this的指向取決于他所處的環境. 大致上,可以分為下面的6種情況:
this在全局范圍內綁定什么呢?這個相信只要學過JS,應該都知道答案。如果不知道,同學真的應該反思自己的學習態度和方法是否存在問題了。話不多說,直接上代碼,一探究竟,揭開this在全局范圍下的真面目:
console.log(this); // Window
不出意外,this在全局范圍內指向window對象()。通常, 在全局環境中, 我們很少使用this關鍵字, 因此對它也沒那么在意. 讓我們繼續看下一個環境.
當我們使用new創建構造函數的實例時會發生什么呢?以這種方式調用構造函數會經歷以下四個步驟:
看完上面的內容,大家想必也知道this在對象的構造函數內的指向了吧!當你使用new關鍵字創建一個對象的新的實例時, this關鍵字指向這個實例 .
舉個栗子:
function Human (age) {
this.age = age;
}
let greg = new Human(22);
let thomas = new Human(24);
console.log(greg); // this.age = 22
console.log(thomas); // this.age = 24
// answer
Person { age:22}
Person { age:24}
方法是與對象關聯的函數的通俗叫法, 如下所示:
let o = {
sayThis(){
console.log(this);
}
}
如上所示,在對象的任何方法內的this都是指向對象本身 .
好了,繼續下一個環境!
可能看到這里,許多同學心里會有疑問,什么是簡單函數?
其實簡單函數大家都很熟悉,就像下面一樣,以相同形式編寫的匿名函數也被認為是簡單函數(非箭頭函數)。
function hello(){
console.log("hello"+this);
}
這里需要注意,在瀏覽器中,不管函數聲明在哪里,匿名或者不匿名,只要不是直接作為對象的方法,this指向始終是window對象(除非使用call,apply,bind修改this指向)。
舉個栗子說明一下:
// 顯示函數,直接定義在sayThis方法內,this指向依舊不變
function simpleFunction() {
console.log(this);
}
var o = {
sayThis() {
simpleFunction();
}
}
simpleFunction(); // Window
o.sayThis(); // Window
// 匿名函數
var o = {
sayThis(){
(function(){consoloe.log(this);})();
}
}
o.sayThis();// Window
對于初學者來說,this在簡單函數內的表現時常讓他們懵逼不已,難道this不應該指向對象本身?這個問題曾經也出現在我的腦海里過,沒錯,在寫代碼時我也踩過這個坑。
通常的,當我們要在對象方法內調用函數,而這個函數需要用到this時,我們都會創建一個變量來保存對象中的this的引用. 通常, 這個變量名稱叫做self或者that。具體說下所示:
const o = {
doSomethingLater() {
const self = this;
setTimeout(function() {
self.speakLeet();
}, 1000);
},
speakLeet() {
console.log(`1337 15 4W350M3`);
}
}
o.doSomethingLater(); // `1337 15 4W350M3`
心細的同學可能已經發現,這里的簡單函數沒有將箭頭函數包括在內,那么下一個環境是什么想必也能猜到啦,那么現在進入下一個環境,看看this指向什么。
和簡單函數表現不太一樣,this在箭頭函數中總是跟它在箭頭函數所在作用域的this一樣(在它直接作用域). 所以, 如果你在對象中使用箭頭函數, 箭頭函數中的this總是指向這個對象本身, 而不是指向Window.
下面我們使用箭頭函數,重寫一下上面的案例:
const o = {
doSomethingLater() {
setTimeout(() => this.speakLeet(), 1000);
},
speakLeet() {
console.log(`1337 15 4W350M3`);
}
}
o.doSomethingLater(); // `1337 15 4W350M3`
最后,讓我們來看看最后一種環境 - 事件偵聽器.
在事件偵聽器內, this被綁定的是觸發這個事件的元素:
let button = document.querySelector('button');
button.addEventListener('click', function() {
console.log(this); // button
});
事實上,只要記住上面this在不同環境的綁定值,足以應付大部分工作。然而,好學的同學總是會忍不住想說,為什么呢?對,為什么this在這些情況下綁定這些值呢?學習,我們不能只知其然,而不知所以然。所以,現在就讓我們來探尋,this值獲取的真相吧。
現在,讓我們回憶一下,在講什么是this的時候,我們說到“this的綁定取決于他所處的環境”。這句話其實不是十分準確,準確的說,this不是編寫時綁定,而是運行時綁定。它依賴于函數調用的上下文條件。this綁定和函數聲明的位置無關,反而和函數被調用的方式有關。
當一個函數被調用時,會建立一個活動記錄,也稱為執行環境。這個記錄包含函數是從何處(call-stack)被調用的,函數是 如何被調用的,被傳遞了什么參數等信息。這個記錄的屬性之一,就是在函數執行期間將被使用的this引用。this實際上是在函數被調用時建立的一個綁定,它指向什么是完全由函數被調用的調用點來決定的。
現在我們將注意力轉移到調用點 如何 決定在函數執行期間this指向哪里。
你必須考察call-site并判定4種規則中的哪一個適用。我們將首先獨立的解釋一下這4種規則中的每一種,之后我們來展示一下如果有多種規則可以適用調用點時,它們的優先級。
第一種規則來源于函數調用的最常見的情況:獨立函數調用。可以認為這種this規則是在沒有其他規則適用時的默認規則。我們給它一個稱呼“默認綁定”.
現在來看這段代碼:
function foo(){
console.log(this);
}
var a = 2;
demo(); // 2
當foo()被調用時,this.a解析為我們的全局變量a。為什么?因為在這種情況下,對此方法調用的this實施了 默認綁定,所以使this指向了全局對象。
在我們的代碼段中,foo()是被一個直白的,毫無修飾的函數引用調用的。沒有其他的我們將要展示的規則適用于這里,所以 默認綁定 在這里適用。
如果strict mode在這里生效,那么對于 默認綁定 來說全局對象是不合法的,所以this將被設置為undefined。
'use strict'
function foo(){
console.log(this.a); // TypeError: Cannot read property 'a' of undefined
}
const a = 1;
foo();
function foo(){
'use strict'
console.log(this.a); // TypeError: Cannot read property 'a' of undefined
}
const a = 1;
foo();
微妙的是,即便所有的this綁定規則都是完全基于調用點,如果foo()的 內容 沒有在strint mode下執行,對于 默認綁定 來說全局對象是 唯一 合法的;foo()的call-site的strict mode狀態與此無關。
function foo(){
console.log(this.a);
}
var a = 1;
(function(){
'use strict';
foo(); // 1
})();
注意: 在代碼中故意混用strict mode和非strict mode通常是讓人皺眉頭的。你的程序整體可能應當不是 Strict 就是非Strict。然而,有時你可能會引用與你的 Strict 模式不同的第三方包,所以對這些微妙的兼容性細節要多加小心。
另一種要考慮的規則是:調用點是否有一個環境對象(context object),也稱為擁有者(owning)或容器(containing)對象。
讓我們來看這段代碼:
function foo() {
console.log(this.a);
}
let o = {
a: 2,
foo,
}
o.foo(); // 2
這里,我們注意到foo函數被聲明然后作為對象o的方法,無論foo()是否一開始就在obj上被聲明,還是后來作為引用添加(如上面代碼所示),都是這個 函數 被obj所“擁有”或“包含”。這里,調用點使用obj環境來引用函數,所以可以說 obj對象在函數被調用的時間點上“擁有”或“包含”這個 函數引用。
當一個方法引用存在一個環境對象時,隱式綁定 規則會說:是這個對象應當被用于這個函數調用的this綁定。
只有對象屬性引用鏈的最后一層是影響調用點的。比如:
function foo(){
console.log(this.a);
}
var obj1 = {
a:2,
obj2:obj2
};
var obj2 = {
a:42,
foo:foo
};
obj1.obj2.foo(); // 42
隱式綁定的隱患
當一個 隱含綁定丟失了它的綁定,這通常意味著它會退回到 默認綁定, 根據strict mode的狀態,結果不是全局對象就是undefined。
下面來看這段代碼:
function foo(){
console.log(this.a);
}
var obj = {
a:2,
foo
};
var bar = obj.foo;
var a = "Global variable";
bar(); // "Global variable"
盡管bar似乎是obj.foo的引用,但實際上它只是另一個foo自己的引用而已。另外,起作用的調用點是bar(),一個直白,毫無修飾的調用,因此 默認綁定 適用于這里。
這種情況發生的更加微妙,更常見,更意外的方式,是當我們考慮傳遞一個回調函數時:
function foo(){
console.log(this.a);
}
function doFoo(fn){
fn();
}
var obj = {
a:2,
foo,
};
var a = "Global variable";
dooFoo(obj.foo); // "Global variable"
參數傳遞僅僅是一種隱含的賦值,而且因為我們在傳遞一個函數,它是一個隱含的引用賦值,所以最終結果和我們前一個代碼段一樣。同樣的,語言內建,如setTimeout也一樣,如下所示
function foo(){
console.log(this.a);
}
var obj = {
a:2,
foo,
};
var a = "Global variable";
setTimeout(obj.foo, 100); // "Global variable"
把這個粗糙的setTimeout()假想實現當做JavaScript環境內建的實現的話:
function setTimeout(fn, delay){
// 等待delay毫秒
fn();
}
正如我們看到的, 隱含綁定丟失了它的綁定是十分常見的,不管哪一種意外改變this的方式,你都不能真正地控制你的回調函數引用將如何被執行,所以你(還)沒有辦法控制調用點給你一個故意的綁定。但是我們可以使用顯示綁定強行固定this。
我們看到隱含綁定,需要我們不得不改變目標對象使它自身包含一個對函數的引用,而后使用這個函數引用屬性來間接地(隱含地)將this綁定到這個對象上。
但是,如果你想強制一個函數調用使用某個特定對象作為this綁定,而不在這個對象上放置一個函數引用屬性呢?
js有提供call()、apply()方法,ES5中也提供了內置的方法 Function.prototype.bind,可以引用一個對象時進行強制綁定調用。
考慮這段代碼:
function foo(){
console.log(this.a);
}
var obj = {
a:2,
};
foo.call(obj); // 2
通過foo.call(..)使用 明確綁定 來調用foo,允許我們強制函數的this指向obj。
如果你傳遞一個簡單原始類型值(string,boolean,或 number類型)作為this綁定,那么這個原始類型值會被包裝在它的對象類型中(分別是new String(..),new Boolean(..),或new Number(..))。這通常稱為“boxing(封箱)”。
注意: 就this綁定的角度講,call(..)和apply(..)是完全一樣的。它們確實在處理其他參數上的方式不同,但那不是我們當前關心的。
單獨依靠call和apply,仍然可能出現函數“丟失”自己原本的this綁定,或者被第三方覆蓋等問題。
但有一個技巧可以避免出現這些問題
考慮這段代碼:
function foo(){
console.log(this.a);
}
var obj = {
a:2
};
var bar = function(){
foo.call(obj);
}
bar(); // 2
setTimeout(bar, 100); // 2
bar.call(window); // 2
我們創建了一個函數bar(),在它的內部手動調用foo.call(obj),由此強制this綁定到obj并調用foo。無論你過后怎樣調用函數bar,它總是手動使用obj調用foo。這種綁定即明確又堅定,該方法被開發者稱為 硬綁定(顯示綁定的變種)(hard binding)
用硬綁定將一個函數包裝起來的最典型的方法,是為所有傳入的參數和傳出的返回值創建一個通道:
function foo(something){
console.log(this.a, something);
return this.a + something;
}
var obj = {
a:2
};
var bar = function() {
return foo.apply(obj, arguments);
}
var b = bar(3);
console.log(b); // 5
另一種表達這種模式的方法是創建一個可復用的幫助函數:
function foo(something){
console.log(this.a, something);
return this.a + something;
}
function bind(fn, obj){
return function(){
return fn.apply(obj, arguments);
};
}
var obj = { a:2};
var bar = bind(foo, obj);
var b = bar(3);
console.log(b); // 5
由于 硬綁定 是一個如此常用的模式,它已作為ES5的內建工具提供,即前文提到的Function.prototype.bind:
function foo(something){
console.log(this.a, something);
return this.a + something;
}
var obj = { a:2};
var bar = foo.bind(obj);
var b = bar();
cobsole.log(b); // 5
bind(..)返回一個硬編碼的新函數,它使用你指定的this環境來調用原本的函數。
注意: 在ES6中,bind(..)生成的硬綁定函數有一個名為.name的屬性,它源自于原始的 目標函數(target function)。舉例來說:bar = foo.bind(..)應該會有一個bar.name屬性,它的值為"bound foo",這個值應當會顯示在調用棧軌跡的函數調用名稱中。
第四種也是最后一種this綁定規則
當在函數前面被加入new調用時,也就是構造器調用時,下面這些事情會自動完成:
考慮這段代碼:
function foo(a){
console.log(this.a);
}
var bar = new foo(2);
console.log(bar.a); // 2
通過在前面使用new來調用foo(..),我們構建了一個新的對象并這個新對象作為foo(..)調用的this。 new是函數調用可以綁定this的最后一種方式,我們稱之為 new綁定(new binding)。
箭頭函數并非使用function關鍵字進行定義,而是通過所謂的“大箭頭”操作符:=>,所以不會使用上面所講解的this四種標準規范,箭頭函數從封閉它的(function或global)作用域采用this綁定,即箭頭函數會繼承自外層函數調用的this綁定。
執行 fruit.call(apple)時,箭頭函數this已被綁定,無法再次被修改。
function fruit(){
return () => {
console.log(this.name);
}
}
var apple = {
name: '蘋果'
}
var banana = {
name: '香蕉'
}
var fruitCall = fruit.call(apple);
fruitCall.call(banana); // 蘋果
this是JavaScript的一個關鍵字,this不是編寫時綁定,而是運行時綁定。它依賴于函數調用的上下文條件。this綁定和函數聲明的位置無關,反而和函數被調用的方式有關。為執行中的函數判定this綁定需要找到這個函數的直接調用點。找到之后,4種規則將會以 這個 優先順序施用于調用點:
與這4種綁定規則不同,ES6的箭頭方法使用詞法作用域來決定this綁定,這意味著它們采用封閉他們的函數調用作為this綁定(無論它是什么)。它們實質上是ES6之前的self = this代碼的語法替代品。
*請認真填寫需求信息,我們會在24小時內與您取得聯系。