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 會員登入 會員註冊

之前因為好奇,稍微trace了一下jQuery的事件管理機制。在看過原始碼跟檢查過jQuery處理過的node之後,寫了一個小程式來取出掛在node上的事件處理函數。

<html>
<script src="js/jquery-1.2.6.js"></script>
<body>
<div id="panel">test</div>
<div id="target">target</div>
<script>
$("#panel").click(function(e){
    for(var i in this) {
        if(i.indexOf("jQuery")==0) {
            var tmp = this[i];
        }
    }
    var ret = '<table border="1" cellspacing="0" cellpadding="2">';
    for (var i in $.cache[tmp]['events']['click']) {
        ret += '<tr><td>' + i + ':</td><td>' + $.cache[tmp]['events']['click'][i] + '</td></tr>';
    }
    ret += '</table>';
    this.innerHTML = ret;
    alert($.event.guid);
});
$("#panel").click(function(e){
    alert(this);
});
$("#target").click(function(e){
    alert('target');
});
$("#panel").click(function(e){
    alert('test');
});
$("#target").click(function(e){
    for(var i in this) {
        if(i.indexOf("jQuery")==0) {
            var tmp = this[i];
        }
    }
    var ret = '<table border="1" cellspacing="0" cellpadding="2">';
    for (var i in $.cache[tmp]['events']['click']) {
        ret += '<tr><td>' + i + ':</td><td>' + $.cache[tmp]['events']['click'][i] + '</td></tr>';
    }
    ret += '</table>';
    this.innerHTML = ret;
    alert($.event.guid);
});
</script>
</body>
</html>

 

 (閱讀全文)

這其實是剛剛在Crockford的書中瞄到的,但是很實用。

看看幾個例子吧。

常見到的問題之一,是想要在onclick事件處理函數中使用i變數,但是寫成這樣:

<html>
<body>
<input type="button" value="test1">
<input type="button" value="test2">
<input type="button" value="test3">
<script>
var a = document.getElementsByTagName("input");
var i=0;
for (i=0; i< a.length; i++) {
    a[i].onclick = function() {
        alert(i);
    }
}
</script>
</body>
</html>

結果在執行事件處理函數時,其實for迴圈已經跑完,所以永遠跑出2。這時候用一個匿名函數傳i進去,返回事件處理函數,就可以解決問題:

var a = document.getElementsByTagName("input");
var i=0;
for (i=0; i< a.length; i++) {
    a[i].onclick = function(i) {
        return function(e) {
            alert(i);
        };
    }(i);
}

常見問題之二,是在函數物件實例中把事件處理函數指定給某個node:

function handle() {
    this.abc = "def";
    this.setHandler = function(obj) {
        obj.onclick = function() {
            alert(this.abc);
        };
    }
}
var a = new handle();
a.setHandler(document.getElementsByTagName('input')[0]);

這樣就會出現"undefined"訊息。

可以用匿名函數把this傳給事件處理函數,類似前面的解法:

function handle() {
    this.abc = "def";
    this.setHandler = function(obj) {
        obj.onclick = function(that) {
            return function() {
                alert(that.abc);
            };
        }(this);
    }
}
var a = new handle();
a.setHandler(document.getElementsByTagName('input')[0]);

最後一個常見問題是使用setTimeout,我們常常忘記這個函數是在global context下執行的。像這樣的例子就會出問題:

function handle() {
    this.abc = "def";
    this.delayMsg = function(m) {
        setTimeout(function(){alert(this.abc);}, m);
    };
}
var a = new handle();
a.delayMsg(600);

用匿名函數解決的方法還是類似:

function handle() {
    this.abc = "def";
    this.delayMsg = function(m) {
        setTimeout(function(a){
            return function(e){
                alert(a.abc);
            };
        }(this), m);
    };
}
var a = new handle();
a.delayMsg(600);

所以用匿名函數,curry還有closure,也可以很直覺地解決scope問題呢。

