同じコードを書いてもらって解説 Re: Web/JS part.4

JavaScript 初心者向けに開催している Web/JS 講習会で課題を出し、JavaScript 皆に同じ課題でコードを書いて貰いました。
そのお悩みポイントに対して補足を書いてみます。

  • 課題内容
  • イベントハンドラと Function Call 演算子の付けどころ
  • window.onload = func に引数を付けたい
  • var 忘れでハマる

課題内容

こんなコードを書いてもらいました。

/**
 * 課題:
 * JavaScript off だと「クマー!」になっている台詞が、on だと違う台詞になるスクリプトを組む
 *
 * ルール:
 * innerHTML 禁止
 * 制限時間目安 30 分
 * スライドを見てもいい
 * 本、資料を参考にしてもいい
 */
 
/**
 * window.onload に関数 func をスタックする
 */
function addOnLoad(func) {
    if (typeof window.onload == "function") {
        var predefined_onloadfunc = window.onload;
        window.onload = function() {
            predefined_onloadfunc();
            func();
        };
    } else {
        window.onload = func;
    }
};
 
/**
 * クマーに定義した台詞を言わせる
 */
function shout() {
    var serifu_array = new Array(
        "あばばばばばばー",
        "常に裏目に出るクマ",
        "明日できることは今日やらないクマ",
        "富樫が連載再開したら本気出すクマ",
        "これからが本当の地獄だクマ",
        "昨日買ったPCが今日壊れたクマ"
        );
    var serifu_index = Math.floor(Math.random()*serifu_array.length);
    var serifu = serifu_array[serifu_index];
    var new_serifu_textnode = document.createTextNode(serifu);
    var shout = document.getElementById("shout");
    shout.removeChild(shout.firstChild);
    shout.appendChild(new_serifu_textnode);
};
 
addOnLoad(shout);

イベントハンドラと Function Call 演算子の付けどころ

window.onload = func;

window.onload = function() {
	predefined_onloadfunc();
	func();
}

で、function call 演算子の付け間違いに苦しむという例があったようで、少し解説します。
window.onload はイベントハンドラです。
イベントハンドラとは、何かイベントが発生した(クリックした、ページのロードが終わった、マウスをポイントした)タイミングで実行される関数のエントリポイントです。
window.onload の初期値は undefined で、ここに関数を割り当ててやると window オブジェクト(ブラウザ)が onload (読み込みを終えた)タイミングでその関数が実行されます。

ポイントは、

  1. window.onload には「関数そのもの」をセットすること
  2. 次に関数の実行タイミングはお任せになっている(ブラウザのロードが完了した時に実行される)こと

です。

これを把握して動作順を丁寧に追うと混乱も少ないと思いますが、さてどうでしょうか。

window.onload = func に引数を付けたい

と思うのは当然!なのですが先に説明したように関数実行のタイミングはお任せになっているのでコード上で function call 演算子を用いて引数を渡す方法が使えません。

これを解決するには、JavaScript での関数の実行モデルについて知っている必要があります。
もう少し先のフォローアップ講習会で扱います。なので今教わっている知識の範囲外です。
其の上で敢えて解説を。

クロージャ(closure)を使います。

function makeShout(word) {
	return function() {
		document.getElementById('shout').innerHTML = word;
	};
};
addOnLoad(shout("ご飯三杯はいける!"));

関数内で変数を取り回すときにスコープは意識していると思います。何かのオブジェクトが今宣言した変数を管理している訳です。この「何か」を環境と呼びましょう。
上記例では、この環境に変数 word を追加して、それとひとくくりになった関数を返し window.onload にセットしています。
JavaScript では関数と環境は常にセットになっており、環境を指定して実行したり、あらかじめバインディングしておく事が出来ます。

今聞くとややこしく聞こえるかも知れませんが、JavaScript の仕様を理解しているとこれはシンプルです。
今後のフォローアップ講習会で詳しく説明して行きますので参加者の皆様は乞うご期待!

var 忘れでハマる

変数宣言で var を付け忘れ、ハマった例がありました。

    var predefined_onloadfunc = window.onload;

これを下記のように間違えると3回目の addOnLoad 呼び出しで too much recursion になって死にます。

    predefined_onloadfunc = window.onload;

他の言語でも同じ事ですが、ローカル変数は関数が実行される都度初期化されます。
addOnLoad は既に onload にセットされている関数があれば新しく関数を作成し、その関数のプロパティに window.onload に登録されている関数の参照を作成し、新しい関数の中でそれを呼び出します。
addOnLoad が実行される度に新しく環境が作成されていくのがポイントです。スタックが4重であれば、4つの環境が存在し、それぞれで関数への参照が数珠つなぎのようにチェーンされている訳です。

間違いコードではこれを関数ローカルの環境ではなくグローバルな環境で管理しています。
これでどうなるかというと、既に onload にセットされている関数があれば新しく関数を作成し、グローバルオブジェクトのプロパティAに window.onload に登録されている関数の参照を作成し、新しい関数の中でそれを呼び出しています。
こうすると新しく作成された関数の中には常にAの呼び出しが含まれるため、2回目の addOnLoad で作成された関数オブジェクトに含まれる呼び出しと3回目のものとが相互に呼び合って無限ループに陥ります。

コメント / トラックバックはありません

コメントする