Now browsing the archives for the 'JavaScript' category.

while ループ条件中の in 演算子の不思議な挙動

JavaScript で単純なキーバリュー型のオブジェクトから、新しい一意のキーを生成する関数を作った(HTML にしたソースコードはこちら)。

function func() {
    var obj = {1:1, 2:2, 3:3};
    var i = 0;
    do { i++; } while (i in obj);
    return i;
}
alert(func());
alert(func());
alert(func());

これを Firefox 3.6 で実行すると、なぜか結果が 4, 4, 2 だったり 4, 2, 2 だったりと毎回結果が変わる不思議な動作となる。 JavaScript のJIT機能を無効にした場合や、IEなどの他ブラウザでは想定どおり 4, 4, 4 となる。不思議なことに while 文の条件を while (i in obj === true) にした場合も 4, 4, 4 となる。

while ループの条件中でこういう in の使い方をするときは以下のいずれかの方式にしたほうが安全っぽい。

while (obj[i] !== undefined)
while (obj.hasOwnProperty(i))

TOP

JavaScript 関数と XPCOM メソッドの例外ハンドリング

JavaScript の関数がスローする例外の内容を知るには、例外オブジェクトの値そのものを調べる。
XPCOM のメソッドがスローする例外の内容を知るには、例外オブジェクト (nsIXPCException オブジェクト) の result プロパティなどを調べる。

例えば以下のような純粋な JavaScript の関数があるとすると、

const Cr = Components.results;
function test() {
    throw Cr.NS_ERROR_FAILURE;
}

関数実行時に catch したオブジェクトの値そのものを調べることで例外の内容を知ることができる。

try {
    test();
}
catch (ex if ex == Cr.NS_ERROR_FAILURE) {
    alert("Failed!");
}

一方、上記の関数 test と同じ内容のメソッドを JavaScript 製 XPCOM のメソッドとして実装した場合、メソッド実行時に catch した例外は nsIXPCException オブジェクトとなり、 result プロパティなどから例外の内容を知ることができる。

try {
    Cc["@XXX"].getService(Ci.XXX).test();
}
catch (ex if ex.result == Cr.NS_ERROR_FAILURE) {
    alert("Failed!");
}

Components.Exception コンストラクタを使えば、純粋な JavaScript の関数で nsIXPCException オブジェクトの例外をスローすることも可能。エラーコンソールに例外発生のソースファイルなどの詳細表示ができるといった利点が挙げられる。

function test() {
    throw new Components.Exception("Failed!", Cr.NS_ERROR_FAILURE);
}

TOP

JavaScript コードモジュール

Using JavaScript code modules – MDC の和訳が完了した。
「JavaScript コードモジュール」とは、 Firefox 3 の JavaScript 製 XPCOM で頻繁に見かける以下のようなやつのことです。

Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");

上記の例のように共通で使用するユーティリティ的な関数群をモジュール化して、色々な場所からインポートして利用することができるだけでなく、あらゆる JavaScript のスコープで共有可能なシングルトンのオブジェクトとしての利用も可能です。今までは同様のことを実現するためには JavaScript 製 XPCOM の定義が必要であり、若干敷居が高かった。

まあ Firefox 3 以降ということで実践投入はだいぶ先のことになりそうだが、 JavaScript コードモジュールによって拡張機能の実装方式の自由度が高まることは間違いないでしょう。

TOP

setTimeout のコールバック関数内でローカル変数を使用する

var fruits = ["apple", "orange", "banana"];

という配列があるとき、

for (var i = 0; i < fruits.length; i++) {
    window.setTimeout(function() { alert(fruits[i]); }, i * 1000);
}

こうすると1秒おきに「undefined」が3回表示されてしまう。コールバック関数が呼び出されたときにはすでにローカル変数 i は破棄されている i の値が3になっているためである。
以下のようにコールバック関数を文字列にしておけば、1秒おきに「apple」「orange」「banana」が表示される。

for (var i = 0; i < fruits.length; i++) {
    window.setTimeout("alert('" + fruits[i] + "');", i * 1000);
}

あるいは、以下のように setTimeout の第3引数でコールバック関数へ引数を渡す方法もある。コールバック関数の内容が複雑になる場合はこの方が良い。 by Piroさん

for (var i = 0; i < fruits.length; i++) {
    window.setTimeout(function(aArg) { alert(aArg); }, i * 1000, fruits[i]);
}

Firefox 2以降、JavaScript 1.7以降限定 by nanto_viさん

for (var i = 0; i < fruits.length; i++) {
    window.setTimeout(let (fruit = fruits[i]) function() { alert(fruit); }, i * 1000);
}

こちらは Firefox 1.0 でもOK by nanto_viさん

for (var i = 0; i < fruits.length; i++) {
    with ({ fruit: fruits[i] }) {
        window.setTimeout(function() { alert(fruit); }, i * 1000);
    }
}

クロージャを使って以下のように書く手もある。 by os0xさん