很久以前寫過關於匿名函數遞迴的方法,用的是arguments屬性。不過後來發現其實不用那麼麻煩。還有一個簡單的方法,以及應用函數語言技巧的方法,可以拿來應用在匿名函數遞迴上。

javascript的function expression其實還有一個作法,讓你可以命名匿名函數,這樣在做匿名函數遞迴時就可以不用到arguments。

var a = (function f(x) {
    if (x>1) {
        return x * f(x-1);
    } else {
        return 1;
    }
})(5);
alert(a);

上面的function expression中,把function命名為f,但是他只作用在()裡面,不會對外面的scope產生影響,所以如果在alert(a)之後呼叫f會出現函數未定義的錯誤。


這幾天回頭看了一下Lambda Calculus的資料,裡面提到第一印象中Lambda Calculus似乎無法做出遞迴結構,但其實可以利用Y Combinator來做。這個作法很有趣。

按照定義,Y combinator:
Y = λ g. (λ x. g (x x)) (λ x. g (x x))

這樣的結構改成javascript就會變成:

var Y = function(g) {
    return (function(x) {
        return g(function() {
            return x(x);
        });
    })(function(x) {
        return g(function() {
            return x(x);
        });
    });
};

如果我想傳一個參數給函數,那需要調整一下這個Y combinator,讓他接受參數:

var Y = function(g) {
    return (function(x) {
        return g(function(n) {
            return x(x)(n);
        });
    })(function(x) {
        return g(function(n) {
            return x(x)(n);
        });
    });
};

這樣傳需要遞迴的函數給Y時,會返回一個可以接受一個參數的函數。

做階乘的函數,可以抽象為:

var g = function(f,n){
    if(n==0) {
        return 1;
    } else {
        return n*f(f,n-1);
    }
};
alert(g(g,5));

使用時,還必須把g傳給自己,才能做出遞迴。這樣做就無法匿名,否則無法傳遞g給自己做參數了。

先把把上面過程curry化備用:

var g = function(f){
    return function(n){
        if (n==0) {
            return 1;
        } else {
            return n*f(f)(n-1);
        }
    };
};
alert(g(g)(5));

使用Y combinator,他會為傳入的函數做出遞迴結構,先不用匿名函數做,比較清楚:

var g = function(f){
    return function(n){
        if (n==0) {
            return 1;
        } else {
            return n*f(n-1);
        }
    };
};
var Y = function(g) {
    return (function(x) {
        return g(function(n) {
            return x(x)(n);
        });
    })(function(x) {
        return g(function(n) {
            return x(x)(n);
        });
    });
};
alert(Y(g)(5));

透過Y,可以在每次遞迴中把g函數裡面的f展開為f(f),並且把g函數傳遞給自己,最後傳回一個遞迴的函數。接著傳遞參數n給這個產生的函數,就可以計算出遞迴的結果。

再來用匿名函數的方法做(跟前面等義,只是把g改成匿名函數):

var Y = function(g) {
    return (function(x) {
        return g(function(n) {
            return x(x)(n);
        });
    })(function(x) {
        return g(function(n) {
            return x(x)(n);
        });
    });
};
alert(Y(function(f){
    return function(n){
        if (n==0) {
            return 1;
        } else {
            return n*f(n-1);
        }
    };
})(5));

有趣吧?

使用上,可以用Y combinator產生遞迴函數來用:

var Y = function(g) {
    return (function(x) {
        return g(function(n) {
            return x(x)(n);
        });
    })(function(x) {
        return g(function(n) {
            return x(x)(n);
        });
    });
};
var fact = Y(function(f){
    return function(n){
        if (n==0) {
            return 1;
        } else {
            return n*f(n-1);
        }
    };
});
alert(fact(5));

接著試試看費氏數列,不使用Y combinator的狀況,必須這樣寫:

var f = function(f,n) {
    if(n==0) {
        return 0;
    } else if(n==1) {
        return 1;
    } else if(n>1) {
        return f(f,n-1) + f(f,n-2);
    }
};
alert(f(f,5));

現在可以用Y combinator來產生費氏數列函數:

