theater.js実験場開設

サブアカウントid:theater-jsを取得して、theater.js iGoogleガジェット実験用ブログを開設しました。

本アカウント(ココ)は、技術的な目的で来られる方が多いので、重たいガジェットは不向きと思われますので。

興味があれば覗いてやってください。ご意見・ご感想もお待ちしております。(こんなん作って、でもいいです。)

iGoogleガジェット実験

theater.jsをiGoogleガジェットにしてみました。

(2007/09/04 サブアカウントのページに移しました。)
http://d.hatena.ne.jp/theater-js/20070903

おおお。動きました。すごい。(←自分で言うな)

YahooからRSS取得して表示してます。画像は記事ページから無理やり取得。でも少ないです・・・。写真のURLも載っているニュースRSSがあれば楽なんですが。誰かご存じないですか?^^;

一度クリックすると自動で表示していきます。最後までいくと最初に戻って繰り返します。

なんでかIEだとエラーが発生します。COMPLETEDの文字も消えないし。原因不明ですorz。動作には問題ないのですが・・・。

→追記。あれ、違う環境だとIEでも発生しない。。。分からんな><;

→8/22 さらに追記。昨日問題なかった環境でも今日はエラーがでてます;; 初回読み込み時には問題なく、リロードすると発生します。キャッシュを消すと解消します。うーん。画像先読みがなにか悪さしてるかな・・・。

→8/24 発生しなくなりました・・・。原因掴めず。まいったな・・・。
→8/26 いちおう対処した・・・つもり。単に怪しい部分をtry-catch節で囲んだだけですが^^;  やっぱりはっきりしないんですよね・・・。

とにかく、これで「はてな」でもtheater.jsを使ってゲームが作れます。いいかげん、コンテンツを作らないといけないと思っていたので、いいタイミングです^^

v2.0.2公開

このまえv2.0.1を公開したばかりですが・・・。

readme等をつけただけです。機能的には変更ありません。

やっとVectorにソフトウェア更新申請ができました^^;

今後は、バグフィックスを除いて正式版のバージョンをしばらく凍結し、機能追加はベータ版として公開していきたいと考えています。

v2.0.1公開

先ほど不具合が発見されたため、修正対応版を公開しました。→ダウンロード

  • 事象:playColdメソッドの引数にグローバルエリアに定義されたシナリオ関数配列の変数名を指定するとエラーとなる場合がある。
  • 原因:プログラムミス。evalでシナリオ関数配列変数名を処理する場合のtry catch漏れ。
  • 対応:try catch節の追加。

そもそもtheater.jsとは・・・

私が作成・公開している、JavaScriptアドベンチャーゲームを作成するためのフレームワークです。→theater.jsのサイト

現状と今後の展開

サイトでの解説がようやく完成しつつあります。(あと上級編を残すのみ。遅すぎるという噂もありますが・・・) 解説の作成が終了したら、v2.0.1もVectorに公開する予定です。その後、改修を加えつつ、ゲームを自作・・・したいなぁと思っています。。。

本当は使っていただけるのが一番嬉しいわけですが。ちょっとマニアックになりすぎたかもしれませんね;;

ご質問、ご意見、ご感想をお待ちしております。(ほんとに^^;)

クロージャとレキシカルスコープ

自分なりのまとめです。

【他、参考となるサイト】
http://d.hatena.ne.jp/keyword/%a5%af%a5%ed%a1%bc%a5%b8%a5%e3
http://www.atmarkit.co.jp/fdotnet/ajaxjs/ajaxjs03/ajaxjs03_03.html
http://www.atmarkit.co.jp/fdotnet/ajaxjs/ajaxjs03/ajaxjs03_04.html

内部関数

関数内で更に関数を定義することができます。

function test(test_str){
  function sub(sub_str){
    alert(sub_str);
  }
  sub("TEST:" + test_str);
}
test("!!!"); //「TEST:!!!」と表示される。
//←ここでsub("!!!");とはできない。