for (var i = 0; i < fruits.length; i++) {
    (function(fruit){
        window.setTimeout(function() { alert(fruit); }, i * 1000);
    })(fruits[i]);
}

TOP

Bug 373518 – event.relatedTarget is never set when leaving popup

mouseover イベントの event.relatedTarget によってマウスがどこから来たか調べたり、 mouseout イベントの event.relatedTarget によってマウスがどこへ行くのかを調べることができる。ところが Firefox 3.0a3pre で試したところ、 popup 要素上で発生した mouseover/mouseout イベントの relatedTarget が null であるため、マウスがポップアップを離れてどこへ行ったのかを判別することができないという問題が発生した。 Firefox 2 では問題なく動作するので、おかしいなと思い Bugzilla へ登録しておいた。
Bug 373518 – event.relatedTarget is never set when leaving popup

ついでに MDC に DOM:event:Comparison of Event Targets – MDC という未完成記事があったので、 mouseover/mouseout に対する target と relatedTarget の違いについて加筆しておいた。

TOP

parseInt の落とし穴 - 8月発生の時限式バグ

何気なくScrapBookのbackupフォルダを覗いてみたところ、なぜか一番新しいバックアップファイルが7月31日に生成されたもので、8月以降およそ2週間まったくバックアップファイルが生成されていなかった。

これは何か怪しいと思い、バックアップ処理を細かく調べたところ、バックアップファイル名の日付が何日前かをチェックする処理において、なぜか本日生成されたばかりのバックアップファイルが200日以上前のものだと判断され、即座に削除の対象にされていた。
ScrapBook では Firefox 起動時に本日付のバックアップファイルが存在するかを確認し、もしなければ本日付のバックアップファイルを生成し、なおかつ古いバックアップファイルの削除処理を行うという仕様になっている。したがって、 Firefox を起動する度に本日付のバックアップファイル生成→本日付のバックアップファイル削除、という処理が8月以降ずっと繰り返されていたというわけだ。

ではなぜ8月以降、バックアップファイルが何日前かを算出する処理がうまくいかなかったのだろうか。OSの設定が狂った、夏時間特有の問題、2つのDateオブジェクトの減算で型変換がうまくいっていない等の原因を考えたが、色々追求した結果、parseInt(“08”) が 0 になることが原因だと判明した。

alert( parseInt("08") );    // 「0」が表示される

今まで parseInt というJavaScript の組み込みグローバル関数は、string 型で表現された整数を、number 型に変換するだけの単純なものだと思っていたが、実は string 型で表現された小数や8進数や16進数や文字列も変換可能である。その際、引数として渡した string 型の値をどのような形式で変換するのかは自動で識別されるため、先頭に0をつけている場合は8進数とみなされ、”08″ が 0 に変換されるのである。また、第二引数に基数 10 を指定することで思い通りに10進数として変換させることが可能である。

alert( parseInt("08", 10) );    // 「8」が表示される

この parseInt(“08”) が原因となるバックアップ処理に関するバグは、8月になると突如発生する時限式バグといえよう。運良く2週間ほどでバグに気づけたことが不幸中の幸いである。このバグを修正したバージョン (1.1.0.2) はすでにリリース済みである。

ところで、ScrapBook を世に公開して間もない、非常に初期のバージョンにて、2004年10月になると突如取り込んだデータが上書きされ続けるという大変恐ろしい時限式バグが発生したが、このときの原因も今回のバグの原因と通ずるものがあり、年・月・日を加算するときに型の自動変換に頼っていたためにバグが引き起こされていた。

var y = 2004;    // number
var m = "09";    // string
var d = 30;    // number
var ymd = y + m + d;
alert(ymd);    // 「20040930」が表示される

9月30日までは、数値 9 を 文字列 “09” にして加算をしていたため、 2004 + “09” は “200409” になってうまくいっていた。

var y = 2004;    // number
var m = 10;    // number
var d = "01";    // string
var ymd = y + m + d;
alert(ymd);    // 「201401」が表示される

しかし、10月1日になると、数値 10 は文字列に変換せずに加算していたため、 2004 + 10 は 2014 になってしまった。

var y = 2004;    // number
var m = 10;    // number
var d = "01";    // string
var ymd = y.toString() + m.toString() + d.toString();
alert(ymd);    // 「20041001」が表示される

数値か文字列かわからないような値同士を加算するときには、必ず toString や parseInt を使って正しい型に変換してから加算をしなければならない。

parseInt のリファレンス:
Core JavaScript 1.5 Reference:Global Functions:parseInt – MDC

TOP

DOMノードのアイソレート

ScrapBook の DOMイレーサー という機能はページ中のクリックした箇所を削除する機能であるが、右クリックによってクリックした箇所以外を削除するという裏技的な機能も有する。この機能を DOMアイソレータ と呼んでいる。
これまでのバージョンでは DOMアイソレータ の仕様は、クリックした箇所のノードを body の直下(つまり body.firstChild の位置)へ insertBefore し、 body の firstChild 以外の全ての childNode を removeChild するという処理の流れになっている。わかりやすく言えば、クリックした箇所のノードを切り離してトップレベルに持ってきて、それ以外を削除するということである。