var fab = Y(function(f){
    return function(n){
        if(n==0) {
            return 0;
        } else if(n==1) {
            return 1;
        } else if(n>1) {
            return f(n-1) + f(n-2);
        }
    };
});
alert(fab(5));


已經把自己弄糊塗了,稍微整理一下關於Y的思考:

var f = function(n) {
    if(n==0) {
        return 1;
    } else {
        return n * f(n-1);
    }
};
alert(f(5));
var g = function(f, n) {
    if(n==0) {
        return 1;
    } else {
        return n * f(n-1);
    }
};
alert(g(f,5));
g = function(f) {
    return function(n) {
        if(n==0) {
            return 1;
        } else {
            return n * f(n-1);
        }
    };
};
alert(g(f)(5));
var fact = g(f);
alert(fact(5));
var Y = function (g) {return g(f);}

用有名字的函數做都不是問題,使用Y combinator的關鍵在於匿名也能做出這樣的結構。第一步做出階乘的函數f。然後調整一下結構,做出一個函數g,接受f作為參數,這樣是為了匿名函數無法呼叫自己,所以把他移出去作成一個參數。接著把g做curry化。這樣做我們會發現,g(f)實際上會返回遞迴的階乘函數f。把這個過程展開,可以得出下面的結構:

var b = (function (x){
    return function(n) {
        if(n==0) {
            return 1;
        } else {
            return n * x(x)(n-1);
        }
    };
})(function (x){
    return function(n) {
        if(n==0) {
            return 1;
        } else {
            return n * x(x)(n-1);
        }
    };
});
alert(b(5));

g與f的差別,在於f靠呼叫f來達成遞迴,g靠傳入f並讓f遞迴,但是g與f的結構一樣,其實能完成的計算也一樣。所以可以靠不斷傳入g做為g的參數而返回計算階乘的函數來構成遞迴。這個過程可以靠一個匿名函數的結構達成,遞迴的地方,則由f改成g(g),產生的函數可以接收n來做階乘計算,在這個函數中又用g(g)來遞迴。我們把匿名函數上半部看成g,下半部傳入的函數叫做g1的話,執行第一次遞迴迭代的是g,之後則是g1。依序為g(g1)->g1(g1)->g1(g1)->g1(g1)......最後就可以計算出階乘的答案。關鍵其實就在於傳入的函數參數,返回的計算函數,計算函數中的遞迴結構把上述過程再度重現。

最後來看Y Combinator怎麼運作:

var Y = function(g) {
    return (function(x) {
        return g(function(n) {
            return x(x)(n);
        });
    })(function(x) {
        return g(function(n) {
            return x(x)(n);
        });
    });
};

其實是一樣的邏輯結構。只要稍微調整過g,把他傳給Y,產生的就是g的遞迴版本函數。關鍵還是在於x(x)(n)。另外,可以看出三個等價的函數f、g(f)、Y(g)。差別在於,前兩個不是匿名函數,最後一個傳入的參數可以是匿名函數。如果不想靠Y Combinator,也可以把這樣的結構展開,用b這個函數的方法來達成。


2008-11-12 18:12 補充:

這篇的靈感其實來自於wikipedia上關於Lambda Calculus的說明:
http://en.wikipedia.org/wiki/Lambda_calculus

另外也用到currying:
http://en.wikipedia.org/wiki/Currying

這兩篇在wikipedia上有中文版本,內容也相當完整。

其實寫這篇的時候,雖然做出結果,但是覺得沒有把他考慮清楚,就重新想了一遍,所以內容前後有一些地方有重複甚至有些矛盾的。不過還是把他當作一個紀錄啦。


2008-11-12 23:02

阿,還有一些參考資料是關於continuation passing style:
http://en.wikipedia.org/wiki/Continuation_passing_style

總之,這篇其實講functional language大於平常的javascript programming。既然Crockford在他的書中提到:「JavaScript是第一個躍上主流的lambda語言。」,有機會還是會想嘗試看看他屬於這一部份的特性。

