細品javascript 定址,閉包,物件模型和相關問題

來源:互聯網
上載者:User

正是因為JS是動態語言,所以JS的定址是現場定址,而非像C一樣,編譯後確定。此外,JS引入了this指標,這是一個很麻煩的東西,因為它“隱式”作為一個參數傳到函數裡面。我們先看“範圍鏈”話題中的例子:
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結果並不相同,很有趣不是嗎?其實,所有的有趣、詭異的概念最後都可以歸結到一個問題上,那就是定址。
簡單變數的定址
JS是靜態還是動態範圍?
告訴你一個很不幸的訊息,JS是靜態範圍的,或者說,變數定址比perl之類的動態範圍語言要複雜得多。下面的代碼是程式設計語言原理上面的例子:
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動態定義的。另外一個例子同樣來自程式設計語言原理:
function big2(){
var x = 1;
function f2(){echo(x)}; //用x的值產生一個輸出
function f3(){var x = 3;f4(f2)};
function f4(f){var x = 4;f()};
f3();
}
big2();//輸出1:深綁定;輸出4:淺綁定;輸出3:特別綁定
輸出的還是1,說明JS不僅是靜態範圍,還是深綁定,這下事情出大了……
ARI的概念
為瞭解釋函數(尤其是允許函數嵌套的語言中,比如Ada)運行時複雜的定址問題,《程式設計語言原理》一書中定義了“ARI”:它是堆棧上一些記錄,包括:
函數地址
局部變數
返回地址
動態連結
靜態連結
這裡,動態連結永遠指向某個函數的調用者(如b執行時調用a,則a的ARI中,動態連結指向b);靜態連結則描述了a定義時的父元素,因為函數的組織是有根樹,所以所有的靜態連結匯總後一定會指向宿主(如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在第一句調用時,我們可以視作“堆棧”上有下面的內容(左邊為棧頂):
[a的ARI] → [宿主]A的靜態鏈直直的戳向宿主,因為a中沒有定義x,解譯器尋找x的時候,就沿著靜態鏈在宿主中找到了x;對b的調用,因為b的局部變數裡記錄了x,所以最後echo的是b裡面的x:'x inside b';
現在,c的狀況有趣多了,調用c時,可以這樣寫出堆棧資訊:
動態鏈:[a]→[c]→[宿主]
靜態鏈:[c]→[宿主];[a]→[宿主]
因為對x的定址在調用a後才進行,所以,靜態連結還是直直的戳向宿主,自然x還是'x in host'咯!
d的狀況就更加有趣了,d建立了一個函數作為傳回值,而它緊接著就被調用了~因為d的傳回值是在d的生命週期內建立的,所以d傳回值的靜態連結戳向d,所以調用的時候,輸出d中的x:'x inside d,a closure-made function'。
靜態連結的建立時機
月影和amingoo說過,“閉包”是函數的“調用時引用”,《程式設計語言原理》上面乾脆直接叫ARI,不過有些不同的是,《程式設計語言原理》裡面的ARI儲存在堆棧中,而且函數的生命週期一旦結束,ARI就跟著銷毀;而JS的閉包卻不是這樣,閉包被銷毀,若且唯若沒有指向它和它的成員的引用(或者說,任何代碼都無法找到它)。我們可以簡單地認為函數ARI就是一個對象,只不過披上了函數的“衣服”而已。
《程式設計語言原理》描述的靜態鏈是調用時建立的,不過,靜態鏈的關係卻是在代碼編譯的時候就確定了。比如,下面的代碼:
PROCEDURE a;
PROCEDURE b;
END
PEOCEDURE c;
END
END
中,b和c的靜態鏈戳向a。如果調用b,而b中某個變數又不在b的局部變數中時,編譯器就產生一段代碼,它希望沿著靜態鏈向上搜堆棧,直到搜到變數或者RTE。
和ada之類的編譯型語言不同的是,JS是全解釋性語言,而且函數可以動態建立,這就出現了“靜態鏈維護”的難題。好在,JS的函數不能直接修改,它就像erl裡面的符號一樣,更改等於重定義。所以,靜態鏈也就只需要在每次定義的時候更新一下。無論定義的方式是function(){}還是eval賦值,函數建立後,靜態鏈就固定了。
我們回到big的例子,當解譯器運行到“function big(){......}”時,它在記憶體中建立了一個函數執行個體,並串連靜態連結到宿主。但是,在最後一行調用的時候,解譯器在記憶體中畫出一塊地區,作為ARI。我們不妨成為ARI[big]。執行指標移動到第2行。
執行到第3行時,解譯器建立了“f1”執行個體,儲存在ARI[big]中,串連靜態鏈到ARI[big]。下一行。解譯器建立“f2”執行個體,串連靜態鏈。接著,到了第5行,調用f2,建立ARI[f1];f2調用f1,建立ARI[f1];f1要輸出x,就需要對x定址。
簡單變數的定址
我們繼續,現在要對x定址,但x並不出現在f1的局部變數中,於是,解譯器必須要沿著堆棧向上搜尋去找x,從輸出看,解譯器並不是沿著“堆棧”一層一層找,而是有跳躍的,因為此時“堆棧”為:
|f1 | ←線程指標
|f2 | x = 2
|big | x = 1
|HOST|
如果解譯器真的沿著堆棧一層一層找的話,輸出的就是2了。這就觸及到Js變數定址的本質:沿著靜態鏈上搜。
繼續上面的問題,執行指標沿著f1的靜態鏈上搜,找到big,恰好big裡面有x=1,於是輸出1,萬事大吉。
那麼,靜態鏈是否會接成環,造成定址“死迴圈”呢?大可不用擔心,因為還記得函數是相互嵌套的嗎?換言之,函數組成的是有根樹,所有的靜態鏈指標最後一定能匯總到宿主,因此,擔心“指標成環”是很荒謬的。(反而動態範圍語言定址容易造成死迴圈。)
現在,我們可以總結一下簡單變數定址的方法:解譯器現在當前函數的局部變數中尋找變數名,如果沒有找到,就沿著靜態鏈上溯,直到找到或者上溯到宿主仍然沒有找到變數為止。
ARI的生命
現在來正視一下ARI,ARI記錄了函數執行時的局部變數(包括參數)、this指標、動態鏈和最重要的——函數執行個體的地址。我們可以假想一下,ARI有下面的結構:
ARI :: {
variables :: *variableTable, //變數表
dynamicLink :: *ARI, //動態連結
instance :: *funtioninst //函數執行個體
}
variables包括所有局部變數、參數和this指標;dynamicLink指向ARI被它的調用者;instance指向函數執行個體。在函數執行個體中,有:
functioninst :: {
source :: *jsOperations, //函數指令
staticLink :: *ARI, //靜態連結
......
}
當函數被調用時,實際上執行了如下的“形式代碼”:
*ARI p;
p = new ARI();
p->dynamicLink = thread.currentARI;
p->instance = 被調用的函數
p->variables.insert(參數表,this引用)
thread.transfer(p->instance->operations[0])
看見了嗎?建立ARI,向變數表壓入參數和this,之後轉移線程指標到函數執行個體的第一個指令。
函數建立的時候呢?在函數指令賦值之後,還要:
newFunction->staticLink = thread.currentARI;
現在問題清楚了,我們在函數定義時建立了靜態連結,它直接戳向線程的當前ARI。這樣就可以解釋幾乎所有的簡單變數定址問題了。比如,下面的代碼:
function test(){
for(i=0;i<5;i++){
(function(t){ //這個匿名函數姑且叫做f
setTimeout(function(){echo(''+t)},1000) //這裡的匿名函數叫做g
})(i)
}
}
test()
這段代碼的效果是延遲1秒後按照0 1 2 3 4的順序輸出。我們著重看setTimeout作用的那個函數,在它建立時,靜態連結指向匿名函數f,f的(某個ARI的)變數表中含有i(參數視作局部變數),所以,setTimeout到時時,匿名函數g搜尋變數t,它在匿名函數f的ARI裡面找到了。於是,按照建立時的順序逐個輸出0 1 2 3 4。
公用匿名函數f的函數執行個體的ARI一共有5個(還記得函數每調用一次,ARI建立一次嗎?),相應的,g也“建立”了5次。在第一個setTimeout到時之前,堆棧中相當於有下面的記錄(我把g分開寫成5個):
+test的ARI [迴圈結束時i=5]
| f的ARI;t=0 ←——————g0的靜態連結
| f的aRI ;t=1 ←——————g1的靜態連結
| f的aRI ;t=2 ←——————g2的靜態連結
| f的aRI ;t=3 ←——————g3的靜態連結
| f的aRI ;t=4 ←——————g4的靜態連結
\------
而,g0調用的時候,“堆棧”是下面的樣子:
+test的ARI [迴圈結束時i=5]
| f的ARI ;t=0 ←——————g0的靜態連結
| f的ARI ;t=1 ←——————g1的靜態連結
| f的ARI ;t=2 ←——————g2的靜態連結
| f的ARI ;t=3 ←——————g3的靜態連結
| f的ARI ;t=4 ←——————g4的靜態連結
\------
+g0的ARI
| 這裡要對t定址,於是……t=0
\------
g0的ARI可能並不在f系列的ARI中,可以視作直接放在宿主裡面;但定址所關心的靜態連結卻仍然戳向各個f的ARI,自然不會出錯咯~因為setTimeout是順序壓入等待隊列的,所以最後按照0 1 2 3 4的順序依次輸出。
函數重定義時會修改靜態連結嗎?
現在看下一個問題:函數定義的時候會建立靜態連結,那麼,函數重定義的時候會建立另一個靜態連結嗎?先看例子:
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啟動並執行時候重定義了宿主中的f,“新”f的靜態連結指向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
不是說重定義就會修改靜態連結嗎?但是,這裡兩個賦值只是賦值,只修改了f1和f的指標(還記得JS的函數是參考型別了嗎?),f真正的執行個體中,靜態連結沒有改變!。所以,四個輸出實際上都是宿主中的x。
結構(對象)中的成分(屬性)定址問題
請基督教(java)派和摩門教(csh)派的人原諒我用這個奇怪的稱呼,不過JS的對象太像Hash表了,我們考慮這個定址問題:
a.b編譯型語言會產生找到a後向後位移一段距離找b的代碼,但,JS是全動態語言,對象的成員可以隨意增減,還有原型的問題,讓JS對象成員的定址顯得十分有趣。
對象就是雜湊表
除開幾個特殊的方法(和原型成員)之外,對象簡直和雜湊表沒有區別,因為方法和屬性都可以儲存在“雜湊表”的“格子”裡面。月版在他的《JS王者歸來》裡面就實現了一個HashTable類。
對象本身的屬性定址
“本身的”屬性說的是hasOwnProperty為真的那些屬性。從實現的角度看,就是對象自己的“雜湊表”裡面擁有的成員。比如:
function Point(x,y){
this.x = x;
this.y = y;
}
var a = new Point(1,2);
echo("a.x:"+a.x)
Point構造器建立了“Point”對象a,並且設定了x和y屬性;於是,a的成員表裡面,就有:
| x | ---> 1
| y | ---> 2
搜尋a.x時,解譯器先找到a,然後在a的成員表裡面搜尋x,得到1。
從構造器給對象設定方法不是好策略,因為它會造成兩個同類的對象方法不等:
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,因為第四行中,對象的abs成員(方法)每次都建立了一個,於是,a.abs和b.abs實際上指向兩個完全不同的函數執行個體。因此,兩個看來相等的方法實際上不等。
扯上原型的定址問題
原型是函數(類)的屬性,它指向某個對象(不是類)。“原型”思想可以類比“照貓畫虎”:類“虎”和類“貓”沒有那個繼承那個的關係,只有“虎”像“貓”的關係。原型著眼於相似性,在js中,代碼估計可以寫作:
Tiger.prototype = new Cat()函數的原型也可以只是空白對象:
SomeClass.prototype = {}我們回到定址上來,假設用.來擷取某個屬性,它偏偏是原型裡面的屬性怎麼辦?現象是:它的確取到了,但是,這是怎麼取到的?如果對象本身的屬性和原型屬性重名怎麼辦?還好,對象本身的屬性優先。
把方法定義在原型裡面是很好的設計策略。假如我們改一下上面的例子:
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));
這下,輸出終於相等了,究其原因,因為a.abs和b.abs指向的是Point類原型的成員abs,所以輸出相等。不過,我們不能直接存取Point.prototype.abs,測試的時候直接出錯。更正:經過重新測試,“Point.prototype.abs不能訪問”的問題是我採用的JSCOnsole的問題。回複是對的,感謝您的指正!
原型鏈可以很長很長,甚至可以繞成環。考慮下面的代碼:
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);
這描述的關係大概就是“我就像你,你也像我”。原型指標對指造成了下面的輸出:
2 , 1
1 , 2
搜尋a.y的時候,沿著原型鏈找到了“a.prototype”,輸出1;b.x也是一樣的原理。現在,我們要輸出“a.z”這個沒有註冊的屬性:
echo(tyoeof a.z)我們很詫異,這裡並沒有死迴圈,看來解譯器有一個機制來處理原型鏈成環的問題。同時,原型要麼結成樹,要麼就成單環,不會有多環結構,這是很簡單的圖論。
this:函數中的潛規則
方法(函數)調用中最令人煩惱的潛規則就是this問題。從道理上講,this是一個指標,戳向調用者(某個對象)。但假如this永遠指向調用者的話,世界就太美好了。但這個可惡的指標時不時的“踢你的狗”。可能修改的情況包括call、apply、非同步呼叫和“window.eval”。
我更願意把this當做一個參數,就像lua裡面的self一樣。lua的self可以顯式傳遞,也可以用冒號來調用:
a:f(x,y,z) === a.f(a,x,y,z)JS中“素”的方法調用也是這個樣子:
a.f(x,y,z) === a.f.call(a,x,y,z)f.call才是真正“乾淨”的調用形式,這就如同lua中乾淨的調用一般。很多人都說lua是js的清晰版,lua簡化了js的很多東西,曝光了js許多的潛規則,著實不假。
修正“this”的原理
《王者歸來》上面提到的“用閉包修正this”,先看代碼:
button1.onclick = (
function(e){return function(){button_click.apply(e,arguments)}}
)(button1)別小看了這一行代碼,其實它建立了一個ARI,將button1綁定於此,然後返回一個函數,函數強制以e為調用者(主語)調用button_click,所以,傳到button_click裡的this就是e,也就是button1咯!事件綁定結束後,環境大概是下面的樣子:
button1.onclick = _F_; //給返回的匿名函數設定一個名字
_F_.staticLink = _ARI_; //建立之後就調用的匿名函數的ARI
_ARI_[e] = button1 //匿名ARI參數表裡面的e,同時也是_F_尋找的那個e
於是,我們單擊button,就會調用_F_,_F_發起了一個調用者是e的button_click函數,根據我們前面的分析,e等於button1,所以我們得到了一個保險的“指定調用者”方法。或許我們還可以繼續發揮這個思路,做成通用介面:
bindFunction = function(f,e){ //我們是好人,不改原型,不改……
return function(){
f.apply(e,arguments)
}
}
相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.