你不知道的this—JS異步編程中的this

  Javascript小學生都知道了javascript中的函數調用時會 隱性的接收兩個附加的參數:this和arguments。參數this在javascript編程中佔據中很是重要的地位,它的值取決於調用的模式。總的來講Javascript中函數一共有4中調用模式:方法調用模式、普通函數調用模式、構造器調用模式、apply/call調用模式。這些模式在如何初始化關鍵參數this上存在差別。「可能還有小夥伴不知道它們之間的區別,那我就勉爲其難擼一擼吧!」javascript


  • 方法調用模式:函數是在某個明確的上下文對象中調用的,this綁定的是那個上下文對象。
    html

  • 普通函數調用模式:默認狀況下,若是函數是被直接調用的,若是在嚴格模式下,就綁定到undefined,不然綁定到全局對象。java

  • 構造器調用模式:函數經過new操做符調用,this綁定的是新建立的對象。web

  • apply/call調用模式:函數經過apply或者call調用,this綁定的是指定的對象,若是把null或者undefined做爲this的綁定對象傳入call/apply,在調用時會被忽略,實際應用的是默認綁定規則。ajax

下面舉一個簡單的綜合例子:編程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var a=2;
function foo(b) {
     this .b=b;
     console.log( this .a);
}
 
var obj={
     a:4,
     foo:foo
};
 
foo(); //普通函數調用,輸出2
obj.foo(); //做爲對象方法調用,輸出4
foo.call(obj); //call顯示綁定,輸出4
foo.call( null ); //輸出2
 
var bar= new foo(8); //構造函數調用,輸出了undefined(由console.log(a)打印)
console.log(bar.b) //輸出8


  上面的例子在瀏覽器環境中已經測試經過了,在Node環境中在函數外面定義的變量不會成爲全局對象的屬性,理解這個例子的輸出結果對於上面提到的四種調用方式大概就理解了。在大多數狀況下,每次遇到函數調用(注意是每次,無論調用時這個函數位於哪裏,只要遇到調用這個函數就要停下來肯定裏面的this),只要仔細區分上面的四種調用模式,就能很快肯定函數中的this綁定的是哪一個對象。可是有一類狀況很特殊,你不能一眼或者兩眼就能看出函數調用的模式,那就是JavaScript中的異步函數調用。下面介紹幾種實際開發過程當中經常使用的異步函數調用中this綁定的例子。瀏覽器

1.超時調用和間歇調用

超時調用須要使用 window 對象的 setTimeout() 方法,它接受兩個參數:要執行的代碼和以毫秒表示的時間(即在執行代碼前須要等待多少毫秒)。其中,第一個參數能夠是一個包含JavaScript代碼的字符串(就和在eval() 函數中使用的字符串同樣),也能夠是一個函數。setTimeout() 的第二個參數告訴 JavaScript 再過多長時間把當前任務添加到隊列中。若是隊列是空的,那麼添加的代碼會當即執行;若是隊列不是空的,那麼它就要等前面的代碼執行完了之後再執行。
下面對setTimeout()的兩次調用都會在一秒鐘後顯示一個警告框。
app

1
2
3
setTimeout( function () {
alert( "Hello world!" );
}, 1000);
 下面看一下setTimeout的回調函數中存在this的狀況

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var a=5;
 
function foo() {
   this .a++;
     setTimeout( function (){
         console.log( this .++a);
     },1000);
}
 
var obj={
     a:2
};
foo.call(obj);
console.log(obj.a);

  在瀏覽器環境測試,上述代碼的輸出結果是3 6,爲何會是3 和6呢,首先咱們知道超時函數的回調函數是異步的,因此先輸出的是最後一條語句執行的結果。foo.call(obj)語句經過call綁定obj,因此foo函數執行時內部的this綁定的是obj,因此this.a++使得obj的a屬性增長了1.接下來經過超時函數設置回調的匿名函數一秒後加入到任務隊列。因此在執行最後一條語句時,超時函數裏的回調函數尚未執行,因此最後一條語句輸出爲3,接下來當任務隊列裏的回調函數被調用執行時,輸出的是6,也就是全局變量a加1,所以超時調用的回調代碼都是在全局做用域中執行的,函數中的this的值指向全局對象,這裏補充說明一下在嚴格模式下this綁定的是undefined。異步

那麼間歇調用setInterval方法是什麼狀況呢。稍微小改一下上面的代碼:函數

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var a=5;
 