YUI3的IO模組除了有XMLHttpRequest的功能外,還可以利用模組裡面的io.swf來做出cross domain request,突破XMLHttpRequest無法跨domain的限制。

其實flash的cross domain request還是有限制的,就是伺服器上必須有一個crossdomain.xml,描述可以接受request的domain。像這樣:

<?xml version="1.0"?>
<cross-domain-policy>
  <allow-access-from domain="a.b.c" />
  <allow-access-from domain="d.e.f" />
</cross-domain-policy>

這樣就可以接受a.b.c與d.e.f網站送出的request。如果要讓所有網站都可以對伺服器發出request,就:

<?xml version="1.0"?>
<cross-domain-policy>
  <allow-access-from domain="*" />
</cross-domain-policy>

接下來寫一個簡單的網頁,擷取blog上面的rss,然後秀出title list(還是用YUI3網站上的tutorial改的):

<html>
<script src="build/yui/yui-min.js"></script>
<body>
<input type="button" value="test" id="test">
<div id="panel"></div>
<script>
YUI().use('io', function(Y) {
    var xdrConfig = {
        id: 'flash',
        yid: Y.id,
        src: 'build/io/io.swf'
    };
    Y.io.transport(xdrConfig);
    var cfg = {
        method: 'GET',
        xdr: {
            use: 'flash',
            responseXML: true
        },
        on: {
            success: function(id, o, a) {
                Y.get('#panel').set('innerHTML','');
                try {
                    var parser = new DOMParser();
                    var xmldoc = parser.parseFromString(o.responseText, 'text/xml').documentElement;
                }catch(e){
                    try {
                        var xmldoc=new ActiveXObject("Microsoft.XMLDOM");
                        xmldoc.async="false";
                        xmldoc.loadXML(o.responseText);    
                    }catch(e){alert(e);}
                }
                var objs = xmldoc.getElementsByTagName('title');
                var str = "";
                str += "<ul>";
                for (var i=0; i<objs.length; i++) {
                    str += "<li>" + objs[i].firstChild.nodeValue + "</li>";
                }
                str += "</ul>";
                Y.get('#panel').set('innerHTML', str);
            },
            failure: function(id, o, a) {
                Y.get('#panel').set('innerHTML','Failed...');
            },
            start: function(id, a) {
                Y.get('#panel').set('innerHTML', 'Starting...');
            },
            abort: function(id, a) {
                Y.get('#panel').set('innerHTML','Aborted...');
            }
        }
    };
    Y.on('io:xdrReady', function() {
        Y.get('#test').on('click', function(e) {
            var obj = Y.io(
                "http://blog.fillano.idv.tw/rss.php?blogId=1&profile=rss20",
                cfg
            );
        });
    });
});
</script>
</body>
</html>

比較麻煩的是,xdr只能回傳responseText,所以必須想辦法把responseText解析成DOM物件,這樣程式才容易處理,在IE需要用到Microsoft.XMLDOM這個activeX物件,在firefox/mozilla則可利用DOMParser物件。(不過這幾個方法可能不適用於其他瀏覽器...究竟不是標準方法)

io模組的使用比較不那麼直覺,首先要用Y.io.transport(xdrconfig)來設定xdr的一些基本參數。使用時,先組好一個request config物件,然後傳給Y.io(config),這時request就會發動,而事件處理函數也會照指定的發動。

下面的連結是自己寫的例子,內容跟上面的程式碼一樣:
http://www.fillano.idv.tw/test344.html

恩,其實是用YUI的範例改的,然後稍微調整成我想要做的例子,順便也理解一下drag&drop模組的運用。

廢話不多說,先來看寫好的例子,是一個填充題,可以把答案拖曳到空格中,最後檢查算出分數。

<html>
<style>
    .cls1 {border: solid 1px black;cursor:move; font-size: 13px}
    .cls2 {text-decoration: underline;}
</style>
<script src="build/yui/yui-min.js"></script>
<script>
    var quiz = {
        quiz1: 'the feast of Crispian',
        quiz2: 'then shall our names',
        quiz3: 'band of brothers'
    };
