goo blog サービス終了のお知らせ 

DEVELOPMENTOR*LEARNING RESOURCE LAB.

ソフトウェア開発現場の視点からソフトウェアと開発者の価値を高める「ホットな何か」をお届けします。(休止)

Function.applyを知ればprototype.jsのFunction.bindを理解できる

2006年10月05日 | prototype cast
[ajax] prototype.jsの活用にはFunction.applyがどう振舞うか知っておきたいの続きです。prototype.jsを使ったコードではfunctionを指定した繰返しの構文をよく見かけます。

次の例はNumber.timesを使って2の8剰を計算し、その結果を「this.v」に保存しています。もちろん結果は256です。
var Foo = Class.create();
Foo.prototype = {
  initialize: function() {
    this.v = 1;
  },
  f1: function() {
    (8).times(function() {
      this.v *= 2;
    }.bind(this));
    alert('Are you ready?: '+this.v);
  }
}

var foo = new Foo();
foo.f1();



前述の例ではfunction{・・・}.bind(this)という馴染みのない構文を使っています。この「.bind(this)」とはどんな意味があるのでしょうか。これからその意味を確かめてみます。

前述の例から「.bind(this)」を削除してみます。結果は256と思いきや1になります。とても結果が1になるようには見えません。
var Foo = Class.create();
Foo.prototype = {
  initialize: function() {
    this.v = 1;
  },
  f1: function() {
    (8).times(function() {
      this.v *= 2;
    });
    alert('Are you ready?: '+this.v);
  }
}



前述の例を別の書き方で表現すると次のようになります。書き方を変えると結果が256でなく1になるのが当然とわかります。
function g1() {
  this.v *= 2;
}
var Foo = Class.create();
Foo.prototype = {
  initialize: function() {
    this.v = 1;
  },
  f1: function() {
    (8).times(g1);
    alert('Are you ready?: '+this.v);
  }
}



前述の例に「.bind(this)」を加えてみます。結果は1でなく256になります。Function.bindの振る舞いとその効果が見えてきましたね。
function g1() {
  this.v *= 2;
}
var Foo = Class.create();
Foo.prototype = {
  initialize: function() {
    this.v = 1;
  },
  f1: function() {
    (8).times(g1.bind(this));
    alert('Are you ready?: '+this.v);
  }
}



次の関数(メソッド)はFunction.bindのソースコードを抜粋したものです。bindは新しいfunctionを生成しています。生成したfunctionは自functionをFunction.applyを使って実行しますが、そのときbindの引数を指定しています。この引数は前述の例では「this」に相当します。
Function.prototype.bind = function() {
  var __method = this, args = $A(arguments), object = args.shift();
  return function() {
    return __method.apply(object, args.concat($A(arguments)));
  }
}


[ajax] prototype.jsの活用にはFunction.applyがどう振舞うか知っておきたいを振り返りながら読み取ってください。Function.bindの理解が深まり、prototype.jsの活用の幅が広がります。

動きそうで動かないJavaScriptを取り込む事例

2006年10月05日 | prototype cast
JavaScriptの中からJavaScriptを取り込もうと次のコードを書いてみました。

myscript.jsの中からprototype.jsを取り込み、そのprototype.jsのEvent.observeを使ってページロード後にメッセージの表示を試みます。myscript.htmlではmyscript.jsを取り込んでいます。

myscript.js

document.write('<script type="text/javascript" src="js/prototype.js><\/script>');
Event.observe(window, 'load', function() {
  alert('Are you ready?');
}, false);

myscript.html

<head>
<script type="text/javascript" src="myscript.js"></script>
</head>



パッと見だと動きそうな気がしますが「Event.observe is not a function」というエラーになります。Event.observeを使う時点ではprototype.jsが評価されないのですね。なんでだろう・・・とJavaScriptを使いはじめた頃、不に落ちない気持ちになったことを思い出します。

JavaScriptを使って大中規模のアプリケーションを開発するとき、JavaScriptの中からJavaScriptを取り込むような構成を考慮する機会があると思います。そのときこの動きそうで動かない事例を知っているとよいですね。