関数subをこのように定義すれば、親関数test内でしか使わないことがはっきりしますし、名前の競合も防ぎやすくなります。

クロージャ

内部関数とクロージャはほぼ同義なんですが、ここでは分けてみました。親関数の外に参照が渡される内部関数をクロージャと呼ぶことにします。→すみません、以降、内部関数もすべてクロージャと記述させてください。

「親関数の外に参照が渡される」とは、具体的には

  • 返却値
  • 関数呼び出しの引数
  • (グローバル等の)親スコープ変数への格納
  • 既存オブジェクトのメンバとして登録*1

等になります。

var g_sub;
function other(a_sub){
  a_sub("ARG"); //「TEST:ARG」と表示される。
}
function test(){
  function sub(sub_str){
    alert("TEST:" + sub_str);
  }
  other(sub); //引数
  g_sub = sub; //グローバル変数へ格納
  return sub; //返却
}
var r_sub = test();
r_sub("RET"); //「TEST:RET」と表示される。
g_sub("GLB"); //「TEST:GLB」と表示される。

注目してほしいのは、関数testの実行終了後も、関数subが有効であることです。

レキシカルスコープ

親関数の変数・仮引数を使用可能

クロージャは、親関数の変数・仮引数を使用することができます。

var g_sub;
function other(a_sub){
  a_sub("ARG"); //「TEST:0:ARG」と表示される。
}
function test(test_str){
  var test_count = 0;
  function sub(sub_str){
    alert(test_str + ":" + test_count + ":" + sub_str);
  }
  other(sub); //引数
  g_sub = sub; //グローバル変数へ格納
  return sub; //返却
}
var r_sub = test("TEST");
r_sub("RET"); //「TEST:0:RET」と表示される。
g_sub("GLB"); //「TEST:0:GLB」と表示される。

ここで注目してほしいのは、関数testの実行終了後も、その変数・仮引数が有効であるということです。このスコープ特性をレキシカルスコープといいます。外に渡されたクロージャへの参照が保持される間、その親関数の変数・仮引数も保持されます。

親関数の変数・仮引数は、その実行毎に保持

上記の例で、関数subにtest_countをカウントアップする処理を追加してみます。

var g_sub;
function other(a_sub){
  a_sub("ARG"); //「TEST:1:ARG」と表示される。
}
function test(test_str){
  var test_count = 0;
  function sub(sub_str){
    test_count++; //カウントアップ
    alert(test_str + ":" + test_count + ":" + sub_str);
  }
  other(sub); //引数
  g_sub = sub; //グローバル変数へ格納
  return sub; //返却
}
var r_sub = test("TEST");
r_sub("RET"); //「TEST:2:RET」と表示される。
g_sub("GLB"); //「TEST:3:GLB」と表示される。

カウントアップされるのが分かると思います。つまり、クロージャ間で親関数の変数・仮引数は共有されているのです。

ここで、test関数を2回実行してみます。

var g_sub;
function other(a_sub){
  a_sub("ARG"); 
}
function test(test_str){
  var test_count = 0;
  function sub(sub_str){
    test_count++; //カウントアップ
    alert(test_str + ":" + test_count + ":" + sub_str);
  }
  other(sub); //引数
  g_sub = sub; //グローバル変数へ格納
  return sub; //返却
}
var r_sub = test("TEST");
r_sub("RET"); 
g_sub("GLB"); 
//もう1回
var r_sub_m = test("TEST_M");
r_sub_m("RET"); 
g_sub("GLB"); //g_subを3回実行
g_sub("GLB"); 
g_sub("GLB"); 
//初回に返却されたクロージャ
r_sub("RET");

実行結果は以下です。

以下が順番に表示される。
TEST:1:ARG
TEST:2:RET
TEST:3:GLB
TEST_M:1:ARG
TEST_M:2:RET
TEST_M:3:GLB
TEST_M:4:GLB
TEST_M:5:GLB
TEST:4:RET

