js學習筆記 — this 詳解
js中的this,如果沒有深入的學習瞭解,那麼this將會是讓開發人員很頭疼的問題。下面,我就針對this,來做一個學習筆記。
1.調用位置
在理解this的綁定過程之前,首先要理解調用位置:調用位置就是函數在代碼中被調用的位置(而不是聲明的位置)。只有分析好調用位置,才能明白這個this到底引用的是什麼?
尋找調用位置,最重要的是分析調用棧(就是爲了到達當前執行位置所調用的所有函數)。調用位置就在當前正在執行的前一個調用中。
下面舉例說明:
function baz (){
// 當前調用棧是:baz
//因此,當前調用位置是全局作用域
console.log("baz");
bar();// <-- bar的調用位置
}
function bar(){
// 當前調用棧是 baz -> bar
// 因此,當前調用位置在 baz 中
console.log('bar');
foo();// <-- foo 的調用位置
}
function foo(){
// 當前調用棧是baz -> bar -> foo
// 因此,當前調用位置在bar中
console.log("foo");
}
baz(); // <-- baz的調用位置
2.綁定規則
2.1 默認綁定
首先看一下最常用的函數調用類型:獨立函數調用。可以把這條規則看作是無法應用其他規則時的默認規則。
如下例:
function foo() {
console.log(this.a);
}
var a = 2;
foo(); // 2
// 在本代碼中,foo() 是直接使用不帶任何修飾的函數引用進行調用的,因此只能使用默認綁定,無法應用其他規則。
// 如果使用嚴格模式,那麼全局對象無法使用默認綁定,因此this會綁定到undefined。
2.2 隱式綁定
另一條需要考慮的規則是調用位置是否有上下文對象,或者說是否被某個對象擁有或者包含。舉例來說:
function foo() {
console.log(this.a);
}
var obj = {
a:2,
foo
};
obj.foo(); // 2
首先要注意的是foo()的聲明方式,以及之後是如何被當做引用屬性添加到obj的。但是無論是直接在obj中定義還是先定義再添加爲引用屬性,這個函數嚴格來說都不屬於obj 對象。
然而,調用位置會使用obj的上下文來引用函數,因此,可以說函數被調用時obj對象“擁有”或者“包含”它。
無論如何稱呼這個模式,當foo()被調用時,落腳點確實指向obj對象。當函數引用有上下文對象時,隱式綁定規則會把函數調用中的this綁定到這個上下文對象。所以this.a和obj.a是一樣的。
對象屬性引用鏈中只有最後一層會影響調用位置。上代碼:
function foo() {
console.log(this.a);
}
var obj2 = {
a:42,
foo
};
var obj1 = {
a:2,
obj2
};
obj1.obj2.foo(); // 42
2.2.1 隱式丟失
一個最常見額this綁定問題就是被隱式綁定的函數會丟失綁定對象,會應用默認綁定,從而把this綁定到全局對象或者undefined上,取決於是否是嚴格模式。看下面的代碼:
function foo() {
console.log(this.a);
}
var obj = {
a:2,
foo
};
var bar = obj.foo; // 函數別名!
var a = "What?"; // a 是全局對象的屬性
bar();//"What?"
雖然bar是obj.foo 的一個引用,但是實際上,它引用的是foo函數本身,因此此時的bar()其實是一個不帶任何修飾符的函數調用,因此應用了默認綁定。
下面舉一個回調函數中隱式丟失的例子:
function foo() {
console.log(this.a);
}
function doFoo(fn){
// fn 其實引用的是foo
fn(); // <- 調用位置
}
var obj = {
a:2,
foo
};
var a = "What?"; // a 是全局對象的屬性
doFoo(obj.foo);//"What?"
參數傳遞其實就是一種隱式賦值,傳入函數時也會被隱式賦值,所以結果和上一個例子一樣。
2.3 顯示綁定
在上面隱式綁定的時候,必須在一個對象內部包含一個指向函數的屬性,並通過這個屬性間接引用函數,從而把this間接(隱式)的綁定到這個對象上。
如果我們不想在對象內部包含函數引用,而想在某個對象上強制調用函數,該如何處理?
基本上大部分函數會包含call(…)和apply(…)方法。但是有的時候JavaScript的宿主環境有時候會提供一些非常特殊的函數,可能沒有這兩個方法,但是極爲罕見。
這兩個函數的第一個參數是一個對象,會把這個對象綁定到this,接着在調用函數時指定這個this。因爲這種方式可以直接指定this的綁定對象,因此我們稱之爲顯示綁定。
上代碼:
function foo() {
console.log(this.a);
}
var obj = {
a:2
};
foo.call(obj); // 2
通過foo.call(…),我們可以在調用foo的時候強制將this綁定在obj上。
如果從傳入了一個原始值(字符串類型、布爾類型或者數字類型)來當做this的綁定對象,這個原始值會被轉換成它的對象形式(也就是new String(…)、new Boolean(…)或者new Number(…)),這通常稱爲“裝箱”。
從this的綁定的角度來說,call(...)和apply(...)是一樣的,他們的區別體現在其他的參數上。
不過上述的代碼不能很好地解決我們提出的丟失綁定的問題。
2.3.1 硬綁定
不過顯示綁定的一個變種可以解決這個問題。
上代碼:
function foo() {
console.log(this.a);
}
var obj = {
a:2
};
var bar = function(){
foo.call(obj);
}
var a = '123';
bar(); // 2
setTimeout(bar,10); // 2
bar.call(window); // 2 此時硬綁定的bar不能修改foo的this。foo總會在obj上調用。
由於硬綁定是一種非常常用的模式,所以在ES5中提供了內置方法 bind ,它的用法如下:
function foo(str) {
console.log(this.a, str)
return this.a + str;
}
var obj = {
a: 2
};
var bar = foo.bind(obj);
var b = bar(3);// 2 3
console.log(b);// 5
2.3.2 API調用的“上下文”
第三方庫的許多函數,以及javaScript語言和宿主環境中的許多新的內置函數,都提供了一個可選的參數,通常被稱爲上下文,其作用和bind一樣,確保回調函數使用指定的this。上代碼:
function foo (item){
console.log(item,this.id);
}
var obj = {
id:"cool"
};
// 調用foo()時把this綁定到obj
[1,2,3].forEach(foo,obj);
// 1 cool 2 cool 3 cool
2.4 new綁定
js中使用new可以構造一個新的對象,使用new來調用函數,或者說發生構造函數調用時,會自動執行下面的操作。
- 創建(或者說構造)一個全新的對象;
- 創建這個新對象會被執行[[原型]]連接;
- 這個新對象會綁定到函數調用的this;
- 如果函數沒有返回其他對象,那麼new表達式中的函數調用會自動返回這個新對象。
上代碼:
function foo(a) {
this.a = a;
}
var a = 3;
var bar = new foo(2);
console.log(bar.a); // 2
使用new來調用foo()時,會構造一個新對象並綁定到foo()調用中的this上。
3.優先級。
- 毫無疑問,默認綁定的優先級是最低的。
那麼隱式綁定和顯示綁定誰更高?上代碼:
function foo() {
console.log( this.a );
}
var obj1 = {
a: 2,
foo: foo
};
var obj2 = {
a: 3,
foo: foo
};
obj1.foo(); // 2
obj2.foo(); // 3
obj1.foo.call( obj2 ); // 3
obj2.foo.call( obj1 ); // 2
可以看到,顯式綁定優先級更高,也就是說在判斷時應當先考慮是否可以應用顯式綁定。
- new 綁定 VS 隱式綁定:
function foo(something) {
this.a = something;
}
var obj1 = {
foo: foo
};
var obj2 = {};
obj1.foo( 2 );
console.log( obj1.a ); // 2
obj1.foo.call( obj2, 3 );
console.log( obj2.a ); // 3
var bar = new obj1.foo( 4 );
console.log( obj1.a ); // 2
console.log( bar.a ); // 4
可以看到 new 綁定比隱式綁定優先級高。
- new 綁定 VS 顯示綁定:
new 和 call/apply 無法一起使用,因此無法通過 new foo.call(obj1) 來直接
進行測試。但是我們可以使用硬綁定來測試它倆的優先級。
function foo(something) {
this.a = something;
}
var obj1 = {};
var bar = foo.bind( obj1 );
bar( 2 );
console.log( obj1.a ); // 2
var baz = new bar(3);
console.log( obj1.a ); // 2
console.log( baz.a ); // 3
可以看到,new 修改了硬綁定(到 obj1 的)調用 bar(…) 中的 this。因爲使用了new 綁定,我們得到了一個名字爲 baz 的新對象,並且 baz.a 的值是 3。
總結
現在我們可以根據優先級來判斷函數在某個調用位置應用的是哪條規則。可以按照下面的
順序來進行判斷:
1.函數是否在 new 中調用(new 綁定)?如果是的話 this 綁定的是新創建的對象。
var bar = new foo()
2.函數是否通過 call、apply(顯式綁定)或者硬綁定調用?如果是的話,this 綁定的是指定的對象
var bar = foo.call(obj2)
3.函數是否在某個上下文對象中調用(隱式綁定)?如果是的話,this 綁定的是那個上下文對象。
var bar = obj1.foo()
4.如果都不是的話,使用默認綁定。如果在嚴格模式下,就綁定到 undefined,否則綁定到全局對象。
var bar = foo()
4.箭頭函數
之前介紹的四條規則已經可以包含所有正常的函數。但是 ES6 中介紹了一種無法使用這些規則的特殊函數類型:箭頭函數。
箭頭函數並不是使用 function 關鍵字定義的,而是使用被稱爲“胖箭頭”的操作符 => 定義的。箭頭函數不使用 this 的四種標準規則,而是根據外層(函數或者全局)作用域來決定 this。
function foo() {
// 返回一個箭頭函數
return (a) => {
//this 繼承自 foo()
console.log( this.a );
};
}
var obj1 = {
a:2
};
var obj2 = {
a:3
};
var bar = foo.call( obj1 );
bar.call( obj2 ); // 2, 不是 3 !
foo() 內部創建的箭頭函數會捕獲調用時 foo() 的 this。由於 foo() 的 this 綁定到 obj1,
bar(引用箭頭函數)的 this 也會綁定到 obj1,箭頭函數的綁定無法被修改。(new 也不
行!)
箭頭函數最常用於回調函數中,例如事件處理器或者定時器:
function foo() {
setTimeout(() => {
// 這裏的 this 在此法上繼承自 foo()
console.log( this.a );
},100);
}
var obj = {
a:2
};
foo.call( obj ); // 2
箭頭函數可以像 bind(…) 一樣確保函數的 this 被綁定到指定對象,此外,其重要性還體
現在它用更常見的詞法作用域取代了傳統的 this 機制。實際上,在 ES6 之前我們就已經
在使用一種幾乎和箭頭函數完全一樣的模式。
function foo() {
var self = this; // lexical capture of this
setTimeout( function(){
console.log( self.a );
}, 100 );
}
var obj = {
a: 2
};
foo.call( obj ); // 2
雖然 self = this 和箭頭函數看起來都可以取代 bind(…),但是從本質上來說,它們想替
代的是 this 機制。
參考資料
- 《你不知道的javaScript》—上卷
你好!我是 JHCan333,公衆號:愛生活的前端狗的作者。公衆號專注前端工程師方向,包括但不限於技術提高、職業規劃、生活品質、個人理財等方面,會持續發佈優質文章,從各個方面提升前端開發的幸福感。關注公衆號,我們一起向前走!