しかし、このような処理ではCSSとの整合性が悪くなり、ページの見た目が崩れやすい問題があった。そこで、ScrapBook 1.0.12 (Build ID 20060625) では、body ~ クリックした箇所のノードまでの道は残して、道から外れている不要なノードを削除するような処理へと改変した。以下はその部分の処理を抜粋した関数である。

function isolateNode(aNode)
{
    if ( !aNode || !aNode.ownerDocument.body ) return;
    while ( aNode != aNode.ownerDocument.body )
    {
        var parent = aNode.parentNode;
        var child = parent.lastChild;
        while ( child )
        {
            dump((child == aNode ? "o" : "x") + " " + parent.nodeName + " " + child.nodeName + "
");
            var prevChild = child.previousSibling;
            if ( child != aNode ) parent.removeChild(child);
            // 前のノードへ移動
            child = prevChild;
        }
        // 親ノードへ移動
        aNode = parent;
    }
}

TOP

非同期ループ処理 (7) - 同期非同期複合型

これまで述べてきた非同期ループ処理の問題点として、 setTimeout の間隔を0ミリ秒にしたところで、全体としては単純な for ループよりもかなり時間がかかってしまうことである。

そこで、 setTimeout しない同期的なループ処理も組み合わせて適度に高速化を図る。
下記の例では3回の処理を一単位としている。

var syncAsyncProcessor = {

    _array : [],
    _count : -1,

    start : function(aArray)
    {
        // 開始処理
        dump("start
");
        // 初期化
        this._count = -1;
        this._array = aArray;
        this._next();
    },

    _next : function()
    {
        var elt = this._array.shift();
        if ( elt ) {
            if ( ++this._count % 3 == 0 )
                // 数回に一度、非同期
                setTimeout(function(){ syncAsyncProcessor._process(elt); }, 1000);
            else
                // それ以外は同期
                syncAsyncProcessor._process(elt);
        } else {
            setTimeout(function(){ syncAsyncProcessor._finish(); }, 1000);
        }
    },

    _process : function(aElt)
    {
        // 処理
        dump("processing (" + this._count + ")... " + aElt + "
");
        // 次の処理へ
        this._next();
    },

    _finish : function()
    {
        // 終了処理
        dump("finish
");
    },

};

syncAsyncProcessor.start(['A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z']);

TOP

非同期ループ処理 (6) - 列挙型

配列でなく列挙型でも同じようなことができる。
下記の例ではブックマークのデータソースから全リソースを取り出し、リソースに対してなんらかの処理を行う。

var asyncEnumProcessor = {

    _enumerator : null,

    start : function(aEnumerator)
    {
        // 開始処理
        dump("start
");
        // 初期化
        this._enumerator = aEnumerator;
        this._next();
    },

    _next : function()
    {
        if ( this._enumerator.hasMoreElements() ) {
            var elt = this._enumerator.getNext();
            setTimeout(function(){ asyncEnumProcessor._process(elt); }, 0);
        } else {
            setTimeout(function(){ asyncEnumProcessor._finish(); }, 0);
        }
    },

    _process : function(aElt)
    {
        aElt.QueryInterface(Components.interfaces.nsIRDFResource);
        // 処理
        dump("processing... " + aElt.Value + "
");
        // 次の処理へ
        this._next();
    },

    _finish : function()
    {
        // 終了処理
        dump("finish
");
    },

};

var RDF_SVC = Components.classes['@mozilla.org/rdf/rdf-service;1'].getService(Components.interfaces.nsIRDFService);
var dataSource = RDF_SVC.GetDataSource("rdf:bookmarks");
var resourceEnum = dataSource.GetAllResources();
asyncEnumProcessor.start(resourceEnum);

TOP

非同期ループ処理 (5) - 進捗表示2

非同期ループ処理 (4) と似ているが、配列から shift して要素を取り出すのではなく、配列全体を保持しつつ位置 _index を加算しながら要素を取り出している。この方法でもプログレスバーなどで進捗状況を表示できる。

var asyncProgressiveProcessor2 = {

    _index : -1,
    _array : [],

    start : function(aArray)
    {
        // 開始処理
        dump("start
");
        // 初期化
        this._array = aArray;
        this._index = -1;
        this._next();
    },

    _next : function()
    {
        if ( ++this._index < this._array.length ) {
            setTimeout(function(){ asyncProgressiveProcessor2._process(); }, 500);
        } else {
            setTimeout(function(){ asyncProgressiveProcessor2._finish(); }, 500);
        }
    },

    _process : function()
    {
        var elt = this._array[this._index];
        // 処理
        dump("processing (" + (this._index+1) + "/" + this._array.length + ")... " + elt + "
");
        // 次の処理へ
        this._next();
    },

    _finish : function()
    {
        // 終了処理
        dump("finish
");
    },

};

asyncProgressiveProcessor2.start(['A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z']);

TOP