This page looks plain and unstyled because you're using a non-standard compliant browser. To see it in its best form, please visit upgrade to a browser that supports web standards. It's free and painless.

Fillano's Learning Notes 會員登入 會員註冊

前幾天試著練習把SpiderMonkey整合進程式中,寫了一個簡單的程式來執行javascript檔案,global物件除了標準以外提供了print函數來列印訊息。

今天下載了V8,編譯了他的shell sample來試試看。我電腦裡還留著幾年前編譯的JS 1.7,也同樣有一個jsshell,所以就把之前做的陣列排序測試用這三個來測試一下。結果大致符合預期,JS 1.8還是贏過V8。

先看看測試的javascript:

var query;
var getquery;
try {
    (function(){
        query = function(_cm) {
            var order=[];
            var selection=[];
            var from=[];
            var criteria=[];
            var limit=[];
            var result=[];
            var cm = _cm
/**
// valid format for parameter a is
// [int,int,int...]
// each number for zero based index number to select from the from array
*/
            this.select = function(a) {
                if (a instanceof Array) {
                    selection = a;
                } else {
                    print("The argument for 'select' function must be an Array");
                }
                return this;
            }
/**
// valid format for parameter a is Array
*/
            this.from = function(a) {
                if (a instanceof Array) {
                    from = a;
                } else {
                    print("The argument for 'select' function must be an Array");
                }
                return this;
            }
/**
// valid format for parameter a is
// [[[and1],[and2],...], [[or1],[or2],...]]
// each criteria is in the following format:
// ["index", "compare method","value"]
// index is the zero based number of the column index of from array
// compare methods maybe one of the following:
// "eq", "gt", "lt", "gteq", "lteq"....
*/
            this.where = function(a) {
                if (a instanceof Array) {
                    criteria = a;
                } else {
                    print("The argument for 'select' function must be an Array");
                }
                return this;
            }
/**
// valid format for parameter a is
// ["index","sort method"]
// index is the zero based number of the column index of the from array which the sort action depends on
// sort method may be one of the following:
// "desc", "asc"
*/
            this.orderby = function(a) {
                if (a instanceof Array) {
                    order = a;
                } else {
                    print("The argument for 'select' function must be an Array");
                }
                return this;
            }
/**
// valid format for paramater a is
// ["start", "limit"]
// where start is the zero based index of the from array the result array start
// and the limit is the length of the result array
*/
            this.limit = function(a) {
                if (a instanceof Array) {
                    limit = a;
                } else {
                    print("The argument for 'select' function must be an Array");
                }
                return this;
            }
            function comp(a,b) {
                var col = 0;
                for(var i=0; i<selection.length; i++) {
                    if(selection[i] == order[0]) col = i;
                }
                if(!isNaN(a[col]) || !isNaN(b[col])) {
                    return (order[1]=="asc")? a[col]-b[col]:0-a[col]+b[col];
                }else{
                    var factor = 10;
                    var tmp1 = a[col].toString();
                    var tmp2 = b[col].toString();
                    var r1 = 0;
                    var r2 = 0;
                    var n1 = (tmp1.length>factor)? factor:tmp1.length;
                    var n2 = (tmp2.length>factor)? factor:tmp2.length;
                    for(var m=0; m<n1; m++) {
                        r1 = r1*32 + tmp1.charCodeAt(m);
                    }
                    for(var m=0; m<n2; m++) {
                        r2 = r2*32 + tmp2.charCodeAt(m);
                    }
                    return (order[1]=="asc")? r1-r2:0-r1+r2;
                }
            }
/**
// the final treatment function
// the flow is as the following:
// 1. prepare to treat each item of the from array
// 2. if no criteria, then put the "selection" fields into ret array
// 3. if there're criterias, then do the following treatment:
//         3.1. sum up the "AND" criteria and the result in selornot1
//      3.2. sum up the "OR" criteria and the result in selornot2
//      3.3. determin the result by combine the two result using "OR" logic
// 4. sort the ret array according to the "order" condition
// 5. return the ret array as result
*/
            this.exec = function() {
                result = [];
                if(!selection.length>0) {
                    return [];
                }
                if(!from.length>0) {
                    return [];
                }
                var ret = [];
                // step 1.
                for(var i=0; i<from.length; i++) {
                    // step 2.
                    if(criteria.length==0) {
                        var tmp = [];
                        if(selection.length==1 || selection[0] == "*") {
                            tmp = from[i];
                        } else {
                            for(var j in selection) {
                                tmp.push(from[i][selection[j]]);
                            }
                        }
                        ret.push(tmp);
                    // step 3.
                    } else {
                        // if there're existing criterias...
                        for(var i=0; i<from.length; i++) {
                            //tranversing all AND criteria to determin which row to add to the result set
                            //to speed up the query, any "FALSE" condition will force the loop to stop and give the result to "FALSE"
                            var selornot1 = false;
                            // 3.1
                            if(criteria.length > 0) {
                                for(var m=0; m<criteria[0].length; m++) {
                                    if(cm[criteria[0][m][1]]!==undefined) {
                                        selornot1 = cm[criteria[0][m][1]](from[i][criteria[0][m][0]], criteria[0][m][2]);
                                        if(!selornot1) break;
                                    }
                                }
                            }
                            //tranversing all AND criteria to determin which row to add to the result set
                            //to speed up the query, any "TRUE" condition will force the loop to stop and give the result to "TRUE"
                            var selornot2 = false;
                            // 3.2
                            if(criteria.length > 1) {
                                for(var m=0; m<criteria[1].length; m++){
                                    if(cm[criteria[1][m][1]]!==undefined) {
                                        selornot2 = cm[criteria[1][m][1]](from[i][criteria[1][m][0]], criteria[1][m][2]);
                                        if(selornot2) break;
                                    }
                                }
                            }
                            // 3.3
                            if(selornot1 || selornot2) {
                                var tmp = [];
                                if(selection.length==1 || selection[0] == "*") {
                                    tmp = from[i];
                                } else {
                                    for(var j in selection) {
                                        tmp.push(from[i][selection[j]]);
                                    }
                                }
                                ret.push(tmp);
                            }
                        }
                    }
                }
                // step 4.
                if(order.length > 0) {
                    ret.sort(comp);
                }
                // step 5.
                if(limit.length>0) {
                    result = ret.slice(limit[0],limit[0]+limit[1]);
                    return ret.slice(limit[0],limit[0]+limit[1]);
                } else {
                    result = ret;
                    return ret;
                }
            }
            this.result = function() {
                return result;
            }
            this.count = function() {
                return result.length;
            }
            this.test = function() {
                print(selection);
                print(from);
                print(criteria);
                print(order);
                print(limit);
            }
        }
        var cm = {
            "eq": function(a,b) {
                if(a==b) return true;
                return false;
            },
            "gt": function(a,b) {
                if(a>b) return true;
                return false;
            },
            "lt": function(a,b) {
                if(a<b) return true;
                return false;
            },
            "gteq": function(a,b) {
                if(a>=b) return true;
                return false;
            },
            "lteq": function(a,b) {
                if(a<=b) return true;
                return false;
            }
        };
        getquery = function() {
            return new query(cm);
        }
    })();
}catch(e){print(e);}
try{
    (function(){
        var b = [];
        for (var i=0; i<10000; i++) {
            var j=10000+i;
            var tmp = [];
            tmp.push(i);
            tmp.push(i+1);
            tmp.push(i+2);
            tmp.push(String.fromCharCode(j,j+1,j+2));
            b.push(tmp);
        }
        function test002() {
            var a = getquery();
            a.select(['*']).from(b).orderby([3,"asc"]);
            var t1 = new Date().getTime();
            var c = a.exec();
            print("asc "+a.count()+" rows: "+(new Date().getTime() - t1)+"ms\n");
            a.orderby([3,"desc"]);
            var t2 = new Date().getTime();
            c = a.exec();
            print("desc "+a.count()+" rows: "+(new Date().getTime() - t2)+"ms\n");
            a.orderby([]);
            var t3 = new Date().getTime();
            c = a.exec();
            print("no sort "+a.count()+" rows: "+(new Date().getTime() - t3)+"ms\n");
        }
        test002();
    })();
}catch(e){print(e);}

