JavaScript 中的 this 並不難

js學習筆記 --- this 詳解

js中的this,若是沒有深刻的學習瞭解,那麼this將會是讓開發人員很頭疼的問題。下面,我就針對this,來作一個學習筆記。前端

1.調用位置

在理解this的綁定過程以前,首先要理解調用位置:調用位置就是函數在代碼中被調用的位置(而不是聲明的位置)。只有分析好調用位置,才能明白這個this到底引用的是什麼?
尋找調用位置,最重要的是分析調用棧(就是爲了到達當前執行位置所調用的全部函數)。調用位置就在當前正在執行的前一個調用中。
下面舉例說明:java

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 隱式綁定

另外一條須要考慮的規則是調用位置是否有上下文對象,或者說是否被某個對象擁有或者包含。舉例來講:app

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是同樣的。測試

對象屬性引用鏈中只有最後一層會影響調用位置。上代碼:this

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()實際上是一個不帶任何修飾符的函數調用,所以應用了默認綁定。
下面舉一個回調函數中隱式丟失的例子:spa

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來調用函數,或者說發生構造函數調用時,會自動執行下面的操做。

  1. 建立(或者說構造)一個全新的對象;
  2. 建立這個新對象會被執行[[原型]]鏈接;
  3. 這個新對象會綁定到函數調用的this;
  4. 若是函數沒有返回其餘對象,那麼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,公衆號:愛生活的前端狗的做者。公衆號專一前端工程師方向,包括但不限於技術提升、職業規劃、生活品質、我的理財等方面,會持續發佈優質文章,從各個方面提高前端開發的幸福感。關注公衆號,咱們一塊兒向前走!

相關文章
相關標籤/搜索