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事件處理程序
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>
<!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 級事件處理程序
1
2
3
4
5
|
var
btn = document.getElementById(
"myBtn"
);
btn.addEventListener(
"click"
,
function
(){
alert(
this
.id);
//"myBtn"
},
false
);
|
在舊版本的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。
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 始終等於currentTarget 的值(也就是添加事件處理程序的元素),而target則只包含事件的實際目標。如果直接將事件處理程序指定給了目標元素則 this、currentTarget 和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語言精粹》