接下來用三支程式來做測試。js001.exe是我練習整合SpiderMonkey時做的,使用動態連結方式呼叫在js32.dll等裡面的函數來執行javascript(這些dll是我直接從Firefox3目錄裡面拷貝過來的)。jsshell.exe是從JS 1.7原始碼編譯的。shell.exe則是V8提供的sample,在編譯時下sample=shell參數就可以編譯出來(靜態編譯)。執行程式以後,再利用他提供的load函數來載入測試程式。以下是簡單的測試結果畫面:

首先是jsshell.exe(js 1.7, firefox2):
v8 vs js17 vs js18

其次是js001.exe(js 1.8, firefox3):
v8 vs js17 vs js18

最後是shell.exe(V8, google chrome):
v8 vs js17 vs js18

照測試結果,一般javascript速度是V8(Chrome) > JS 1.8(Firefox3) > JS 1.7(Firefox2),但是陣列排序速度是JS 1.8(Firefox3) > V8(Chrome) > JS 1.7(Firefox2)。因為Array.sort會呼叫我自己寫的排序函數,所以強烈懷疑這個結果是因為JS 1.8的context switching速度比較快的關係。

有空時把V8整合到程式裡面以後,再做一次測試吧。


阿,文貼上來才發現,其實shell這一支只要傳檔名當做參數就可以了,我進去shell再用load跑有一點多此一舉。但是速度差不多。


