一文看穿JavaScript中this的圈圈繞

導文目錄


  • 爲何說JavaScript中 this 指針圈圈繞?
  • JavaScript 中 this 綁定做用域的四種狀況
    • 先搞清Node環境中和瀏覽器環境中全局對象的異同
    • 默認綁定
    • 隱式綁定
    • 硬綁定(或者說 顯示綁定)
    • new操做符綁定
    • 四種綁定的優先級
  • ES6中引入箭頭函數對this的綁定產生了什麼影響?
  • 附上前面程序的輸出答案

爲何說JavaScript中 this 指針圈圈繞?


相比C++或者Java中的this指針的概念而言,JavaScript中的this指針更爲 "靈活" ,C++或Java中的 this在類定義時便成爲了一個指向特定類實例的指針,可是JavaScript中的this指針是能夠動態綁定的,也就是說是依據上下文環境來指定this到底指向誰。這樣的機制給編程帶來了很大的靈活性(以及趣味性),但這也致使了在JavaScript編程中若不明白this指針的做用機制而濫用this指針的話,經常會引起一些 "莫名其妙" 的問題。好比說,下面這段程序:javascript

1. let num = 10;
2. console.log(global.num); 
3. global.num = 20;
4. console.log(this.num);
5. console.log(this === global); 
6. function A(){
7.    console.log(this.num++);
8. }
9. let obj = {
10.     num : 30,
11.     B : function(){
12.         console.log(this.num++);
13.         return () => console.log(this.num++);
14.      }
15. }
16. A();
17. let b = obj.B; 
18. b()();  
19. obj.B();   
20. b.apply(obj);
21. new A(); 
22. console.log(global.num); 
複製代碼

