最近零零碎碎看了許多關於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程式碼的語法替代品。
參考文章: