一文梳理JavaScript中的this
最近零零碎碎看了許多關於this的文章,本着“好記性不如爛筆頭”的思想,特在這裏整理一下this有關的知識點。【長文警告!!!】
接下來,筆者將按照以下目錄對this進行闡述:
- this是什麼?
- this指向
- this在全局範圍內
- this在對象的構造函數內
- this在對象的方法內
- this在簡單函數內
- this在箭頭函數內
- this在一個事件偵聽器內
- this綁定規則
- 默認綁定
- 隱式綁定
- 显示綁定(this修改)
- 優先級
- 箭頭函數
1. this是什麼?
this是JavaScript的一個關鍵字,但它時常矇著面紗讓人無法捉摸,許多對this不明就裡的同學,常常會有這樣的錯誤認知:
-
this在函數內指向函數自身
-
function foo(num){ console.log("foo: " + num); //記錄foo被調用次數 this.count++; } foo.count = 0; for(let i=0; i<10; i++){ if(i > 5){ foo(i); } } console.log(foo.count); // 0, this並沒有指向foo函數,foo.count沒有進行任何操作
-
-
this在函數內指向函數的作用域
-
function foo(){ var a = 2; this.bar(); } function bar(){ console.log(this.a); } foo();// undefined, window對象沒有bar這一屬性
-
2. this指向
this的指向取決於他所處的環境. 大致上,可以分為下面的6種情況:
- this在全局範圍內
- this在對象的構造函數內
- this在對象的方法內
- this在一個簡單的函數內
- this在箭頭函數內
- this在一個事件偵聽器內
2.1 this在全局範圍內
this在全局範圍內綁定什麼呢?這個相信只要學過JS,應該都知道答案。如果不知道,同學真的應該反思自己的學習態度和方法是否存在問題了。話不多說,直接上代碼,一探究竟,揭開this在全局範圍下的真面目:
console.log(this); // Window
不出意外,this在全局範圍內指向window對象()。通常, 在全局環境中, 我們很少使用this關鍵字, 因此對它也沒那麼在意. 讓我們繼續看下一個環境.
2.2 this在對象的構造函數內
當我們使用new創建構造函數的實例時會發生什麼呢?以這種方式調用構造函數會經歷以下四個步驟:
-
創建一個空對象;
-
將構造函數的作用域賦給新對象(this指向了這個新對象),繼承函數的原型;
-
執行構造函數中的代碼;
-
返回新對象。
看完上面的內容,大家想必也知道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}
2.3 this在對象方法內
方法是與對象關聯的函數的通俗叫法, 如下所示:
let o = {
sayThis(){
console.log(this);
}
}
如上所示,在對象的任何方法內的this都是指向對象本身 .
好了,繼續下一個環境!
2.4 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指向什麼。
2.5 this在箭頭函數內
和簡單函數表現不太一樣,this在箭頭函數中總是跟它在箭頭函數所在作用域的this一樣(在它直接作用域). 所以, 如果你在對象中使用箭頭函數, 箭頭函數中的this總是指向這個對象本身, 而不是指向Window.
下面我們使用箭頭函數,重寫一下上面的案例:
const o = {
doSomethingLater() {
setTimeout(() => this.speakLeet(), 1000);
},
speakLeet() {
console.log(`1337 15 4W350M3`);
}
}
o.doSomethingLater(); // `1337 15 4W350M3`
最後,讓我們來看看最後一種環境 – 事件偵聽器.
2.6 this在事件偵聽器內
在事件偵聽器內, this被綁定的是觸發這個事件的元素:
let button = document.querySelector('button');
button.addEventListener('click', function() {
console.log(this); // button
});
3. this綁定規則
事實上,只要記住上面this在不同環境的綁定值,足以應付大部分工作。然而,好學的同學總是會忍不住想說,為什麼呢?對,為什麼this在這些情況下綁定這些值呢?學習,我們不能只知其然,而不知所以然。所以,現在就讓我們來探尋,this值獲取的真相吧。
現在,讓我們回憶一下,在講什麼是this的時候,我們說到“this的綁定取決於他所處的環境”。這句話其實不是十分準確,準確的說,this不是編寫時綁定,而是運行時綁定。它依賴於函數調用的上下文條件。this綁定和函數聲明的位置無關,反而和函數被調用的方式有關。
當一個函數被調用時,會建立一個活動記錄,也稱為執行環境。這個記錄包含函數是從何處(call-stack)被調用的,函數是 如何被調用的,被傳遞了什麼參數等信息。這個記錄的屬性之一,就是在函數執行期間將被使用的this引用。this實際上是在函數被調用時建立的一個綁定,它指向什麼是完全由函數被調用的調用點來決定的。
僅僅是規則
現在我們將注意力轉移到調用點 如何 決定在函數執行期間this指向哪裡。
你必須考察call-site並判定4種規則中的哪一個適用。我們將首先獨立的解釋一下這4種規則中的每一種,之後我們來展示一下如果有多種規則可以適用調用點時,它們的優先級。
3.1 默認綁定規則
第一種規則來源於函數調用的最常見的情況:獨立函數調用。可以認為這種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 模式不同的第三方包,所以對這些微妙的兼容性細節要多加小心。
3.2 隱式綁定
另一種要考慮的規則是:調用點是否有一個環境對象(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。
3.3 显示綁定
我們看到隱含綁定,需要我們不得不改變目標對象使它自身包含一個對函數的引用,而後使用這個函數引用屬性來間接地(隱含地)將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”,這個值應當會显示在調用棧軌跡的函數調用名稱中。
3.4new 綁定
第四種也是最後一種this綁定規則
當在函數前面被加入new調用時,也就是構造器調用時,下面這些事情會自動完成:
- 一個全新的對象會憑空創建(就是被構建)
- 這個新構建的對象會被接入原形鏈([[Prototype]]-linked)
- 這個新構建的對象被設置為函數調用的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)。
3.5 優先級
- new綁定
- 显示綁定
- 隱式綁定
- 默認綁定(嚴格模式下會綁定到undefined)
4. 箭頭函數
箭頭函數並非使用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); // 蘋果
5. 小結
this是JavaScript的一個關鍵字,this不是編寫時綁定,而是運行時綁定。它依賴於函數調用的上下文條件。this綁定和函數聲明的位置無關,反而和函數被調用的方式有關。為執行中的函數判定this綁定需要找到這個函數的直接調用點。找到之後,4種規則將會以 這個 優先順序施用於調用點:
- 被new調用?使用新構建的對象。
- 被call或apply(或 bind)調用?使用指定的對象。
- 被持有調用的環境對象調用?使用那個環境對象。
- 默認:strict mode下是undefined,否則就是全局對
與這4種綁定規則不同,ES6的箭頭方法使用詞法作用域來決定this綁定,這意味着它們採用封閉他們的函數調用作為this綁定(無論它是什麼)。它們實質上是ES6之前的self = this代碼的語法替代品。
參考文章:
深入理解JavScript中的this
詳解JavaScript中的this
你不懂this:豁然開朗
你不懂this:this是什麼?
本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】
※教你寫出一流的銷售文案?
※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益
※回頭車貨運收費標準
※別再煩惱如何寫文案,掌握八大原則!
※超省錢租車方案
※產品缺大量曝光嗎?你需要的是一流包裝設計!