function foo() {
   this .a++;
     setInterval( function (){
         console.log(++ this .a);
     },1000);
}
 
var obj={
     a:2
};
foo.call(obj);
console.log(obj.a);

上面的代碼輸出爲3 6 7 8 9·····

也就是說間歇調用和超時調用的狀況同樣,回調函數也是在全局環境中執行的。

2.事件處理程序

  • (1)HTML事件處理程序

 在事件處理函數內部, this 值等於事件的目標元素,例如:
1
2
<!-- 輸出  "Click Me" -->
< input type = "button" value = "Click Me" onclick = "alert(this.value)" >

因此你以爲你懂這個東東了,《JS高程》紅寶書中說,直接在HTML中添加事件處理會動態建立一個事件處理函數,執行這個函數時的this爲目標元素。那咱們看個例子,瞧瞧本身是否是真的懂了。

點擊一個div,讓div裏的文本從5每隔一秒遞減一直到0

  
  
  
  
<!doctype html><html lang="en"><head> <meta charset="UTF-8"> <title></title><script type="text/javascript"> function test(){ this.innerHTML='5'; var timer=setInterval(function(){ if (this.innerHTML==1) { clearInterval(timer); } this.innerHTML--; },1000) }</script></head><body><div onclick="test()">沐浴星光</div> </body></html>

你以爲上面的代碼能知足要求麼?答案固然是NO,否則後面怎麼編。前面咱們說過會建立一個動態函數,在這個函數中調用了test(),也就是說test()函數是當作普通函數調用的,裏面的this是指向window的並非目標元素。進行下面的修改就行了,根據紅寶書所言,動態函數裏有一個局部變量event,也就是事件對象,咱們把事件對象的目標元素傳給test便可。
   
   
   
   
<!doctype html><html lang="en"><head> <meta charset="UTF-8"> <title></title><script type="text/javascript"> function test(target){ target.innerHTML='5'; var timer=setInterval(function(){ if (target.innerHTML==1) { clearInterval(timer); } target.innerHTML--; },1000) }</script></head><body><div onclick="test(event.target)">沐浴星光</div> </body></html>
 
  • (2)DOM0 級事件處理程序

使用DOM0級方法指定的事件處理程序被認爲是元素的方法。所以,這時候的事件處理程序是在元素的做用域中運行;換句話說,程序中的 this 引用當前元素。來看一個例子。

1
2
3
4
var btn = document.getElementById( "myBtn" );
btn.onclick = function (){
alert( this .id); //"myBtn"
};
  • (3)DOM2 級事件處理程序

要在按鈕上爲 click 事件添加事件處理程序,可使用下列代碼:
1
2
3
4
5
var btn = document.getElementById( "myBtn" );
btn.addEventListener( "click" , function (){
alert( this .id); //"myBtn"
}, false );
DOM0級方法同樣,這裏添加的事件處理程序也是在其依附的元素的做用域中運行。
在舊版本的IE瀏覽器中有一種特殊狀況,舊版本的IE能夠經過attachEvent() 添加事件處理程序,IE中使用attachEvent() 與使用DOM0級方法的主要區別在於事件處理程序的做用域。在使DOM0級方法的狀況下,事件處理程序會在其所屬元素的做用域內運行;在使用 attachEvent() 法的狀況下,事件處理程序會在全局做用域中運行,所以 this 等於 window。來看下面的例子。

1
2
3
4
var btn = document.getElementById( "myBtn" );
btn.attachEvent( "onclick" , function (){
     alert( this === window); //true
});
 爲了加深理解,咱們看下面一個比較難懂的例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
functionJSClass(){
this .m_Text = 'division element' ;
this .m_Element = document.createElement( 'div' );
this .m_Element.innerHTML = this .m_Text;
this .m_Element.addEventListener( 'click' , this .func);
// this.m_Element.onclick = this.func;
}
JSClass.prototype.Render= function (){
     document.body.appendChild( this .m_Element);
}
JSClass.prototype.func = function (){
   alert( this .m_Text);
};
var jc =newJSClass();
jc.Render(); // add div
jc.func(); // 輸出 division element

click添加的div元素division element會輸出underfined,爲何? 
答案:division element undefined