以前このことで再設計を迫られてちょっと恥ずかしい思いをした体験からの記事でした。(^^;

prototype.jsのObjectRangeで上限と下限の範囲をテストする

2006年10月05日 | prototype cast
[ajax] prototype.jsのObjectRangeでforなどの繰返し構文の不足点を補うの続きです。prototype.jsのObjectRangeを使うと、○以上△以下といった上限と下限の範囲をテストできます。

次のようなifをObjectRangeで代替します。xが10以上かつ20以下かどうかテストしています。
  if (x >= 10 && x <= 20) {
// TODO: }

上記のifをObjectRangeで表現すると次のようになります。ObjectRangeの第3引数(exclusive)には上限値を含める(除外しない)を指定していますので10以上かつ20以下の条件になります。
  var r = new ObjectRange(10, 20, false);
  if (r.include(x)) {
    // TODO:
  }

ObjectRangeは短く$Rともできます。
  if ($R(10, 20, false).include(x)) {
    // TODO:
  }

次のようなifをObjectRangeで代替します。xが10以上かつ20未満かどうかテストしています。
  if (x >= 10 && x <20) {
上記のforをObjectRangeで表現すると次のようになります。ObjectRangeの第3引数(exclusive)には上限値を含めない(除外する)を指定していますので10以上かつ20未満の条件になります。
  var r = new ObjectRange(10, 20, true);
  if (r.include(x)) {
    // TODO:
  }

ObjectRangeは短く$Rともできます。
  if ($R(10, 20, true).include(x)) {
    // TODO:
  }


prototype.jsのObjectRangeでforなどの繰返し構文の不足点を補う

2006年10月05日 | prototype cast
prototype.jsのObjectRangeを使うと、forなどの繰返し構文を代替したりその不足点を補えます。

次のようなforをObjectRangeで代替します。iを1から3まで3回繰り返します。
  for (var i=1; i<=3; i++) {
//TODO: }

上記のforをObjectRangeで表現すると次のようになります。ObjectRangeの第3引数(exclusive)には上限値を含める(除外しない)を指定しています。繰返し中のvは1~3に変化します。またiはインデックス相当で0~2に変化します。繰返し中の値とインデックス相当が得られるのがポイントです。
  var r = new ObjectRange(1, 3, false);
  r.each(function(v, i) {
    //TODO:
  });

ObjectRangeは短く$Rともできます。
  var r = $R(1, 3, false);
  r.each(function(v, i) {
    alert(v+':'+i);
  });

次のようなforをObjectRangeで代替します。iを0から2まで3回繰り返します。
  for (var i=0; i<3; i++) {
上記のforをObjectRangeで表現すると次のようになります。ObjectRangeの第3引数(exclusive)には上限値を含めない(除外する)を指定しています。繰返し中のvは0~2に変化します。第2引数のインデックス相当は同値のため省略しています。
  var r = new ObjectRange(0, 3, true);
  r.each(function(v) {
    //TODO:
  });

ObjectRangeは短く$Rともできます。
  var r = $R(0, 3, true);
  r.each(function(v) {
    //TODO:
  });

上記のforはNumber.timesでも表現できます。繰返し中のvは0~2に変化します。Numberオブジェクトがあって繰返しの始点が0のとき役立ちます。ちなみにNumber.timesの中ではObjectRangeが使われています。
  var r = Number(3);
  r.times(function(v) {
    //TODO:
  });


prototype.jsの活用にはFunction.applyがどう振舞うか知っておきたい

2006年10月05日 | prototype cast
prototype.jsを使いはじめたとき、スッキリと受け入れにくい機能や構文に出くわすことが多々ありました。

いま振り返ってみると、prototype.jsの中で使われるJavaScriptのFunction.applyの理解不足が原因だった気がします。つまりFunction.applyの振る舞いを理解しておくことがprototype.jsを活用する近道になります。

次のような簡単な関数を使ってFunction.applyの振る舞いを観察します。
function f() {
  alert(this.v);
}


次のようにそのまま関数fを呼び出すと「undefined」というメッセージが表示されます。
f();



次のようにFunction.applyを使って関数fを呼び出すと「Are you ready?: 1」というメッセージが表示されます。このときFunction.applyの引数にオブジェクトを指定したのがポイントです。
var foo = new Object();
foo.v = 'Are you ready?: 1';
f.apply(foo);



次のような簡単なクラスを使ってFunction.applyの振る舞いを観察します。


var Foo = Class.create();
Foo.F1 = function() {
  f();
}
Foo.F2 = function() {
  f.apply(this);
}
Foo.prototype = {
  initialize: function() {
    this.v = 'Are you ready?: 2';
  },
  f1: function() {
    f();
  },
  f2: function() {
    f.apply(this);
  }
}

次のようにstaticメソッド(風関数)から関数fを呼び出すと、両者とも「undefined」というメッセージが表示されます。このとき両者の結果が同じ(違いがない)ことがポイントです。
Foo.F1();
Foo.F2();



次のようにオブジェクトを生成してメソッドから関数fを呼び出すと、前者は「undefined」後者は「Are you ready?: 2」というメッセージが表示されます。このときFunction.applyの引数に自分自身のオブジェクトを指定したのがポイントです。
var foo = new Foo();
foo.f1();
foo.f2();



関数fを呼び出すときの状況、関数fを呼び出すときにFunction.applyを使うかどうかに注目してください。またそのときのthis.vの値を比較しながらFunction.applyの仕様を再確認してください。きっとFunction.applyの振る舞いがイメージしやすくなり理解が進むと思います。

prototype.jsのClassをパッケージ階層っぽく使う

2006年10月05日 | prototype cast
prototype.jsにはAjax.RequestやInsertion.Beforeのようにパッケージっぽい名前をもつクラスがあります。この「Ajax.」や「Insertion.」といったパッケージっぽいことを自前のクラスでも実現してみます。

次のソースコードはFooパッケージのBarクラスを定義したつもりですがエラーとなって動きません。
Foo.Bar = Class.create();
Foo.Bar.prototype = {
  initialize: function() {
  },
  baz: function(message) {
    alert(message);
  }
}

エラーの原因はFooが未定義のためです。ですので、次のようにFooを事前にオブジェクにしておきます。
var Foo = new Object();
Foo.Bar = Class.create();
Foo.Bar.prototype = {
  initialize: function() {
  },
  baz: function(message) {
    alert(message);
  }
}

これでエラーなく期待したとおりに動作します。
  var bar = new Foo.Bar();
  bar.baz('Are you ready?');

パッケージっぽいところは単なるオブジェクトだったのですね。(^^;

prototype.jsのElement.showで表示できないときは...

2006年10月05日 | prototype cast
次のような(非表示の)ブロック要素があり...
<div id="r">(ブロック要素)</div>

そのブロック要素を次のようにElement.showで指定しても表示できないという話題をよく見かけます。
Element.show('r');

意図しない振る舞いに「これはElement.showの不具合ではないか?」と立ち止まってしまいがちですが、prototype.jsのソースコードを調べるとその原因が見えてきます。

次の関数(メソッド)はElement.showのソースコードを抜粋したものです。ソースコードを見るとElement.showは指定した要素のdisplay属性を既定値(初期値)に戻しているようです。
  show: function() {
    for (var i = 0; i <arguments.length; i++) {
とすると、Element.showで指定した要素が表示できないときは、次のようなスタイルを指定していると推測できます。このスタイルだとElement.showとしても結果としてdisplay属性がnone(非表示)となってしまいます。
#r {
  color: red;
  display: none;
}

ですので、あらかじめ要素を非表示としておいてElement.showで表示をするときは、その要素のインラインスタイルでdisplay属性をnoneとし、スタイルではdisplay属性を指定しないという方法が1つの解決策になります。
#r {
  color: red;
  /*display: none;*/
}

<div id="r" style="display: none;">(ブロック要素)</div>



prototype.jsのObject.extendで関数の省略値を補完する

2006年10月05日 | prototype cast
関数(メソッド)の引数を省略可とし、省略したとき何らかの初期値を補完したいことがよくあります。ただJavaScriptは関数のオーバーロードができないので... 関数の引数が省略されたかチェックし、省略されたときは初期値を補完するといったコードを起こす必要があります。

巷のライブラリは、省略可なパラメータを1つの引数にまとめることが多いようです。次の関数の例は「a1」「a2」は必須「options」を省略可としています。
また「options」は引数を省略可とし、さらに「oprions.a」「oprions.b」「oprions.c」のいずれも省略可としています。
function f(a1, a2, options) {
  var r = Object.extend({
    a: 123,
    b: 'abc',
    c: 'xyz'
  }, options || {});
}

上記の関数は次のように様々な呼び出し方ができます。このとき省略した「options」もしくは「options.a」「options.b」「options.c」は、あらかじめ指定しておいた初期値で期待どおり補完されます。
  f('#1', '#2');
  f('#1', '#2', {});
  f('#1', '#2', {a: 456});
  f('#1', '#2', {a: 456, b: 'def'});
  f('#1', '#2', {a: 456, b: 'def', c: 'XYZ'});


prototype.jsのEnumerable.findでラジオボタンの値を取得する

2006年10月05日 | prototype cast
次のようなラジオボタンを使ったフォームはよく使います。
<form name="f">
<input type="radio" name="i" value="123" checked>
<input type="radio" name="i" value="abc">
<input type="radio" name="i" value="xyz">
</form>

ただJavaScriptから選択されたラジオボタンの値を取得するのはちょいと面倒だったりします。
  var r = null;
  for (var i=0; i<document.f.i.length; i++) {
    if (document.f.i[i].checked) {
      r = document.f.i[i];
      break;
    }
  }
  alert(r.value);

次のようにEnumerableオブジェクトのfindメソッドを使えば簡潔になります。
  var r = $A(document.f.i).find(function(v) {
    return v.checked;
  });
  alert(r.value);

Ajax.InPlaceEditorで任意のパラメータをPOSTする

2006年10月05日 | prototype cast
script.aculo.usのAjax.InPlaceEditorでデータを送信するとき、識別用のIDといった任意のパラメータを付与するサンプルがWeb2.0note script.aculo.usの使い方(InPlaceEditor) その2で公開されています。

サンプルはGET(POSTではなく)でデータ送信するという制約があるので、該当ページのコメントをヒントにcallback関数を使ってPOSTでデータ送信するサンプルを用意しました。

<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<script type="text/javascript" src="prototype.js"></script>
<script type="text/javascript" src="effects.js"></script>
<script type="text/javascript" src="controls.js"></script>
</head>
<body>
<h1 id="callback">(callback)</h1>
<script type="text/javascript">
<!--
new Ajax.InPlaceEditor($('callback'),
  'inplaceeditorcallback.cgi', {
  callback: function(form) {
    var id = document.createElement('input');
    id.type = 'hidden';
    id.name = 'id';
    id.value = 'ABC12345';
    form.appendChild(id);
    return Form.serialize(form);
  },
  ajaxOptions: {method: 'post'}
});
-->
</script>
</body>
</html>


callback関数はデータ送信する直前に呼び出されます。callback関数の戻り値は「Ajax.Request parameters:」の値となります。callback関数の引数には送信フォームが渡されます。この送信フォームはAjax.InPlaceEditorの内部(隠し?)フォームでデータ送信の元ネタとなっています。

サンプルはこの特性を利用してAjax.InPlaceEditorで任意のパラメータをPOSTしています。callback関数の中で識別用のIDパラメータ(隠しフィールド)を送信フォームに追加し、その後「Ajax.Request parameters:」の値となるように送信フォームをシリアライズしています。

Ajax.InPlaceEditorの詳しい仕様はAjax.In Place Editor in scriptaculous wikiを参照してください。ちなみにForm.serialize(prototype.js)はパラメータ値をencodeURIComponentでエンコードするようです。

データ受信は次のスクリプトを使いました。

#!/usr/bin/perl
use CGI;
my $cgi = new CGI;
print $cgi->header(-type=>'text/plain');
print $cgi->request_method;
print $cgi->save(*STDOUT);
"True Value";