剛剛發現為什麼陣列排序速度會比較慢了。我剛剛在V8的Issue List上看到:Issue 5: Poor Performance in Benchmark,進去V8的src目錄下,會看到array.js,打開看看,嗯......看起來是真的。所以原因就是:在V8中,陣列是用Javascript來實作的喔!!!(其實不只啦,可以看到其他的js檔案,所以像Math也是...其他還沒看。)JS 理所當然是用C,編譯過後比較快是理所當然。V8也很厲害,他可以把javascript編譯到速度那麼快。JS 1.7的陣列當然還是用C實作的,但是速度比javascript編譯的還差一截呢。

之前沒有徹底搞懂這兩者的差別,今天回頭去看Martin Fowler講Closure的文章以及wikipedia對closure的解釋時,總算弄清楚了(之前偷懶,沒把東西好好看完)。

lexical scope的特性,讓我們可以用區域變數的方式,把變數當作一個function物件的private member,但是又可以用一個function當作getter/setter來存取他。例如:

function Bean() {
	var X;
	var Y;
	this.setX = function(x) {
		X=x;
	}
	this.getX = function() {
		return X;
	}
	this.setY = function(y) {
		Y = y;
	}
	this.getY = function() {
		return Y;
	}
}
var a = new Bean();
a.setX(3);
a.setY(3);
alert(a.getX()+""+a.getY());

另一個更常用的時機,是使用setTimeout。例如:

function TT() {
	this.lm = "the message";
	var localthis = this;
	this.startT = function() {
		setTimeout(function(){alert(localthis.lm);},500);
	}
}
var d = new TT("the message");
d.startT();

在傳給setTimeout的匿名函數裡面使用this的話,這個this會指向Window物件,所以要在這裡要先把他指定給一個區域變數,這樣就可以讓存在this.lm裡的訊息,透過localthis來取用。

像這樣可以用函數把區域變數攜出到別的scope使用,還只是lexical scope的應用,要產生closure,需要一些特別的條件:

