ブログが続かないわけ

この日記のはてなブックマーク数
Webエンジニアが思うこと by junichiro on Facebook

[JavaScript]jQuery のイベントとクロージャ

このエントリーを含むはてなブックマーク hateb

以前のエントリ([JavaScript]僕、スコープとかクロージャとか曖昧でした )で書いた、Javascript のクロージャの話と全く同じことなのですが、あれをjQuery のclick イベントで書くとどうなるかということを、自分用にメモとして残しておきたいと思います。

例えば、div でブロックを3つ作って、それぞれにclick イベントを持たせるとします。そのとき、そのclick に反応して、そのblock が何番目に作られたものなのかをalert するようにしたいとします。

jQuery の.click は第一引数にコールバック関数をとるので、クロージャのことを意識せずに直感的に書くと、次のようなコードになるのではないでしょうか。

var div;
var i;
for(i=0; i<3; i++) {
    div = $('<div/>').click( function( e ) {
        alert(i)
    } );
    div.html('click me - ' + i); 
    div.css('cursor', 'pointer');
    $('body').append(div);
}

普段からJavascript を書いているかたであれば、すぐに問題点に気付くと思います。これは例の通り、click me - [i] のところのi は正しくその番号のものが表示されるのですが、実際にそれをクリックしてみても、alert で表示されるi は目的のものとは違うはずです。ここでも前回見た通り、「関数を返す無名関数をすぐに実行する」という方法で対応できます。

考え方としては、まず、「関数を返す無名関数を『すぐに実行する』」というところが重要なので、.click の引数にあたるところで、関数をすぐに実行させます。順を追って書くと、まずこんな感じになります。

.click( function () {} () );

当然、このfunction はすぐに実行されるのですが、.click は引数にコールバック関数をとるものなので、この実行される関数がそのコールバック関数を返さなければなりません。つまり、こう書くことになります。

.click( function () {
    return function() {};
}() );

そして、その中に書かれた返されるコールバック関数のところに目的のコードを書くことになります。

.click( function () {
    return function() {
        alert(_i);
    };
}() );

ここで、alert(i) と書いたのですが、このi は外側のfor のi とは別物ということをはっきりさせるためにi と書くことにしました。つぎに、このi に外側のfor でまわされているi を渡したすというのを表現します。

.click( function (i) {
    var _i = i;
    return function() {
        alert(_i);
    };
}(i) );

すぐに実行される無名関数に、引数として i をとれるようにしました。ここでは、紛らわしくないように別の変数名(例えばn)を使ってつぎように書くこともできます。この方がi の違いを区別しやすくなるので、わかりやすいというかたもいらっしゃるかもしれません。

.click( function (n) {
    var _i = n;
    return function() {
        alert(_i);
    };
}(i) );

これで目的のclick イベントは完成です。jQuery のイベントに渡すコールバック関数では、引数にevent そのものをとることができます。このような書き方をした場合、それはこの内側に書いたコールバック関数で受取れます。次のe のような書き方です(今回の例ではこのe を使うことはありませんが)。

.click( function (n) {
    var _i = n;
    return function(e) {
        alert(_i);
    };
}(i) );

以上を踏まえて、最初に書いたものを書き直すと、以下のようになります。

var div;
var i
for(i=0; i<3; i++) {
    div = $('<div/>') .click( function( i ) {
        var _i = i;
        return function(e) {
            alert(_i);
        };
    }(i) );
    div.html('click me - ' + i);
    div.css('cursor', 'pointer');
    $('body').append(div);
}

僕もまだまだ初学者の域をでておりませんので、上記のような思考のもとに書いております。

[JavaScript]僕、スコープとかクロージャとか曖昧でした

このエントリーを含むはてなブックマーク hateb

JavaScript: The Good Parts を読みましたところ、いろいろと勉強になることがありました。

Douglas Crockford, 水野 貴明 ¥ 1,890
手っ取り早く習得
痒いとこだけ掻いてくれる
JavaScriptを勉強しなおすのにとってもよいです。
JavaScript コアに関する最高の本

