|
var testvar = 'window屬性';
var o1 = {testvar:'1', fun:function(){alert('o1: '+this.testvar);}};
var o2 = {testvar:'2', fun:function(){alert('o2: '+this.testvar);}};
o1.fun(); // '1'
o2.fun(); // '2'
o1.fun.call(o2); //'2'三次alert結(jié)果并不相同,很有趣不是么?其實(shí),所有的有趣、詭異的概念最后都可以歸結(jié)到一個(gè)問(wèn)題上,那就是尋址。
簡(jiǎn)單變量的尋址
JS是靜態(tài)還是動(dòng)態(tài)作用域?
告訴你一個(gè)很不幸的消息,JS是靜態(tài)作用域的,或者說(shuō),變量尋址比perl之類的動(dòng)態(tài)作用域語(yǔ)言要復(fù)雜得多。下面的代碼是程序設(shè)計(jì)語(yǔ)言原理上面的例子:
01| function big(){
02| var x = 1;
03| eval('f1 = function(){echo(x)}');
04| function f2(){var x = 2;f1()};
05| f2();
06| };
07| big();
輸出的是1,和pascal、ada如出一轍,雖然f1是用eval動(dòng)態(tài)定義的。另外一個(gè)例子同樣來(lái)自程序設(shè)計(jì)語(yǔ)言原理:
function big2(){
var x = 1;
function f2(){echo(x)}; //用x的值產(chǎn)生一個(gè)輸出
function f3(){var x = 3;f4(f2)};
function f4(f){var x = 4;f()};
f3();
}
big2();//輸出1:深綁定;輸出4:淺綁定;輸出3:特別綁定
輸出的還是1,說(shuō)明JS不僅是靜態(tài)作用域,還是深綁定,這下事情出大了……
ARI的概念
為了解釋函數(shù)(尤其是允許函數(shù)嵌套的語(yǔ)言中,比如Ada)運(yùn)行時(shí)復(fù)雜的尋址問(wèn)題,《程序設(shè)計(jì)語(yǔ)言原理》一書中定義了“ARI”:它是堆棧上一些記錄,包括:
函數(shù)地址
局部變量
返回地址
動(dòng)態(tài)鏈接
靜態(tài)鏈接
這里,動(dòng)態(tài)鏈接永遠(yuǎn)指向某個(gè)函數(shù)的調(diào)用者(如b執(zhí)行時(shí)調(diào)用a,則a的ARI中,動(dòng)態(tài)鏈接指向b);靜態(tài)鏈接則描述了a定義時(shí)的父元素,因?yàn)楹瘮?shù)的組織是有根樹,所以所有的靜態(tài)鏈接匯總后一定會(huì)指向宿主(如window),我們可以看例子(注釋后為輸出):
var x = 'x in host';
function a(){echo(x)};
function b(){var x = 'x inside b';echo(x)};
function c(){var x = 'x inside c';a()};
function d(){
var x = 'x inside d,a closure-made function';
return function(){echo(x)}};
a();// x in host
b();// x inside b
c();// x in host
d()();// x inside d,a closure-made function在第一句調(diào)用時(shí),我們可以視作“堆?!鄙嫌邢旅娴膬?nèi)容(左邊為棧頂):
[a的ARI] → [宿主]A的靜態(tài)鏈直直的戳向宿主,因?yàn)閍中沒(méi)有定義x,解釋器尋找x的時(shí)候,就沿著靜態(tài)鏈在宿主中找到了x;對(duì)b的調(diào)用,因?yàn)閎的局部變量里記錄了x,所以最后echo的是b里面的x:'x inside b';
現(xiàn)在,c的狀況有趣多了,調(diào)用c時(shí),可以這樣寫出堆棧信息:
動(dòng)態(tài)鏈:[a]→[c]→[宿主]
靜態(tài)鏈:[c]→[宿主];[a]→[宿主]
因?yàn)閷?duì)x的尋址在調(diào)用a后才進(jìn)行,所以,靜態(tài)鏈接還是直直的戳向宿主,自然x還是'x in host'咯!
d的狀況就更加有趣了,d創(chuàng)建了一個(gè)函數(shù)作為返回值,而它緊接著就被調(diào)用了~因?yàn)閐的返回值是在d的生命周期內(nèi)創(chuàng)建的,所以d返回值的靜態(tài)鏈接戳向d,所以調(diào)用的時(shí)候,輸出d中的x:'x inside d,a closure-made function'。
靜態(tài)鏈接的創(chuàng)建時(shí)機(jī)
月影和amingoo說(shuō)過(guò),“閉包”是函數(shù)的“調(diào)用時(shí)引用”,《程序設(shè)計(jì)語(yǔ)言原理》上面干脆直接叫ARI,不過(guò)有些不同的是,《程序設(shè)計(jì)語(yǔ)言原理》里面的ARI保存在堆棧中,而且函數(shù)的生命周期一旦結(jié)束,ARI就跟著銷毀;而JS的閉包卻不是這樣,閉包被銷毀,當(dāng)且僅當(dāng)沒(méi)有指向它和它的成員的引用(或者說(shuō),任何代碼都無(wú)法找到它)。我們可以簡(jiǎn)單地認(rèn)為函數(shù)ARI就是一個(gè)對(duì)象,只不過(guò)披上了函數(shù)的“衣服”而已。
《程序設(shè)計(jì)語(yǔ)言原理》描述的靜態(tài)鏈?zhǔn)钦{(diào)用時(shí)創(chuàng)建的,不過(guò),靜態(tài)鏈的關(guān)系卻是在代碼編譯的時(shí)候就確定了。比如,下面的代碼:
PROCEDURE a;
PROCEDURE b;
END
PEOCEDURE c;
END
END
中,b和c的靜態(tài)鏈戳向a。如果調(diào)用b,而b中某個(gè)變量又不在b的局部變量中時(shí),編譯器就生成一段代碼,它希望沿著靜態(tài)鏈向上搜堆棧,直到搜到變量或者RTE。
和ada之類的編譯型語(yǔ)言不同的是,JS是全解釋性語(yǔ)言,而且函數(shù)可以動(dòng)態(tài)創(chuàng)建,這就出現(xiàn)了“靜態(tài)鏈維護(hù)”的難題。好在,JS的函數(shù)不能直接修改,它就像erl里面的符號(hào)一樣,更改等于重定義。所以,靜態(tài)鏈也就只需要在每次定義的時(shí)候更新一下。無(wú)論定義的方式是function(){}還是eval賦值,函數(shù)創(chuàng)建后,靜態(tài)鏈就固定了。
我們回到big的例子,當(dāng)解釋器運(yùn)行到“function big(){......}”時(shí),它在內(nèi)存中創(chuàng)建了一個(gè)函數(shù)實(shí)例,并連接靜態(tài)鏈接到宿主。但是,在最后一行調(diào)用的時(shí)候,解釋器在內(nèi)存中畫出一塊區(qū)域,作為ARI。我們不妨成為ARI[big]。執(zhí)行指針移動(dòng)到第2行。
執(zhí)行到第3行時(shí),解釋器創(chuàng)建了“f1”實(shí)例,保存在ARI[big]中,連接靜態(tài)鏈到ARI[big]。下一行。解釋器創(chuàng)建“f2”實(shí)例,連接靜態(tài)鏈。接著,到了第5行,調(diào)用f2,創(chuàng)建ARI[f1];f2調(diào)用f1,創(chuàng)建ARI[f1];f1要輸出x,就需要對(duì)x尋址。
簡(jiǎn)單變量的尋址
我們繼續(xù),現(xiàn)在要對(duì)x尋址,但x并不出現(xiàn)在f1的局部變量中,于是,解釋器必須要沿著堆棧向上搜索去找x,從輸出看,解釋器并不是沿著“堆?!币粚右粚诱?,而是有跳躍的,因?yàn)榇藭r(shí)“堆棧”為:
|f1 | ←線程指針
|f2 | x = 2
|big | x = 1
|HOST|
如果解釋器真的沿著堆棧一層一層找的話,輸出的就是2了。這就觸及到Js變量尋址的本質(zhì):沿著靜態(tài)鏈上搜。
繼續(xù)上面的問(wèn)題,執(zhí)行指針沿著f1的靜態(tài)鏈上搜,找到big,恰好big里面有x=1,于是輸出1,萬(wàn)事大吉。
那么,靜態(tài)鏈?zhǔn)欠駮?huì)接成環(huán),造成尋址“死循環(huán)”呢?大可不用擔(dān)心,因?yàn)檫€記得函數(shù)是相互嵌套的么?換言之,函數(shù)組成的是有根樹,所有的靜態(tài)鏈指針最后一定能匯總到宿主,因此,擔(dān)心“指針成環(huán)”是很荒謬的。(反而動(dòng)態(tài)作用域語(yǔ)言尋址容易造成死循環(huán)。)
現(xiàn)在,我們可以總結(jié)一下簡(jiǎn)單變量尋址的方法:解釋器現(xiàn)在當(dāng)前函數(shù)的局部變量中尋找變量名,如果沒(méi)有找到,就沿著靜態(tài)鏈上溯,直到找到或者上溯到宿主仍然沒(méi)有找到變量為止。
ARI的生命
現(xiàn)在來(lái)正視一下ARI,ARI記錄了函數(shù)執(zhí)行時(shí)的局部變量(包括參數(shù))、this指針、動(dòng)態(tài)鏈和最重要的――函數(shù)實(shí)例的地址。我們可以假想一下,ARI有下面的結(jié)構(gòu):
ARI :: {
variables :: *variableTable, //變量表
dynamicLink :: *ARI, //動(dòng)態(tài)鏈接
instance :: *funtioninst //函數(shù)實(shí)例
}
variables包括所有局部變量、參數(shù)和this指針;dynamicLink指向ARI被它的調(diào)用者;instance指向函數(shù)實(shí)例。在函數(shù)實(shí)例中,有:
functioninst :: {
source :: *jsOperations, //函數(shù)指令
staticLink :: *ARI, //靜態(tài)鏈接
......
}
當(dāng)函數(shù)被調(diào)用時(shí),實(shí)際上執(zhí)行了如下的“形式代碼”:
*ARI p;
p = new ARI();
p->dynamicLink = thread.currentARI;
p->instance = 被調(diào)用的函數(shù)
p->variables.insert(參數(shù)表,this引用)
thread.transfer(p->instance->operations[0])
看見(jiàn)了么?創(chuàng)建ARI,向變量表壓入?yún)?shù)和this,之后轉(zhuǎn)移線程指針到函數(shù)實(shí)例的第一個(gè)指令。
函數(shù)創(chuàng)建的時(shí)候呢?在函數(shù)指令賦值之后,還要:
newFunction->staticLink = thread.currentARI;
現(xiàn)在問(wèn)題清楚了,我們?cè)诤瘮?shù)定義時(shí)創(chuàng)建了靜態(tài)鏈接,它直接戳向線程的當(dāng)前ARI。這樣就可以解釋幾乎所有的簡(jiǎn)單變量尋址問(wèn)題了。比如,下面的代碼:
function test(){
for(i=0;i<5;i++){
(function(t){ //這個(gè)匿名函數(shù)姑且叫做f
setTimeout(function(){echo(''+t)},1000) //這里的匿名函數(shù)叫做g
})(i)
}
}
test()
這段代碼的效果是延遲1秒后按照0 1 2 3 4的順序輸出。我們著重看setTimeout作用的那個(gè)函數(shù),在它創(chuàng)建時(shí),靜態(tài)鏈接指向匿名函數(shù)f,f的(某個(gè)ARI的)變量表中含有i(參數(shù)視作局部變量),所以,setTimeout到時(shí)時(shí),匿名函數(shù)g搜索變量t,它在匿名函數(shù)f的ARI里面找到了。于是,按照創(chuàng)建時(shí)的順序逐個(gè)輸出0 1 2 3 4。
公用匿名函數(shù)f的函數(shù)實(shí)例的ARI一共有5個(gè)(還記得函數(shù)每調(diào)用一次,ARI創(chuàng)建一次么?),相應(yīng)的,g也“創(chuàng)建”了5次。在第一個(gè)setTimeout到時(shí)之前,堆棧中相當(dāng)于有下面的記錄(我把g分開寫成5個(gè)):
+test的ARI [循環(huán)結(jié)束時(shí)i=5]
| f的ARI;t=0 ←――――――g0的靜態(tài)鏈接
| f的aRI ;t=1 ←――――――g1的靜態(tài)鏈接
| f的aRI ;t=2 ←――――――g2的靜態(tài)鏈接
| f的aRI ;t=3 ←――――――g3的靜態(tài)鏈接
| f的aRI ;t=4 ←――――――g4的靜態(tài)鏈接
/------
而,g0調(diào)用的時(shí)候,“堆?!笔窍旅娴臉幼樱?
+test的ARI [循環(huán)結(jié)束時(shí)i=5]
| f的ARI ;t=0 ←――――――g0的靜態(tài)鏈接
| f的ARI ;t=1 ←――――――g1的靜態(tài)鏈接
| f的ARI ;t=2 ←――――――g2的靜態(tài)鏈接
| f的ARI ;t=3 ←――――――g3的靜態(tài)鏈接
| f的ARI ;t=4 ←――――――g4的靜態(tài)鏈接
/------
+g0的ARI
| 這里要對(duì)t尋址,于是……t=0
/------
g0的ARI可能并不在f系列的ARI中,可以視作直接放在宿主里面;但尋址所關(guān)心的靜態(tài)鏈接卻仍然戳向各個(gè)f的ARI,自然不會(huì)出錯(cuò)咯~因?yàn)閟etTimeout是順序壓入等待隊(duì)列的,所以最后按照0 1 2 3 4的順序依次輸出。
函數(shù)重定義時(shí)會(huì)修改靜態(tài)鏈接嗎?
現(xiàn)在看下一個(gè)問(wèn)題:函數(shù)定義的時(shí)候會(huì)建立靜態(tài)鏈接,那么,函數(shù)重定義的時(shí)候會(huì)建立另一個(gè)靜態(tài)鏈接么?先看例子:
var x = "x in host";
f = function(){echo(x)};
f();
function big(){
var x = 'x in big';
f();
f = function(){echo (x)};
f()
}
big()
輸出:
x in host
x in host
x in big
這個(gè)例子也許還比較好理解,big運(yùn)行的時(shí)候重定義了宿主中的f,“新”f的靜態(tài)鏈接指向big,所以最后一行輸出'x in big'。
但是,下面的例子就有趣多了:
var x = "x in host";
f = function(){echo(x)};
f();
function big(){
var x = 'x in big';
f();
var f1 = f;
f1();
f = f;
f()
}
big()
輸出:
x in host
x in host
x in host
x in host
不是說(shuō)重定義就會(huì)修改靜態(tài)鏈接么?但是,這里兩個(gè)賦值只是賦值,只修改了f1和f的指針(還記得JS的函數(shù)是引用類型了么?),f真正的實(shí)例中,靜態(tài)鏈接沒(méi)有改變!。所以,四個(gè)輸出實(shí)際上都是宿主中的x。
結(jié)構(gòu)(對(duì)象)中的成分(屬性)尋址問(wèn)題
請(qǐng)基督教(Java)派和摩門教(csh)派的人原諒我用這個(gè)奇怪的稱呼,不過(guò)JS的對(duì)象太像Hash表了,我們考慮這個(gè)尋址問(wèn)題:
a.b編譯型語(yǔ)言會(huì)生成找到a后向后偏移一段距離找b的代碼,但,JS是全動(dòng)態(tài)語(yǔ)言,對(duì)象的成員可以隨意增減,還有原型的問(wèn)題,讓JS對(duì)象成員的尋址顯得十分有趣。
對(duì)象就是哈希表
除開幾個(gè)特殊的方法(和原型成員)之外,對(duì)象簡(jiǎn)直和哈希表沒(méi)有區(qū)別,因?yàn)榉椒ê蛯傩远伎梢?a href=/pingce/cunchu/ target=_blank class=infotextkey>存儲(chǔ)在“哈希表”的“格子”里面。月版在他的《JS王者歸來(lái)》里面就實(shí)現(xiàn)了一個(gè)HashTable類。
對(duì)象本身的屬性尋址
“本身的”屬性說(shuō)的是hasOwnProperty為真的那些屬性。從實(shí)現(xiàn)的角度看,就是對(duì)象自己的“哈希表”里面擁有的成員。比如:
function Point(x,y){
this.x = x;
this.y = y;
}
var a = new Point(1,2);
echo("a.x:"+a.x)
Point構(gòu)造器創(chuàng)建了“Point”對(duì)象a,并且設(shè)置了x和y屬性;于是,a的成員表里面,就有:
| x | ---> 1
| y | ---> 2
搜索a.x時(shí),解釋器先找到a,然后在a的成員表里面搜索x,得到1。
從構(gòu)造器給對(duì)象設(shè)置方法不是好策略,因?yàn)樗鼤?huì)造成兩個(gè)同類的對(duì)象方法不等:
function Point(x,y){
this.x = x;
this.y = y;
this.abs = function(){return Math.sqrt(this.x*this.x+this.y*this.y)}
}
var a = new Point(1,2);
var b = new Point(1,2);
echo("a.abs == b.abs ? "+(a.abs==b.abs));
echo("a.abs === b.abs ? "+(a.abs===b.abs));
兩個(gè)輸出都是false,因?yàn)榈谒男兄?,?duì)象的abs成員(方法)每次都創(chuàng)建了一個(gè),于是,a.abs和b.abs實(shí)際上指向兩個(gè)完全不同的函數(shù)實(shí)例。因此,兩個(gè)看來(lái)相等的方法實(shí)際上不等。
扯上原型的尋址問(wèn)題
原型是函數(shù)(類)的屬性,它指向某個(gè)對(duì)象(不是類)?!霸汀彼枷肟梢灶惐取罢肇埉嫽ⅰ保侯悺盎ⅰ焙皖悺柏垺睕](méi)有那個(gè)繼承那個(gè)的關(guān)系,只有“虎”像“貓”的關(guān)系。原型著眼于相似性,在js中,代碼估計(jì)可以寫作:
Tiger.prototype = new Cat()函數(shù)的原型也可以只是空白對(duì)象:
SomeClass.prototype = {}我們回到尋址上來(lái),假設(shè)用.來(lái)獲取某個(gè)屬性,它偏偏是原型里面的屬性怎么辦?現(xiàn)象是:它的確取到了,但是,這是怎么取到的?如果對(duì)象本身的屬性和原型屬性重名怎么辦?還好,對(duì)象本身的屬性優(yōu)先。
把方法定義在原型里面是很好的設(shè)計(jì)策略。假如我們改一下上面的例子:
function Point(x,y){
this.x = x;
this.y = y;
}
Point.prototype.abs = function(){return Math.sqrt(this.x*this.x+this.y*this,y)}
var a = new Point(1,2);
var b = new Point(1,2);
echo("a.abs == b.abs ? "+(a.abs==b.abs));
echo("a.abs === b.abs ? "+(a.abs===b.abs));
這下,輸出終于相等了,究其原因,因?yàn)閍.abs和b.abs指向的是Point類原型的成員abs,所以輸出相等。不過(guò),我們不能直接訪問(wèn)Point.prototype.abs,測(cè)試的時(shí)候直接出錯(cuò)。更正:經(jīng)過(guò)重新測(cè)試,“Point.prototype.abs不能訪問(wèn)”的問(wèn)題是我采用的JSCOnsole的問(wèn)題。回復(fù)是對(duì)的,感謝您的指正!
原型鏈可以很長(zhǎng)很長(zhǎng),甚至可以繞成環(huán)??紤]下面的代碼:
A = function(x){this.x = x};
B = function(x){this.y = x};
A.prototype = new B(1);
B.prototype = new A(1);
var a = new A(2);
echo(a.x+' , '+a.y);
var b = new B(2);
echo(b.x+' , '+b.y);
這描述的關(guān)系大概就是“我就像你,你也像我”。原型指針對(duì)指造成了下面的輸出:
2 , 1
1 , 2
搜索a.y的時(shí)候,沿著原型鏈找到了“a.prototype”,輸出1;b.x也是一樣的原理?,F(xiàn)在,我們要輸出“a.z”這個(gè)沒(méi)有注冊(cè)的屬性:
echo(tyoeof a.z)我們很詫異,這里并沒(méi)有死循環(huán),看來(lái)解釋器有一個(gè)機(jī)制來(lái)處理原型鏈成環(huán)的問(wèn)題。同時(shí),原型要么結(jié)成樹,要么就成單環(huán),不會(huì)有多環(huán)結(jié)構(gòu),這是很簡(jiǎn)單的圖論。
this:函數(shù)中的潛規(guī)則
方法(函數(shù))調(diào)用中最令人煩惱的潛規(guī)則就是this問(wèn)題。從道理上講,this是一個(gè)指針,戳向調(diào)用者(某個(gè)對(duì)象)。但假如this永遠(yuǎn)指向調(diào)用者的話,世界就太美好了。但這個(gè)可惡的指針時(shí)不時(shí)的“踢你的狗”??赡苄薷牡那闆r包括call、apply、異步調(diào)用和“window.eval”。
我更愿意把this當(dāng)做一個(gè)參數(shù),就像lua里面的self一樣。lua的self可以顯式傳遞,也可以用冒號(hào)來(lái)調(diào)用:
a:f(x,y,z) === a.f(a,x,y,z)JS中“素”的方法調(diào)用也是這個(gè)樣子:
a.f(x,y,z) === a.f.call(a,x,y,z)f.call才是真正“干凈”的調(diào)用形式,這就如同lua中干凈的調(diào)用一般。很多人都說(shuō)lua是js的清晰版,lua簡(jiǎn)化了js的很多東西,曝光了js許多的潛規(guī)則,著實(shí)不假。
修正“this”的原理
《王者歸來(lái)》上面提到的“用閉包修正this”,先看代碼:
button1.onclick = (
function(e){return function(){button_click.apply(e,arguments)}}
)(button1)別小看了這一行代碼,其實(shí)它創(chuàng)建了一個(gè)ARI,將button1綁定于此,然后返回一個(gè)函數(shù),函數(shù)強(qiáng)制以e為調(diào)用者(主語(yǔ))調(diào)用button_click,所以,傳到button_click里的this就是e,也就是button1咯!事件綁定結(jié)束后,環(huán)境大概是下面的樣子:
button1.onclick = _F_; //給返回的匿名函數(shù)設(shè)置一個(gè)名字
_F_.staticLink = _ARI_; //創(chuàng)建之后就調(diào)用的匿名函數(shù)的ARI
_ARI_[e] = button1 //匿名ARI參數(shù)表里面的e,同時(shí)也是_F_尋找的那個(gè)e
于是,我們單擊button,就會(huì)調(diào)用_F_,_F_發(fā)起了一個(gè)調(diào)用者是e的button_click函數(shù),根據(jù)我們前面的分析,e等于button1,所以我們得到了一個(gè)保險(xiǎn)的“指定調(diào)用者”方法?;蛟S我們還可以繼續(xù)發(fā)揮這個(gè)思路,做成通用接口:
bindFunction = function(f,e){ //我們是好人,不改原型,不改……
return function(){
f.apply(e,arguments)
}
}
JavaScript技術(shù):細(xì)品javascript 尋址,閉包,對(duì)象模型和相關(guān)問(wèn)題,轉(zhuǎn)載需保留來(lái)源!
鄭重聲明:本文版權(quán)歸原作者所有,轉(zhuǎn)載文章僅為傳播更多信息之目的,如作者信息標(biāo)記有誤,請(qǐng)第一時(shí)間聯(lián)系我們修改或刪除,多謝。