m2

Java プログラマから見た JavaScript

Java プログラマが理解しにくい JavaScript のポイント

Java プログラマJavaScript を理解する上で重要だと思っているポイントが2点ある。

  • this の理解

だ。
ちょうど id:amachang さんのとこでその両方にかかるエントリーがあったのでそれを流用して、Java プログラマの立場から解説したい。

引用したコードを Java っぽく書いてみる

[javascript]XMLHttpRequest の onreadystatechange の this
http://d.hatena.ne.jp/amachang/20060921/1158796639
で動かなかったとされるコードは以下。(要点を絞り込むため、簡略化している)

var req = new XMLHttpRequest();
req.onreadystatechange = function() {
    switch (this.readyState) {
            :
    }
};

これを現在*1Java っぽく書くと以下のようになる。

XMLHttpRequest req = new XMLHttpRequest();
req.addReadyStateChangeListener( new ReadyStateChangeListener() {
    public void stateChangd(ReadyStateChangeEvent e){
        switch (this.readyState) {
          :
    }
});

細かい説明は省略するが readyState が XMLHttpRequest のフィールドだとすると、Java プログラマならこのコードに問題があることがわかるだろう。
それから期待通りに動作したとされるコードも Java っぽく書いてみる。

XMLHttpRequest req = new XMLHttpRequest();
req.addReadyStateChangeListener( new ReadyStateChangeListener() {
    public void stateChangd(ReadyStateChangeEvent e){
        switch (req.readyState) {
          :
    }
});

こんどは何故 req にアクセスできるのかわからないのではないだろうか?*2

JavaScript における this は実行時に決まる

前述の Java コードでの this はもちろん自分自身、つまり「ReadyStateChangeListenerのインスタンス」を指す。readyState フィールドを持っているのは XMLHttpRequestインスタンスなのだから、当然期待通りには動作しない(コンパイルできない)。元の JavaScript のコードも似たような理由で動作しない。
Javaインスタンスメソッドはそのインスタンスへの操作として定義され、this は別のインスタンスを指したりはしない。が、JavaScript の function は変数に代入、つまりあらゆるオブジェクトのメンバになり得るため、function を定義した時点では this が何を指すかは決まらず、実行時にきまる。だからその function がどのように実行されるか判断できない場合、this は使用すべきでない。
以下の例がわかりやすかったので引用する。

function hoge(){
    alert(this);
}
var myObj = new Object();
myObj.hoge = hoge;
hoge();
myObj.hoge();

うん、わかりやすい。そして、

どうも、Javaとかやってる人にはそれがスゴイ気持ち悪いらしいのです。

うん、気持ち悪いw

クロージャクロージャが生成された個所のローカル変数を参照できる*3

前述の this を req に書き換えた Java コードもコンパイルはできない。インナークラスからそのインナークラスが宣言されたスコープのローカル変数にアクセスできないからだ。
が、JavaScriptクロージャからはアクセスできるから意図したとおりに動作する。*4
Java でもローカル変数にアクセスすることは可能で、以下のように final 宣言をしてその変数が指す参照が書き換えられないことを保証する必要がある。

final XMLHttpRequest req = new XMLHttpRequest();
req.addReadyStateChangeListener( new ReadyStateChangeListener() {
    public void stateChangd(ReadyStateChangeEvent e){
        switch (req.readyState) {
          :
    }
});

クロージャでもローカル変数を書き換えてはならない

このエントリで一番言いたかったこと。
Java のインナークラスからローカル変数にアクセスする場合は、言語仕様上ローカル変数が書き換えられないわけだが、JavaScriptクロージャでもアクセスするローカル変数を書き換えてはならない下に追記あり。このことは以下のページに詳しい。


クロージャOOPJavaScriptの謎仕様
http://ishikawa.arielworks.com/memo/2005/07/24/031449


Java のインナークラスと関連付けて考えると、意外とすんなり理解できるかもしれない。

元記事の JavaScript コードも、以下のようにすると動作しない。

var req = new XMLHttpRequest();
req.onreadystatechange = function() {
    switch (req.readyState) {
            :
    }
};
req.open('GET', '.');
req.send(null);
req=null;
  • -

(2007/02/17 追記)
さすがに4ヶ月間ほとんど JavaScript ばかりやっていると理解度が違うな。
さて、上の主張とは全く違うことを言うけど、アクセスするローカル変数を書き換えてはならないというのは間違いです。つーか書き換えられるのがクロージャのキモだった。ゴメン。
クロージャーを利用することでアクセスするローカル変数をプライベートに扱うことができます。これは以下に詳しいです。
Collection & Copy - JavaScriptにおけるプライベートメンバ
http://d.hatena.ne.jp/brazil/20051028/1130468761
ただ↓のまとめに書いていることは間違いありません。変数を"何処に"宣言して"何処から"アクセスしているか、ちゃんと理解してコードを書くようにしましょう。

まとめ

クロージャの中で参照しているローカル変数を書き換えてしまって意図した通りに動かなくなることは、おそらく多くの人(もちろん私も)がハマることだと思う。mala さんも

ちょっとした神秘だ。

と言っている。ぜひ気をつけたい。*5

*1:Java にもクロージャがサポートされるらしい。http://journal.mycom.co.jp/articles/2006/08/23/java7closuer/

*2:わからないで欲しい

*3:タイトルは http://kmaebashi.com/programmer/devlang/object.html から引用

*4:Javaのような厳格な言語に慣れていると、Perl なんかはまるで甘いケーキみたいだ

*5:このことに名前を付けると広まりやすいかもしれない。「クロージャ地獄」なんてのはどうだろう。