付録を除くと100ページちょいという、とてもライトな本ですが、内容は濃密です。JavaScript の中で、一番わかりにくいだろうなと思われるようなところが集中的に解説されているように感じられ、勉強したてでかつそろそろわかりかけてきたなーと思い始めていた僕にとって、最適の一冊でした。知って良かったなと思えるところがたくさんありましたので、これから少しずつ紹介して行きたいと思いますが、今日はその中でも特に印象的だった、変数のスコープにまつわるお話をしてみたいと思います。

変数のスコープはその変数が宣言された関数の中だけに限定されます。なんとなく知ってはいたのですが、for 文とかではこういう風に書いてしまうことが多々ありました。

for (var i=0; i<array.length; i++) {
    // alert('test: ' + i);
}
ブロックレベルのスコープはないので、これは下記と同じことなんですね。
var i;
for (i=0; i<array.length; i++) {
    // alert('test: ' + i);
}
変数宣言はそのスコープをできるだけ狭める方がお行儀が良いので、他のプログラミング言語ではなるべく内側のブロック、例えばさっきの例ではfor 文のブロック内で宣言するのがよいとされているのですが、JavaScript では、関数内の先頭に書くのが一番わかりやすくなります。そうすれば、その関数全体がスコープになるというのが直感的にわかるようになると思います。

これを踏まえた問題をひとつ。これも、この本からの引用なので恐縮です。 下記のようなhtml が与えられたときに、h1 の「JavaScript」 をクリックしたら「1」を、h2 の「The Good Parts」 をクリックしたら「2」をalert するというような、そのnode が何番目のnode なのかをalert するというコードを考えるとします。

<?xml version="1.0" encoding="UTF-8" ?><!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<link href="good_parts.css" rel="stylesheet" type="text/css" />
<title>JavaScript The Good Parts</title>
</head>
<body>
<h1>JavaScript</h1><h2>The Good Parts</h2><pre><script src="program.js"></script></pre>
</body>
</html>
【参考】 0番目のnode: body タグの直後の改行(テキストノード) 1番目のnode: h1 2番目のnode: h2 3番目のnode: pre 4番目のnode: pre タグの直後の改行(テキストノード)

program.js

var add_the_handlers = function (nodes) {
    var i;
    for (i = 0; i < nodes.length; i += 1) {
        nodes[i].onclick = function () {
            alert(i);
        }
    }
};
add_the_handlers(document.body.childNodes);
これは間違いの例です。僕はパッと見ただけではこのコードの問題点がわかりませんでした。 実は、上記のjs では、どちらをクリックしても4 がalert されてしまいます。4 というのはノードの総数です。

これはクロージャの概念と変数のスコープがわかっていればわかることらしいので、本では詳細が書かれておりませんでした。そこで、僕なりの解釈を付け加えてみたいと思います。このfor 文のi=1 の時には、「1番目のnode(つまりh1) のonclick イベントに function() { alert(i); } を割り当てろ」という命令がなされています。これで、h1 がonclick イベントに反応する準備ができました。そして、実際に、h1 がクリックされると、その際に実行されるのは、alert(i) です。では、このalert(i) が実行されたときに、i の値はどうなっているでしょうか。onclick を割り当てたタイミングでは1 だったかもしれないi の値ですが、実際にはにfor 文がnodes の総数分実行されたあとなので、i = 4 になってしまっています。その結果、alert(i) で、「4」がalert されてしまうということになります。

では、どのように直せばよいのでしょうか。

