Javascript小學生都知道了javascript中的函數調用時會 隱性的接收兩個附加的參數:this和arguments。參數this在javascript編程中占據中非常重要的地位,它的值取決於調用的模式。總的來說Javascript中函數一共有4中調用模式:方法調用模式、普通函數調用模式、 ...
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);
|
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)" >
|
- (2)DOM0 級事件處理程式
使用DOM0級方法指定的事件處理程式被認為是元素的方法。因此,這時候的事件處理程式是在元素的作用域中運行;換句話說,程式中的 this 引用當前元素。來看一個例子。
1 2 3 4 |
var btn = document.getElementById( "myBtn" );
btn.onclick = function (){
alert( this .id); //"myBtn"
};
|
-
(3)DOM2 級事件處理程式
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
關於事件處理程式理解上面這些還不夠,我們還要關註一下事件委托,對“事件處理程式過多”問題的解決方案就是事件委托。事件委托利用了事件冒泡,只指定一個事件處理程式,就可以管理某一類型的所有事件。例如, click 事件會一直冒泡到 document 層次。也就是說,我們可以為整個頁面指定一個 onclick 事件處理程式,而不必給每個可單擊的元素分別添加事件處理程式。以下麵的 HTML 代碼為例。解析:第一次輸出很好理解,func()作為對象的方法調用,所以輸出division element,點擊添加的元素時,this其實已經指向this.m_Element,也就是事件的目標元素(事件對象的currentTarget屬性值-或者說是註冊事件處理程式的元素),因為是this.m_Element調用的addEventListener函數,所以內部的this全指向它了,而這個元素並沒有m_Text屬性,所以輸出undefined。
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>
|
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"
});
|
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