讀古今文學網 > 編寫高質量代碼:改善JavaScript程序的188個建議 > 建議89:正確應用this >

建議89:正確應用this

this總是指向當前作用域對象,如果當前定義對象的作用域沒有發生變化,則this會指向當前對象。但是,this關鍵字的用法比較靈活,這也在一定程度上干擾了用戶對於它所代表對象的準確判定。另外,this關鍵字可以存在於任何位置,並不局限於對象的方法內,還可以被應用在全局域內、函數內,以及其他特殊上下文環境中。

(1)函數的引用和調用

函數的引用和調用分別表示不同的概念,雖然它們都無法改變函數的定義作用域,但是引用函數能夠改變函數的執行作用域,而調用函數是不會改變函數的執行作用域的。繼續上面的示例進行說明:


var o={

name:\"對像o\",

f:function{

return this;

}

}

o.o1={

name:\"對像o1\",

me:o.f//引用對像o的方法f

}


可以看到,函數中的this所代表的是當前執行域對像o1:


var who=o.o1.me;

alert(who.name);//字符串\"對像o1\",說明當前this代表對像o1


如果把對像o1的me屬性值改為函數調用:


o.o1={

name:\"對像o1\",

me:o.f//調用對像o的方法f

}


則會看到,函數中的this所代表的是定義函數時所在的作用域對像o:


var who=o.o1.me;

alert(who.name);//字符串\"對像o\",說明當前this代表對像o


(2)call和apply

call和apply方法可以直接改變被執行函數的作用域,使其作用域指向所傳遞的參數對象。因此,函數中包含的this關鍵字也指向參數對象。