你能列出最終全部的輸出嗎?你能夠先嚐試着寫一下,不要複製到VSCode中運行哦~ ,手動寫出答案!寫完先看一下最後面的答案,看你是否寫對了。若是寫對了說明你已經基本掌握了JavaScript中this指針的機制 (PS:設定這裏運行環境是node環境 ;若是沒有寫對,那看完本文相信就能夠對this有一個基本清楚的認識了。java

相信你確定忍不住去看了答案了,或許答案看起來雜亂無章,這也是爲何this做爲JavaScript中最複雜的機制之一常常被拿到面試中去考察JS的功底。如下內容可能須要花費8-10分鐘時間,可是會讓讀者你受益不淺的,你的疑問也能夠在下面的內容中獲得解答!node

JavaScript 中 this 綁定做用域的四種狀況


先搞清Node環境中和瀏覽器環境中全局對象的異同

在講解this綁定做用域的四種狀況以前,咱們先要弄清楚一個問題。Node環境中的全局做用域和瀏覽器環境下的全局做用域有什麼不一樣?面試

這個問題很重要,由於這個異同,會致使一樣的代碼在Node環境和瀏覽器環境下的表現不盡相同。就好比咱們這裏要講的this指針的指向會由於環境不一樣而不一樣。這個不一樣體如今如下三點:編程

  • 瀏覽器的全局做用域的全局對象是window ; 而Node中這個"等價"的全局對象是global
  • 瀏覽器環境下全局做用域下的this指向的就是window對象; 可是Node環境下全局做用域中的thisglobal是分離的,this指針指向一個空對象
  • 瀏覽器環境下全局做用域中聲明的變量會被認爲是全局對象window的屬性;可是Node下全局做用域下的聲明的變量不屬於global

由此,你即可知,上面代碼中1-5的輸出了,就像下面這樣:瀏覽器

undefined  // 1 
undefined  // 2
false      // 3
複製代碼

爲了方便講解,我給每一個輸出編了號,咱們依次來看:微信

  1. 第一個undefined是由於Node的全局做用域上的變量並不會做爲global的屬性,此時global.num還沒有賦值,因此是undefined
  2. 第二個undefined是由於Node中全局做用域中的this並不指向global,因此此時this.num還沒有賦值,因此也是undefined
  3. 第三個false也更加應證了 2 中的結論,Node中全局做用域的thisglobal風馬牛不相及

【PS】上面我一直強調是全局做用域下的this ,爲何呢?由於Node中在子做用域中的this的行爲和瀏覽器中是相仿的,基本一致閉包

默認綁定

下面咱們來說解JavaScript中this綁定做用域的四種狀況。app

先來講第一種——默認綁定 ,咱們能夠這樣理解 默認綁定 ,this指針在做用域內沒有認領的對象時就會默認綁定到全局做用域的全局對象中去,在Node中就是會綁定到global對象上去。這是一個很形象的說法,雖然不嚴謹可是好理解,咱們看下面這幾個例子,來講明什麼狀況下,this沒有對象認領。函數

global.name = 'javascript';
(function A(){
    this.name += 'this';
    console.log(this.name);//輸出 javascriptthis
})();
console.log(global.name);//輸出 javascriptthis
複製代碼

在函數A的做用域內,this並無能夠依靠的對象,因此this指針便開啓默認綁定模式,此時指向的是global

這裏咱們有必要明確一個概念,有別於JavaScript中"一切皆爲對象"的概念,雖然A確實是一個Function類型的對象 , 下面的例子可證實確實如此

function A(){}
console.log(A instanceof Function); //輸出 true
console.log(A instanceof Object);   //輸出 true
複製代碼

可是function A(){}只是一個函數的聲明,並無實例對象的產生,而this是須要依託於一個存在的實例對象 , 若是使用new A()則便有了實例對象,this也就有了依託,有關new操做符綁定在後面說。

明白了這一點,咱們來看一個更爲複雜的例子:

global.name = 'javascript';
(function A(){
    this.name += 'this';
    return function(){
        console.log(this.name);//輸出 javascriptthis
    }
})()();
console.log(global.name);//輸出 javascriptthis
複製代碼

這個例子中函數A返回了一個匿名函數也能夠叫閉包,咱們發現this照樣綁定在了global上。這個例子是想告訴讀者,默認綁定和做用域層級沒有關係,只要是在做用域內this找不到認領的實例對象,那就會啓用默認綁定。

由此,你是否是能夠知道開篇的例子中 6,7,8,16行的輸出結果了?

20  //這裏是後置自增運算,因此先輸出後加一
複製代碼

隱式綁定

隱式綁定顧名思義沒有顯式的代表this的指向,可是已經綁定的某個實例對象上去了。舉個簡單的例子,這個用法實際上是咱們最經常使用的:

global.name = 'javascript' ;
let obj = {
    name : 'obj',
    A    : function(){
        this.name += 'this';
        console.log(this.name); 
    }
}
obj.A();//輸出 objthis
console.log(global.name);//輸出 javascript
複製代碼

這個例子中函數A的做用域內,this總算是有對象認領了,這個對象就是obj,因此this.name指向的就是obj中的name ,這種狀況就叫作隱式綁定

隱式綁定雖然是咱們最經常使用的,也是相對好理解的一種綁定方式,可是確是四種綁定中最坑的一種,爲何呢?由於,這種狀況下this一不當心就會找不到認領她的對象了,咱們稱之爲"丟失"。而在"丟失"的狀況下,this的指向會啓用默認綁定。咱們看下面的例子;

global.name = 'javascript' ;
let obj = {
    name : 'obj',
    A    : function(){
        this.name += 'this';
        console.log(this.name)
    },
    B    : function(f){
        this.name += 'this';
        f();
    },
    C    : function(){
      setTimeout(function(){
          console.log(this.name);
      },1000);
    }
}
let a = obj.A;              // 1
a();                        // 2
obj.B(function(){           // 3
    console.log(this.name); // 4
});                         // 5
obj.C();                    // 6
console.log(global.name);   // 7
複製代碼

這裏列出了三種"丟失"的狀況:

  1. 1-2行中obj的A函數賦值給了a,而後調用a(),這時候函數的執行上下文發生了變化,至關因而全局做用域下的一個函數的執行,因此承接咱們上面所說,此時啓用了默認綁定
  2. 3-5行中給obj.B傳遞一個Function參數,而且在Bf()執行,這至關於一個B中的當即執行函數,此時在this所在做用域找不到認領的對象,因而啓用默認綁定
  3. 6行是最有意思的一行,爲何呢?由於這一行在Node環境瀏覽器環境下的結果是不同的,按照常理來講,回調函數中的this一樣會由於丟失而啓用默認綁定,在瀏覽器環境下確實如此。可是在node中事情好像沒那麼簡單,咱們先看看輸出的結果,在作分析
javascriptthis // 1-2行執行結果
javascriptthis // 3-5行執行結果
javascriptthis // 7行執行結果
undefined      // 6行執行結果
複製代碼

你會發現有一個值很扎眼,沒錯,就是undefined,那爲何setTimeout()回調中的this沒有啓用默認綁定呢?這裏根據這篇博客作了一個猜測 : NodeJS 回調函數中的this ,我建議你看一看這篇博客

亦如fs.open()回調同樣,setTimeout()函數會先初始化本身,那麼此時回調函數做用域上就是存在實例對象了,只是這個對象咱們看不到而已,因此此時this.name並未初始化,因此輸出undefined。爲此我作了一個實驗來證實,setTimeout()this指向不等於global

function A(){
    console.log(this === global);
}
A();  //輸出 true
setTimeout(function(){
    console.log(global === this);
},1000);  // 輸出 false
複製代碼

由此,咱們能夠知道,開篇例子中18,19行的輸出即是:

21 // 隱式綁定丟失
22 // 箭頭函數綁定上級做用域this指針,這個後面會講
30 //隱式綁定
複製代碼

硬綁定(顯式綁定)

接下來要講的是硬綁定 , 這個比較簡單,是經過JS的三個經常使用API來顯式的實現綁定特定做用域,這三個API爲

  • apply
  • call
  • bind

這三個API之間的關係非本篇關鍵,能夠自行了解,本篇以apply爲例

咱們知道JS函數的一大特色就是有 定義時上下文運行時上下文 以及 上下文可變 的概念,而apply就是幫助咱們改變函數運行時上下文的環境,這種經過API顯式指定某個函數執行上下文環境的綁定方式就是 硬綁定

咱們來看下面這個例子:

global.name = 'global';
function A(){
    console.log(this.name);
}
let obj = {
    name : 'obj'
}
A.apply(obj); //輸出 obj
A.apply(global); //輸出 global
複製代碼

對,你應該懂了,什麼叫硬綁定。就是無論函數你定義在哪裏,這樣使用了我這個API,你就能夠隨心所欲,綁定到任意做用域的對象上去,哪怕是global都不帶怕的,硬核API !!!

由此,你也能夠獲得開篇例子中20行輸出結果應該是:

31  //obj.name在此以前被加了一次1,因此這裏是31
複製代碼

new操做符綁定

最後一種綁定方式是new操做符綁定,這個也是JS中最經常使用的用法之一了,簡單來講就是經過new操做符來實例化一個對象的過程當中發生了this指針的綁定,這個過程是不可見的,是後臺幫你完成了這一綁定過程。具體是什麼過程呢?這裏咱們就已開篇的例子爲例吧

function A(){
    console.log(this.num++);
}
new A(); //輸出爲 NaN
複製代碼

NaN是JSNumber對象上的一個靜態屬性,意如其名"not a number",表示不是數字。這裏new A()實例化了一個對象,此時在A的做用域裏就用對象認領this指針了,因此此時this指向實例化對象,可是這個對象中num屬性並無初始化,所以是undefined,而undefined非數字卻使用了++運算,所以最終輸出了NaN

四種綁定的優先級

既然this的綁定有四種機制,那一定會出現機制衝突的狀況,不要緊,其實從上面的講解中你應該已經能隱約感受到這四種機制是有優先級存在的。好比,在new操做符綁定的時候,就是由於new綁定優先級高於默認綁定,因此this指針指向的是新實例化的對象而不是全局對象global。這裏給出這四種綁定的優先級 :

 new 操做符綁定   >    硬綁定   >    隱式綁定   >    默認綁定

這個關係仍是挺明顯的,故不做例子闡述了。

ES6中引入箭頭函數對this的綁定產生了什麼影響?


快要結束了,再堅持一下,最後有必要說明如下ES6中的箭頭函數對於this指針綁定的影響,ES6中引入箭頭函數是爲了更優雅的書寫函數,對於那些簡單的函數咱們使用箭頭函數代替原來的函數寫法能夠大大簡化代碼量並且看上去更加整潔優雅。也正是由於箭頭函數的設計是爲了簡潔優雅,因此箭頭函數除了簡化代碼表示之外,還簡化了函數的行爲。

  • 箭頭函數不能聲明的方式定義,只能經過函數表達式
  • 箭頭函數不能經過new來實例化對象
  • 也是由於上面的緣由,箭頭函數中並無本身的this指針,這不表明不能使用this,箭頭函數中的this是繼承自父級做用域上的this,也就是說箭頭函數中的this綁定的是父級做用域內this所指向的對象

舉個例子來說:

name = 'global';
this.name = 'this';
let obj = {
    name : 'obj',
    A    : function(){
        ()=>{
            console.log(this.name)
        }
    },
    B    :() => {
        console.log(this.name)
    }
}
obj.A(); //輸出 obj
obj.B(); //輸出 this 
複製代碼

這裏或許obj.B()輸出讓你疑惑,其實咱們開篇也講了,全局做用域下thisglobal風馬牛不相及,因此這裏對應到父級做用域中this對應的對象就是this自己或者export

由此,開篇示例中18行的輸出即可知曉了 :

21 // b() 所輸出
22 // (b())()所輸出
複製代碼

這裏有些繞,之因此最終this綁定到了global上,是分了兩步

  • 首先,由於是箭頭函數,因此this繼承父級this綁定到了obj
  • 由於隱式調用的"丟失",致使父級this默認綁定到了global

附上前面程序的輸出答案

undefined
undefined
false
20
21
22
30
31
NaN
23
複製代碼

總算是寫完了,寫做的過程,筆者收穫也很大,就好比Node中回調函數的this指向問題我也沒有想到,是經過實驗才印證Node中回調函數中this指向的是自身實例化的對象,這個工做一樣不可見,後臺完成了,就像new同樣。 但願讀者也能夠獲得收穫!

下面是個人微信公衆號,若是以爲本篇文章你收穫很大,能夠關注個人微信公衆號,我會同步文章,這樣能夠RSS訂閱方便閱讀,感謝支持!

相關文章
相關標籤/搜索