実行一回目のクロージャと、実行二回目のクロージャで変数が共有されていないことが分かります。つまり、親関数の実行毎に変数・仮引数が保持されているということです。

クロージャはグローバル(windowオブジェクトのメンバ)として実行される

クロージャをそのまま実行すると、グローバル(windowオブジェクトのメンバ)として実行されます。このため、親関数内のthisとクロージャ内のthisは意味が異なります。

<button id="button">TEST</button>
<script>
var button = document.getElementById("button");
function test(){
  alert(this.id); //「button」と表示される。
  alert((this == button)); //「true」と表示される。
  function sub(){
    alert(this.id); //「undefined」と表示される。
    alert((this == window)); //「true」と表示される。
  }
  sub();
} 
button.onclick = test;
</script>

要素のonclickプロパティに登録された関数は、イベント発生時、要素のメンバとして実行されます。(thisが要素を意味する。) クロージャで親関数のthisを参照したい場合は、以下のように親関数内でthisを変数に格納して参照します。

<button id="button">TEST</button>
<script>
var button = document.getElementById("button");
function test(){
  alert(this.id); //「button」と表示される。
  alert((this == button)); //「true」と表示される。
  var _this = this; //変数にthisを格納。
  function sub(){
    alert(_this.id); //「button」と表示される。
    alert((_this == window)); //「false」と表示される。
  }
  sub();
} 
button.onclick = test;
</script>

または、クロージャをapply/callメソッドを使用して呼び出すことでも可能です。

<button id="button">TEST</button>
<script>
var button = document.getElementById("button");
function test(){
  alert(this.id); //「button」と表示される。
  alert((this == button)); //「true」と表示される。
  function sub(){
    alert(this.id); //「button」と表示される。
    alert((this == window)); //「false」と表示される。
  }
  sub.apply(this);
} 
button.onclick = test;
</script>

呼び出しにオブジェクトを必要としない分、前者のほうが汎用性は高いと思います。

クロージャは他のコードより優先して生成される(※例外あり、後述)

問題です。以下のalertは何が表示されるでしょう。

function test(){
  var count = 0;
  count++;
  function sub(){
    alert(count); //???
  }
  count++;
  return sub;
} 
var retFunc = test();
retFunc();

親関数の変数、およびそれを使った処理が、クロージャの前にあるか後ろにあるかは関係ありません。問題はクロージャ実行時にどうなっているかです。上記例の場合は、return後の状態になります。なので、答えは「2」です。

・・・というか、クロージャの親関数内での位置には意味がありません。コメントいただいて気が付いたのですが

JSではfunctionやvarによる宣言部分(変数の初期化代入は宣言に含まれません)は、スコープ内で他のコードよりも先に処理される

のですね。

function test(){
  var str = "TEST";
  return sub;
  function sub(){
    alert(str);
  }
} 
var retFunc = test();
retFunc(); //「TEST」と表示される
例外:クロージャ(関数)が式に組み込まれている場合

ただし、クロージャが式に組み込まれている場合は少し話が違います。以下の実行結果は?

function test(){
  var str = "TEST";
  return sub; 
  var wrap = function sub(){
    alert(str);
  };
} 
var retFunc = test();
retFunc();

正解は、「ブラウザによって異なる」です・・・。

  • IE:「TEST」と表示される。
  • Firefox:3行目でエラー。(sub is not defined)
  • Opera:3行目でエラー。(Reference to undefined variable: sub)

あとで気が付いたのですが、FirefoxOperaでは、subが有効なスコープはその関数の中だけになるようです。IEについては、あらかじめ生成した上、実行時にも生成しているような気がします。

*2

例1

過去に回答した質問question:1180098481を題材にさせていただきます。

問題です。次のコードの最終2行実行時、何が表示されるでしょう。