var add_the_handlers = function (nodes) {
    var i;
    for (i = 0; i < nodes.length; i += 1) {
        nodes[i].onclick = function (i) {
            return function () {
                alert(i);
            }
        }(i);
    }
};
add_the_handlers(document.body.childNodes);
このように、「関数を返す無名関数をすぐに実行する」という少しわかりづらいコードで、問題を回避できます。先ほどと同じように、for 文のi=1 の時に何が起きているか見てみたいと思います。i=1 の時には、「h1 のonclick イベントに、function(i) { // [function を返すコード] }(i) の実行結果(戻り値が関数)を割り当てろ」という命令がなされています。具体的に言うと、このfunction(i){ }(i) はこのタイミングで直ちに実行されますので、戻ってくる関数は「funcion() { alert(1); }」となります(i=1 なのでalert の中身が1 になる)。つまり、for 文のi=1 の時には、「h1 のonclick イベントにfunction() { alert(1); } を割り当てろ」という命令がなされていることになります。i=1 以外の時も同様で、onclick イベントに割り当てるタイミング(つまりfor 文がまわってるとき)で、その時点でのi を処理してしまっていることが最大のポイントです。その結果、実際にh1 のnode がクリックされると、今度は正しくalert(1) が実行されるようになります。

言葉で書くと返ってわかりづらく見えてしまいましたが、そういうことです。 この、無名関数を定義して、すぐに実行するという、function(){}(); というのは結構便利なので、覚えておくと良いかもしれません。

[JavaScript]今度はボードゲームのダイヤモンドを作った

このエントリーを含むはてなブックマーク hateb
js 特訓月なので、js をいろいろ触っているのだが、今度はダイヤモンドという人気のボードゲームを実装してみた。本来のダイヤモンドは、ダイヤモンドの眠る危険な洞窟に、これ以上奥まで行くか、はたまたここで帰るのかというジレンマを、周囲のみんなを牽制し合いながら楽しむという非常にスリリングなのものなのだが、その大事な部分をばっさりとカットして、今回は1人用のダイヤモンドにした。

奥深くまで行く方がメリットが出るようにアレンジし、ひとりでも楽しめるようにしたのだが、まあ、評判は悪い。
11:10 mizzu_1: もうでけたのか
11:10 mizzu_1: ああ
11:10 junichiro: ひとり用だもの
11:10 mizzu_1: 1人プレイかい
(中略)
11:13 mizzu_1: 坊主めくりじゃんって
11:13 mizzu_1: 気がしちゃうんです

【それでも遊びたい方はこちら→】Diamond
僕は、最高で259個のダイヤモンドを持ち帰ったことがある。

diamond/index.html
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<link rel="stylesheet" href="../css/diamond.css">
<script src="../lib/jquery/jquery-1.3.2.min.js" type="text/javascript"></script>
<script src="../lib/diamond.js" type="text/javascript"></script>
<title>Diamond</title>
</head>
<body>
<h1>Diamond</h1>
<div id="status">
<span class="label">現在のダイヤモンド: </span>
<span class="value"><span id="count">0</span>個</span>
<span class="label">危険度: </span>
<span class="value"><span id="danger">0</span>%</span>
<span class="label">残り枚数: </span>
<span class="value"><span id="left">30</span>枚</span>
</div>
<div id="field"></div>
<div id="action">
<input type="button" id="go" value="Go!" />
<input type="button" id="return" value="Return..." />
</div>
<div>
<h3>危険度によるブースト</h3>
<ul>
<li> 0〜10% - そのまま</li>
<li>10〜20% - 2倍</li>
<li>20〜30% - 4倍</li>
<li>30〜40% - 8倍</li>
<li>40% 以上 - 20倍</li>
</ul>
</div>
</body>
</html>

lib/diamond.js
if( typeof( window.Diamond ) == "undefined") {

// Diamond コンストラクタ
var Diamond = function() { return this; };

// Version
Diamond.VERSION = '0.01';

// Diamond オブジェクトのプロパティ(デフォルト値)
Diamond.prototype.cgiurl = 'diamond.cgi';
Diamond.prototype.cards = [
2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,
'Sneak', 'Sneak', 'Sneak',
'Explosion', 'Explosion', 'Explosion',
'Gas', 'Gas', 'Gas',
'Scorpion', 'Scorpion', 'Scorpion',
'Rock', 'Rock', 'Rock',
];

Diamond.prototype.bads = {
"Sneak":0, "Explosion":1, "Gas":2, "Scorpion":3, "Rock":4
}

Diamond.prototype.potential = new Array();
Diamond.prototype.accident = new Array();
for(var i = 0; i<5; i++) {
Diamond.prototype.potential[i] = 3;
Diamond.prototype.accident[i] = 0;
}

Diamond.prototype.diamond = 0;
Diamond.prototype.left = 30;

Diamond.prototype.init = function() {
var __this = this;
__this.cards = [
2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,
'Sneak', 'Sneak', 'Sneak',
'Explosion', 'Explosion', 'Explosion',
'Gas', 'Gas', 'Gas',
'Scorpion', 'Scorpion', 'Scorpion',
'Rock', 'Rock', 'Rock',
];
$("#go").click(function(e) {
__this.go(e);
});
$("#return").click(function(e) {
__this.go_home(e);
});
__this.diamond = 0;
__this.left = 30;
for(var i = 0; i<5; i++) {
__this.potential[i] = 3;
__this.accident[i] = 0;
}
__this.display();
}

Diamond.prototype.go = function( arg ) {
var __this = this;
var left = __this.cards.length;
var draw_sub = Math.floor( Math.random() * left );
var draw = __this.cards[draw_sub];
__this.add_field( draw );
var new_cards = new Array();
for( var i = 0; i<left; i++ ) {
if( i == draw_sub ) continue;
new_cards.push(__this.cards[i]);
}
__this.cards = new_cards;
if(typeof draw == 'string') { // accident!
var sub = __this.bads[draw];
__this.potential[sub]--;
__this.accident[sub]++;
if(__this.is_finished()) {
__this.game_over();
};
}
if(typeof draw == 'number') { // diamond!
__this.diamond += draw * __this.boost();
}
__this.display();
}

Diamond.prototype.go_home = function( arg ) {
var __this = this;
alert("You are a chiken!¥nGo home!¥n¥nYou got " + __this.diamond + " diamonds!");
$("#go").unbind();
$("#return").unbind();
$("#action").empty();
}

Diamond.prototype.danger = function( arg ) {
var __this = this;
var dan = 0;
for(var i = 0; i<5; i++) {
if(__this.accident[i] == 1) dan += __this.potential[i];
}
var danger = Math.floor(dan * 100 / __this.cards.length * 10) / 10;
return danger;
}

Diamond.prototype.boost = function( arg ) {
var __this = this;
var danger = __this.danger();
if(danger < 10) return 1;
if(danger < 20) return 2;
if(danger < 30) return 4;
if(danger < 40) return 8;
return 20;
}

Diamond.prototype.is_finished = function( arg ) {
var __this = this;
for(var i = 0; i<5; i++) {
if(__this.accident[i] == 2) return true;
}
return false;
}

Diamond.prototype.display = function ( arg ) {
var __this = this;
$('#left').empty().append(__this.cards.length);
$('#count').empty().append(__this.diamond);
$('#danger').empty().append(__this.danger());
}


Diamond.prototype.game_over = function ( arg ) {
var __this = this;
alert("You got no diamond!¥nGAME OVER!");
$("#go").unbind();
$("#return").unbind();
$("#action").empty();
__this.diamond = 0;
__this.left = 30;
__this.init();
}


Diamond.prototype.add_field = function ( arg ) {
var div = document.createElement('div');
div.setAttribute('class', 'card');
var text = document.createTextNode(arg);
div.appendChild(text);
if(typeof arg == 'string'){
jQuery(div).css('background-color', '#FFDDDD');
}
$('#field').append( div );
if(typeof arg == 'string'){
var br = document.createElement('br');
jQuery(br).css('clear', 'both');
$('#field').append( br );
}
}

// リモート計算を行うメソッド
Diamond.prototype.expr = function ( arg ) {
// CGI に渡すパラメータオブジェクトを生成
var param = {
q: arg
};
// コールバック関数無いではthis が使えないのでコピー
var __this = this;
// JSON データを受取るコールバック関数
var func = function ( data ) {
if ( ! data ) return null;
if ( data.status != 'ok' ) data.result = '';
$(__this.result).text(data.result);
$(__this.status).text(data.status);
};
// CGI を呼び出して、JSON データを受取る
jQuery.getJSON( this.cgiurl, param, func );
}
}

// main
$(document).ready(function(){
var d = new Diamond();
$('#field').empty();
d.init();
});

AI を実装してNPC とプレイできるようにすればそこそこ盛り上がるとアドバイスをもらい、AI といえばjunichiro くらいのノリで行きたい僕としては、多いにモチベーションがあがったのだが、js の特訓という目的から大きく逸脱していることに気がついてやめた。

次のお勉強は、ドラッグアンドドロップとなんとなくうわついたフレームみたいなやつを学ぶ予定。それからちょっとしたビジュアル的ななにかを勉強して行きたい。ダイヤモンド のNPC のAI はその後だが、その頃には需要がなくなっていると予想。