function f{

//如果當前執行域對象的構造函數等於當前函數,則表示this為實例對像

if(this.constructor==arguments.callee)alert(\"this=實例對像\");

//如果當前執行域對像等於window,則表示this為Window對像

else if(this==window)alert(\"this=window對像\");

//如果當前執行域對像為其他對象,則表示this為其他對像

else alert(\"this==其他對像n this.constructor=\"+this.constructor);

}

f;//this指向Window對像

new f;//this指向實例對像

f.call(1);//this指向數值實例對像


在這個示例中,當直接調用函數f時,因為函數的執行作用域為全局域,所以this代表window。當使用new運算符調用函數時,將創建一個新的實例對象,函數的執行作用域為實例對像所在的上下文,因此,this就指向這個新創建的實例對象。

而在使用call方法執行函數f時,call會把函數f的作用域強制修改為參數對像所在的上下文。由於call方法的參數值為數字1,因此JavaScript解釋器會把數字1強制封裝為數值對象,此時this就會指向這個數值對象。

再看一個很有趣的用法。在下面這個示例中,call方法把函數f強制轉換為對像o的一個方法並執行,這樣函數f中的this就指代對像o,this.x的值就等於1,而this.y的值就等於2,結果就返回3。


function f{//函數f

alert(this.x+this.y);

}

var o={//對像直接量

x:1,

y:2

}

f.call(o);//執行函數f,返回值為3


(3)原型繼承

JavaScript通過原型模式實現類的延續和繼承,如果父類的成員中包含了this關鍵字,當子類繼承了父類的這些成員時,this的指向就會很迷惑人。

在一般情況下,子類繼承父類的方法後,this可能指向子類的實例對象,也可能指向子類的原型對象,而不是子類的實例對象。例如:


function Base{//基類

this.m=function{//基類的方法m

return\"Base\";

};

this.a=this.m;//基類的屬性a,調用當前作用域中的m方法

this.b=this.m;//基類的方法b,引用當前作用域中的m方法

this.c=function{//基類的方法c,以閉包結構調用當前作用域中的m方法

return this.m;

}

}

function F{//子類

this.m=function{

return\"F\"

}

}

F.prototype=new Base;//繼承基類

var f=new F;//實例化子類

alert(f.a);//字符串\"Base\",說明this.m中this指向F的原型對像

alert(f.b);//字符串\"Base\"

alert(f.c);//字符串\"F\",說明this.m中this指向F的實例對像


在上面的示例中,基類Base包含4個成員,其中成員b和c以不同方式引用當前作用域內的方法m,而成員a存儲著當前作用域內的方法m的調用值。在將這些成員繼承給子類F後,其中m、b和c成為原型對象的方法,而a成為原型對象的屬性。但c的值為一個閉包體,當在子類的實例中調用它時,實際上它的返回值已經成為實例對象的成員,也就是說,閉包體在哪裡被調用,其中包含的this就會指向哪裡。所以,可以看到f.c中的this指向實例對象,而不是F類的原型對象。

為了避免因繼承關係而影響父類中this所代表的對象,除了通過上面介紹的方法把函數的引用傳遞給父類的成員外,還可以為父類定義私有函數,然後再把它的引用傳遞給其他父類成員,這樣就避免了由於函數閉包的原因而改變this的值。例如:


function Base{

var_m=function{//定義基類的私有函數_m

return\"Base\";

};

this.a=_m;

this.b=_m;

}


這樣基類的私有函數_m就具有完全隱私性,外界其他任何對象都無法直接訪問基類的私有函數_m。因此,在一般情況下,在定義方法時,對於相互依賴的方法,可以把它定義為私有函數,並且以引用函數的方式對外公開,這樣就避免了外界對於依賴方法的影響。

(4)異步調用

異步調用就是通過事件機制或計時器來延遲或調整函數的調用時間和時機。因為調用函數的執行作用域不再是原來的定義作用域,所以函數中的this總是指向引發該事件的對象。例如:


<input type=\"button\"/>

<script language=\"javascript\"type=\"text/javascript\">

var button=document.getElementsByTagName(\"input\")[0];

var o={};

o.f=function{

if(this==o)alert(\"this=o\");

if(this==window)alert(\"this=window\");

if(this==button)alert(\"this=button\");

}

button.onclick=o.f;

</script>


根據上面的講解可知,這裡的方法f所包含的this不再指向對像o,而是指向按鈕button,因為它是被傳遞給按鈕的事件處理函數之後再被調用的。由於函數的執行作用域發生了變化,所以不再指向定義方法時所指定的對象。

如果使用DOM 2級標準為按鈕註冊事件處理函數:


if(window.attachEvent){//兼容IE

button.attachEvent(\"onclick\",o.f);

}

else{//兼容符合DOM標準的瀏覽器

button.addEventListener(\"click\",o.f,true);

}


則會看到,在IE中,this指向window和button,而在符合DOM標準的瀏覽器中僅指向button。在IE中,attachEvent是Window對象的方法,在調用該方法時,執行作用域為全局作用域,所以this會指向window。同時由於該方法被註冊到按鈕對像上,因此它的真正執行作用域應該為button對像所在的上下文。這一點可以在符合DOM標準的瀏覽器中看到。這種解釋可能很勉強,但在IE中this同時指向Window和Button對像本身。

為了解決這個問題,可以借助call或apply方法強制在對像o上執行f方法,也就是說,強制改變f方法的執行作用域,避免因為環境的不同而影響函數作用域的變化。代碼如下:


if(window.attachEvent){

button.attachEvent(\"onclick\",function{//以閉包的形式封裝call方法來強制執行f

o.f.call(o);

});

}

else{

button.addEventListener(\"click\",function{

o.f.call(o);

},true);

}


這樣,當再次預覽時,其中包含的this關鍵字始終指向對像o,也就是說,f方法的執行作用域始終與它的定義作用域保持一致。

(5)定時器

異步調用的另一種形式就是使用定時器來調用函數。定時器就是通過調用Window對象的setTimeout或setInterval方法來延期調用函數。例如,可以這樣來設計延期調用方法o.f。


var o={};

o.f=function{

if(this==o)alert(\"this=o\");

if(this==window)alert(\"this=window\");

if(this==button)alert(\"this=button\");

}

setTimeout(o.f,100);


經測試發現,在IE中,this指向Window和Button對象,具體原因與上面講解的attachEvent方法相同。但是,在符合DOM標準的瀏覽器中,this指向Window對象,而不是Button對象,因為setTimeout方法是在全局作用域中被執行的,所以this自然指向Window對象。要解決這個問題,仍然可以使用call或apply方法來實現:


setTimeout(function{

o.f.call(o);

},100);