var a = new Array();
function f(){
  for(var i = 0;i < 10;i++){
    a[i] = function(){
      window.alert(i);
    };
  }
}
f();
a[0](); //???
a[9](); //???

「0」と「9」、と思えるかもしれません。実際、質問者の方もそう思われたのです。しかし、すでに挙げたクロージャの性質、

  • クロージャの親関数の変数・仮引数は、その実行毎に保持される

という点や、今までの例を踏まえれば、2回とも「10」が表示される、ということがご理解いただけると思います。

ここでさらに問題。配列aに格納されているクロージャは、それぞれ同じものでしょうか、それとも別のものでしょうか。

var a = new Array();
function f(){
  for(var i = 0;i < 10;i++){
    a[i] = function(){
      window.alert(i);
    };
  }
}
f();
alert((a[0] == a[9])); // true or false ?

正解はfalseです。別なんですね。前述したように、式に組み込まれているクロージャは、その実行時に生成されているのです。これはIEでもFirefoxOperaでも同じです。ただ、使用している変数が同じなので、同じ挙動になるのです。

さて、それぞれ違う値を表示する関数を配列aに登録するにはどうしたらよいのでしょう。

var a = new Array();
function f(){
  function make(j){
    return function(){
      window.alert(j);
    };
  }
  for(var i = 0; i < 10; i++){
    a[i] = make(i);
  }
}
f();
a[0]();
a[9]();

上記は私の回答です。ちょっとややこしいですね^^; クロージャを返すクロージャmakeを作成して、for文で実行しています。こうすることで、make実行毎に仮引数jが保持されるので、配列aの関数は、それぞれ違う挙動になるのです。

例2

クロージャの威力がよく発揮されるシーンとして、タイマーイベントや要素のイベントへの関数登録があります。

<html>
<head>
<script>
function test(id){
  var elem = document.getElementById(id);
  var count = 0;
  elem.onclick = start;
  var tid = null;
  
  function start(){
    tid = setInterval(countup, 1000);
    elem.onclick = stop;
  }
  function countup(){
    elem.innerHTML = count;
    count++;
  }
  function stop(){
    clearInterval(tid);
    elem.onclick = start;
  }
}

window.onload = function(){
  test("div1");
  test("div2");
}
</script>
</head>
<body>
それぞれクリックするとスタート←→ストップが切り替わります。
<div id="div1" style="border:solid blue 2px;background-color:#ccccff">クリックするとスタート</div>
<div id="div2" style="border:solid red 2px;background-color:#ffcccc">クリックするとスタート</div>
</body>
</html>

青い枠のDIVと赤い枠のDIVがあります。それぞれクリックすると、1秒ごとに数値をカウントアップして表示します。もう一度クリックすると停止し、更にクリックすると再開します。それぞれの数値は独立しています。

この機能を、関数testのみで実装しています。タイマー識別子やカウンタをグローバルエリアに持つ必要はありません。また、表示要素が増えたとしても、その分関数testを呼び出せばいいだけです。関数testに改変は必要ありません。こんなことができるのも、レキシカルスコープの特性があるからです。

おわりに

レキシカルスコープは、「prototype.js解読」のときにEnumerableクラスのeachメソッドで出くわして、散々悩みました^^; 経験言語(CやJava)にはない話だったので・・・。でも、いちど理解できると、JavaScriptが非常に効率よく組めるようになり、楽しくなった覚えがあります。この記事が、みなさんのJavaScript効率化の一助になれば幸いです。

・・・また、間違いがありましたらコメント等でご指摘くださいますよう、お願いいたします。。。m_ _m

*1:7/23 追記しました。以降の例では取り上げてません^^;

*2:以前、グローバルエリアで関数定義を後にしたら参照できないという、間違った内容をここに書いて、コメントで指摘いただきました。

Ajax.Requestでのフォームデータ送信サンプル(postBodyへの設定)

