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


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


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

  • 普通函數調用模式:默認情況下,如果函數是被直接調用的,如果在嚴格模式下,就綁定到undefined,否則綁定到全局對象。

  • 構造器調用模式:函數通過new操作符調用,this綁定的是新創建的對象。

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

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

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()的兩次調用都會在一秒鍾后顯示一個警告框。

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

  
  
  
          
  1. <!doctype html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title></title>
  6. <script type="text/javascript">
  7. function test(){
  8. this.innerHTML='5';
  9. var timer=setInterval(function(){
  10. if (this.innerHTML==1) {
  11. clearInterval(timer); }
  12. this.innerHTML--;
  13. },1000)
  14. }
  15. </script>
  16. </head>
  17. <body>
  18. <div onclick="test()">沐浴星光</div>
  19. </body>
  20. </html>

你覺得上面的代碼能滿足要求么?答案當然是NO,不然后面怎么編。前面我們說過會創建一個動態函數,在這個函數中調用了test(),也就是說test()函數是當做普通函數調用的,里面的this是指向window的並不是目標元素。進行下面的修改就好了,根據紅寶書所言,動態函數里有一個局部變量event,也就是事件對象,我們把事件對象的目標元素傳給test即可。
   
   
   
           
  1. <!doctype html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title></title>
  6. <script type="text/javascript">
  7. function test(target){
  8. target.innerHTML='5';
  9. var timer=setInterval(function(){
  10. if (target.innerHTML==1) {
  11. clearInterval(timer); }
  12. target.innerHTML--;
  13. },1000)
  14. }
  15. </script>
  16. </head>
  17. <body>
  18. <div onclick="test(event.target)">沐浴星光</div>
  19. </body>
  20. </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語言精粹》








免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM