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