var m1 = "free variable to be bound.";
var m2 = "another free variable to be bound.";
function a(free) {
	return function(){setTimeout(function(){alert(free);},500);}
}
var b = a(m1);
m1 = null;
b();
var c = a(m2);
m2 = null;
c();
b();
c();
(本例有誤,請見下文補充,感謝Josh網友指教。)

執行這一段程式,結果會依序跳出"free variable to be bound", "another free variable to be bound", "free variable to be bound", "another free variable to be bound"訊息。closure發生的要件就是,一個free variable透過一段敘述,結果就被bound在這一段敘述裡了。透過這一段程式可以發現,不論傳給a()什麼,什麼就被綁在裡面了,即是傳給他的變數之後用assign的方式釋放或改變了(理論上),被綁在a()裡面的東西也不會改變。

許多討論javascript closure的網站,都會舉類似下面的意外使用到closure的狀況:

function addEvent(node) {
	node.onclick = function() {
		node.innerHTML += "bla ";//here the leak started
	}
}

像上述的程式,你對多少dom node做處理,就有多少dom node會被keep在記憶體中。數量多的話.....

另外,第一個closure的例子透過b=null;c=null;這樣應該可以釋放被「封絕」的東西;而第二個例子透過node.onclick=null;也應該可以。但是在舊版的IE(6?)就不是這一回事。因為HTML部份跟Script是分開的兩個ActiveX元件,可以做出Closure,但是沒有辦法這樣釋放記憶體......結果就......建議你不要使用Closure。(不然就會變成火炬喔)


2008-10-4 0:24 補充

最近在看Crockford的《Javascript: 優良部分》,看起來他並沒有把「參數傳入函式」當作形成closure的必要條件,所以像「參數傳入函式」這種狀況應該算是closure的一種,只是side effect比較大。


2008-12-15 16:04補充

今天蒙Josh兄指正我的例子有問題(第三個例子),的確也有問題,我不應該用String做例子,修改後如下:

var str = {a:"test"};
function a(v) {
	v['a'] = "test2";
}
a(str);
alert(str.a);
function b(v) {
	var b = v;
	this.getB = function() {
		return b.a;
	}
}
var c=new b(str);
str = null;
alert(c.getB());

上面這一段code才能表現出我要說的意思,對於DOM物件也是類似的狀況。我另外測試了所有的型別,發現一些有趣的東西,我用另一篇文章寫好了。


2008-12-16 10:28補充

另外寫了一篇「對於Javascript所有型別在Closure中表現的測試」,可以參考一下。

Mock Objects/JMock的作者們(Steve Freeman、Nat Pryce)正在寫一本測試驅動開發的書,逐章修改並在網路上討論發表: Growing Object-Oriented Software, Guided by Tests - Table of Contents

書還在成型中,所以不時會去瞄一眼。今天注意到,文獻目錄已經先出來了,就進去看一下,有一個連結引起我的注意: Bibliography。 裡面有一個連結是: [Kay1998] Prototypes vs Classes. Alan Kay. 1998. 好奇之下,就點進去看了。

頭兩段話馬上吸引我的視線:(黑體字是Freeman跟Pryce文中引用的句子)

Just a gentle reminder that I took some pains at the last OOPSLA to try to remind everyone that Smalltalk is not only NOT its syntax or the class library, it is not even about classes. I'm sorry that I long ago coined the term "objects" for this topic because it gets many people to focus on the lesser idea.