</script>
<body>
<div>
<script>
(function(){
    for(var i in quiz) {
        document.write("<span class="cls1" id=""+i+"">"+quiz[i]+"</span>&nbsp;");
    }
})();
</script>
</div><br>
<div><input type="button" id="check" value="check" style="visibility:hidden"></div><br>
<div style="font-size:13px">
    This day is called <span class="cls2" id="quiz1a">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</span>:<br>
    He that outlives this day, and comes safe home,<br>
    Will stand a tip-toe when the day is named,<br>
    And rouse him at the name of Crispian.<br>
    He that shall live this day, and see old age,<br>
    Will yearly on the vigil feast his neighbours,<br>
    And say 'To-morrow is Saint Crispian:'<br>
    Then will he strip his sleeve and show his scars.<br>
    And say 'These wounds I had on Crispin's day.'<br>
    Old men forget: yet all shall be forgot,<br>
    But he'll remember with advantages<br>
  What feats he did that day: <span class="cls2" id="quiz2a">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</span>.<br>
    Familiar in his mouth as household words<br>
    Harry the king, Bedford and Exeter,<br>
    Warwick and Talbot, Salisbury and Gloucester,<br>
    Be in their flowing cups freshly remember'd.<br>
    This story shall the good man teach his son;<br>
    And Crispin Crispian shall ne'er go by,<br>
    From this day to the ending of the world,<br>
    But we in it shall be remember'd;<br>
    We few, we happy few, we <span class="cls2" id="quiz3a">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</span>;<br>
    For he to-day that sheds his blood with me<br>
    Shall be my brother; be he ne'er so vile,<br>
    This day shall gentle his condition:<br>
    And gentlemen in England now a-bed<br>
    Shall think themselves accursed they were not here,<br>
    And hold their manhoods cheap whiles any speaks<br>
    That fought with us upon Saint Crispin's day.
</div>
<script>
YUI().use('dd-drop','dd-proxy',function(Y){
    var nodes = Y.all('.cls1');
    Y.each(nodes,function(v, k, items) {
        var drag = new Y.DD.Drag({
            node: items.item(k),
            groups: ['one'],
            proxy: true,
            dragMode: 'intersect',
            moveOnEnd: false
        });
        drag.on('drag:start', function() {
            var p = this.get('dragNode');
            var n = this.get('node');
            n.setStyle('opacity', .25);
            p.set('innerHTML', n.get('innerHTML'));
            p.setStyle('opacity', .65);
            p.setStyle('fontSize', '13px');
        });
        drag.on('drag:end', function() {
            var n = this.get('node');
            n.setStyle('opacity', 1);
        });
        drag.on('drag:drophit', function(e) {
            e.drop.get('node').set('innerHTML', this.get('node').get('innerHTML'));
            this.removeFromGroup('one');
            this.get('node').setStyle('display','none');
            Y.get('#check').setStyle('visibility', 'visible');
        });
    });
    var places = Y.all('.cls2');
    Y.each(places,function(v, k, items) {
        var drop = new Y.DD.Drop({
            node: items.item(k),
            groups: ['one']
        });
    });
    Y.get('#check').on('click', function(e) {
        var result = 0;
        Y.each(places, function(v, k, items) {
            if(v.get('innerHTML') == quiz[v.get('id').substring(0, v.get('id').length-1)]) result++;
        });
        alert("You got " + result + " points");
    });
});
</script>
</body>
</html>

有興趣可以到這個連結看看:http://www.fillano.idv.tw/test342.html

要產生可drag的物件,只要用Y.DD.Drag,把一些設定以及node傳給他的constructor(上例的var drag=new Y.DD.Drag(.....))。Drop也差不多。

使用proxy可以做出一個隨著滑鼠拖曳的proxy,同一個group的drag物件拖曳到drop物件上,就會發生drag:drophit事件(這是在drag-drop裡面定義的)。其他的還有拖曳開始後,拖曳結束後的一些事件處理。簡單寫幾行就可以做出來了。