解析:第一次輸出很好理解,func()做爲對象的方法調用,因此輸出division element,點擊添加的元素時,this其實已經指向this.m_Element,也就是事件的目標元素(事件對象的currentTarget屬性值-或者說是註冊事件處理程序的元素),由於是this.m_Element調用的addEventListener函數,因此內部的this全指向它了,而這個元素並無m_Text屬性,因此輸出undefined。

 關於事件處理程序理解上面這些還不夠,咱們還要關注一下事件委託對「事件處理程序過多」問題的解決方案就是事件委託。事件委託利用了事件冒泡,只指定一個事件處理程序,就能夠管理某一類型的全部事件。例如, click 事件會一直冒泡到 document 層次。也就是說,咱們能夠爲整個頁面指定一個 onclick 事件處理程序,而沒必要給每一個可單擊的元素分別添加事件處理程序。如下面的 HTML 代碼爲例。
1
2
3
4
5
<ul id= "myLinks" >
  <li id= "goSomewhere" >Go somewhere</li>
  <li id= "doSomething" >Do something</li>
  <li id= "sayHi" >Say hi</li>
</ul>
其中包含3個被單擊後會執行操做的列表項。按照傳統的作法,須要像下面這樣爲它們添加3個事件處理程序。
1
2
3
4
5
6
7
8
9
10
11
12
var item1 = document.getElementById( "goSomewhere" );
var item2 = document.getElementById( "doSomething" );
var item3 = document.getElementById( "sayHi" );
item1.addEventListener( "click" , function (event){
     alert( this .id); //"goSomewhere"
});
item2.addEventListener( "click" , function (event){
     alert( this .id); //"oSomething"
});
item3.addEventListener( "click" , function (event){
     alert( this .id); //"sayHi"
});
若是在一個複雜的 Web 應用程序中,對全部可單擊的元素都採用這種方式,那麼結果就會有數不清的代碼用於添加事件處理程序。此時,能夠利用事件委託技術解決這個問題。使用事件委託,只需在DOM樹中儘可能最高的層次上添加一個事件處理程序,以下面的例子所示。
1
2
3
4
var list=document.getElementById( '"myLinks' );
list.addEventListener( 'click' , function (event){
     alert( this .id);
})
那上面的例子可否實現事件委託前的功能呢,咱們用下面的代碼在瀏覽器中測試一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!DOCTYPE html>
< html >
< head >
     < meta charset = "utf-8" />
     < title ></ title >
     
</ head >
< body >
   < ul id = "myLinks" >
    < li id = "goSomewhere" >Go somewhere</ li >
    < li id = "doSomething" >Do something</ li >
    < li id = "sayHi" >Say hi</ li >
  </ ul >
 
</ body >
< script type = "text/javascript" >
     var list=document.getElementById('myLinks');
    list.addEventListener('click',function(event){
     alert(this.id);
   })
</ script >
</ html >

測試結果



也就是說不論點擊哪個列表,彈出的是父元素的ID,那麼該怎麼改寫才能實現預期的功能呢?咱們知道事件對象event有不少屬性,其中包括兩個屬性currentTarget和target,在事件處理程序內部,對象this 始終等於currentTarget 的值(也就是添加事件處理程序的元素),而target則只包含事件的實際目標。若是直接將事件處理程序指定給了目標元素則 thiscurrentTarget target包含相同的值。若是事件處理程序是被委託代理的,那麼這些值通常不一樣。來看下面的例子。

1
2
3
4
5
6
var list=document.getElementById( 'myLinks' );
  list.addEventListener( 'click' , function (event){
     alert(event.currentTarget===list) //ture
     alert( this ===list) //ture
     
  });

這也解釋了上面錯誤的事件委託爲何一直彈出「myLinks」了。正確的事件委託程序是:

1
2
3
4
var list=document.getElementById( 'myLinks' );
list.addEventListener( 'click' , function (event){
     alert(event.target.id);
})


3.Ajax請求中的this

最後簡要說明一下ajax請求中的this

1
2
3
4
5
6
7
8
9
10
11
12
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function (){
     if (xhr.readyState == 4){
         if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304){
             alert(xhr.responseText);
         } else {
             alert( "Request was unsuccessful: " + xhr.status);
         }
     }
};
xhr.open( "get" , "example.txt" , true );
xhr.send( null );

這個例子在onreadystatechange事件處理程序中使用了xhr對象,沒有使用this對象,緣由是onreadystatechange事件處理程序的做用域問題。若是使用this對象,在有的瀏覽器中會致使函數執行失敗,或者致使錯誤發生。所以,使用實際的XHR對象實例變量是較爲可靠的一種方式。


參考:

《JavaScript高級程序設計》

《You Don't Konw JS:This&Object Prototypes》

《JavaScript語言精粹》





相關文章
相關標籤/搜索