The big idea is "messaging" -- that is what the kernal of Smalltalk/Squeak is all about (and it's something that was never quite completed in our Xerox PARC phase). The Japanese have a small word -- ma -- for "that which is in between" -- perhaps the nearest English equivalent is "interstitial". The key in making great and growable systems is much more to design how its modules communicate rather than what their internal properties and behaviors should be. Think of the internet -- to live, it (a) has to allow many different kinds of ideas and realizations that are beyond any single standard and (b) to allow varying degrees of safe interoperability between these ideas.

從討論串看出來,儘管苦口婆心,還是有很多人沒弄明白Kay的意思。不過至少Freeman跟Pryce引這段話是很有深意的:Chapter 4. Test-Driven Development with Objects

到底Alan Kay是誰(我知道我孤陋寡聞)?上google找一下,wikipedia有他的資料:Alan Kay。嗯嗯,1997年圖靈獎得主,OOP跟GUI先驅(嗯嗯,簡單地說,Object Orient Programming這個名詞是他發明的,他是Smalltalk重要成員,曾經在全錄的PARC實驗室工作,開發GUI系統,這後來被Apple搶去用了......換句話說,我能在這裡打這篇文章,都是他的成果囉)。

另外,這篇討論串的出處,是一個叫Squeak的動態媒體軟體的maillist。這個計畫操作介面的後續計畫叫做Tweak,在內部設計上採用classes base,但是使用者操作(撰寫程式)上,卻使用prototypes base的物件系統。

該懺悔一下,之前沒有好好看Douglas Crockford的網站,不知道上面還有這麼好玩的東西。(就在http://javascript.crockford.com/右上角)

今天上去晃了一下,發現他有幾個Functional Javascript網站的連結?!之前竟然都沒發現,自己呆呆地把其他程式語言的做法改到javascript上做......

第一個網站是:Higher-Order JavaScript,作者是Sean M. Burke。他示範了一些perl higher order語法的javascript版本,然後寫了許多範例。

第二個網站是:Functional Javascript,作者是Oliver Steele。這是一個函式庫,定義了一些常見的函數式語言常用的函數,像是map, reduce, select等。

第三個網站是:Curried JavaScript functions ,作者是Svend Tofte。他在這個blog裡面深入探討了currying及lambda calculus的主題以及如何在javascript中實作。

這幾個網站看起來都很有趣:)

想要知道Firefox3與IE7的速度差距?用陣列排序來測試可能最明顯...

之前用了Fluent Interface模式寫了一個javascript陣列(二維,類似資料表)搜尋的物件,使用方式類似SQL。大體功能做出來以後,做了一下功能的驗證。這時發現明顯的速度差距......

自己做了三個功能測試,先用迴圈產生一個有一萬筆資料的陣列,第一個測試是測試功能,第二個是測試排序的速度,第三個是驗證文字排序的效果。結果兩個瀏覽器在排序上出現了明顯的速度差距:

Firefox3的結果:

  1. 升冪:18ms
  2. 降冪:79ms
  3. 不排序:9ms
IE7的結果:
  1. 升冪:1668ms
  2. 降冪:1506ms
  3. 不排序:43ms

雖然只做了幾次測試,但是數據差距蠻大的,所以結果應該是很明顯......Firefox3勝出。

有興趣的話可以自己試試看,我不知道在不同瀏覽器上是不是會有問題就是了:http://www.fillano.idv.tw/query.html

javascript程式:http://www.fillano.idv.tw/query.js

最近看了一些討論動態載入javascript與dependency的討論,主要問題在於動態載入後直接呼叫。通常Lazy Load可以用兩種方法達成,一種是靠ajax與eval,另一種是靠動態在dom裡面插入script element。

使用ajax/eval的方法因為程式用eval執行了,不會有延遲,所以立刻執行已載入的程式是沒有問題的。但是不想使用ajax,而改用在dom裡面插入script元素時,又會因為延遲而不一定能隨即呼叫已載入的程式。

要蹶決這樣的問題,通常的方法是把這個script元素加上事件處理函數,然後在事件發生時才執行已載入的程式。但是又會碰到一個小問題,就是在ie只能使用onreadystatechange事件,在ff則只能使用onload事件。

簡單的方法是兩個統統都實現。因為對於javascript來說,給一個物件額外增加onreadystatechange或是onload屬性,其實都不會有錯誤,這樣就不必判斷是哪個瀏覽器。不過這只是一個tricky的方法就是了,也許新的瀏覽器會不適用....