Ajax.Requestでのフォームデータ送信サンプルを載せておきます。ポイントはForm.serializeメソッドです。これがフォーム内容をpostBodyプロパティに適合した文字列に変換してくれます。

[test.htm]UTF-8で記述
<html>
<head>
<title></title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<script language="javascript" src="./prototype.js" charset="utf-8"></script>
<script language="javascript">
function test(fm){
  new Ajax.Request('test.jsp', {
    method : 'post',
    postBody : Form.serialize(fm),//Form.serializeメソッドを使う
    onComplete : function(req){
      $('test').innerHTML = req.responseText;
    }
  });
}
</script>
</head>
<body>
<form>
<input type="text" name="TEXT" value="テキストボックス"/>
<textarea name="TEXTAREA">テキストエリア</textarea>
<input type="checkbox" name="CHECKBOX" value="CHECKED!" checked/>
<input type="radio" name="RADIO" value="RADIO1" checked/>
<input type="radio" name="RADIO" value="RADIO2"/>
<select name="SELECT">
  <option value="OPTION1">オプション1</option>
  <option value="OPTION2" selected>オプション2</option>
  <option value="OPTION3">オプション3</option>
  <option value="OPTION4">オプション4</option>
</select>
<select size="3" name="SELECTSIZE" multiple>
  <option value="OPTION1" selected>オプション1</option>
  <option value="OPTION2">オプション2</option>
  <option value="OPTION3" selected>オプション3</option>
  <option value="OPTION4">オプション4</option>
</select>
<button onclick="test(this.form);">TEST</button>
</form>
<div id="test"></div>
</body>
</html>
[test.jsp]UTF-8で記述
<%@ page contentType="text/plain;charset=utf-8" %>
<%@ page import="java.util.*, java.io.*" %>
<%!
//2バイト文字Unicode変換
public String strEncode(String strVal)
  throws UnsupportedEncodingException{
  if(strVal == null){
    return ( null);
  }else{
    return (new String(strVal.getBytes("ISO-8859-1"), "UTF-8"));
  }
}
%>
<%
//キャッシュ無効化
Calendar today = new GregorianCalendar();
response.setDateHeader("Last-Modified", today.getTime().getTime());
response.setDateHeader("Expires", 0);
response.setHeader("Pragma","no-cache");
response.setHeader("Cache-Control","no-cache");

//受信フォーム内容すべてをテーブルに書き出す
Map parameterMap = request.getParameterMap();
out.print("<table border><tr><th>key</th><th>value</th></tr>");
if(parameterMap != null){ 
  for ( Iterator i = parameterMap.keySet().iterator(); i.hasNext();) {
    String key = (String)i.next();
    String[] values = (String[])parameterMap.get(key);
    String str = "";
    if(values != null){ 
      for(int j = 0; j < values.length; j++){ 
        str += strEncode(values[j]) + ",";
      } 
    } 
    out.print("<tr><td>" + key + "</td><td>" + str + "</td></tr>");
  }
}
out.print("</table>");
%>

ローカルファイルを読み書きするサンプル(Scripting.FileSystemObject)

question:1169003191 で回答したものです。

<html>
<head>
<title></title>
<script>
var fso = new ActiveXObject("Scripting.FileSystemObject");
var path = "C:\\text.txt"

function readFile(){
  if(!fso.FileExists(path)){
    alert("ファイルが存在しません");
    return false;
  }
  var textObj = fso.OpenTextFile(path, 1);
  document.getElementById("edit").value = textObj.ReadAll();
  textObj.Close();
}

function writeFile(){
  var textObj = fso.OpenTextFile(path, 2, true);
  textObj.Write(document.getElementById("edit").value);
  textObj.Close();
}
</script>
</head>
<body>
<textarea id="edit"></textarea>
<button onclick="readFile();">READ</button>
<button onclick="writeFile();">WRITE</button>
</body>
</html>

参考:http://msdn.microsoft.com/ja-jp/library/cc409798.aspx
扱いには十分注意してください・・・。