JavaScriptの動かないコード (初級編) with文にまつわるエラー
以下のJavaScriptコードが意図した動作をしないのは,なぜですか。(制限時間1分)
やりたい事:
- 「はい」ボタンと「いいえ」ボタンの押下を区別して応答する。
- ボタンのトータルの押下回数も表示する。
<input type="button" value="はい" onClick="f(1)"> <input type="button" value="いいえ" onClick="f(0)"> <script language="JavaScript"> function f( num ) // 引数が 1 :「はい」,0 :「いいえ」 { with( obj ) { // 押されたのは「はい」ボタンか? if( num == 1 ) // f(1) の場合 { yes(); } else { no(); } } } // 応答をするオブジェクト var obj = { // ボタンを押したトータルの回数を記録・表示する。 num:1, yes:function(){alert( this.num + "回目:はい" ); this.num++; }, no:function(){alert( this.num + "回目:いいえ" ); this.num++; } }; </script>
答
どちらのボタンを押したかに関わりなく,
- 1回目は「はい」を押したとみなされてしまう。
- 2回目以降は「いいえ」を押したとみなされてしまう。
if文の条件分岐に失敗している。
理由は,その if 文が参照している num という変数が,意図した値になっていないため。
- プログラマの意図した「num」: f( 0 ), f( 1 ) の引数
- 実際の処理に使われた「num」: obj のメンバ変数
となっている。なぜなら,with文で obj というオブジェクトを指定しているから。
with文で指定したオブジェクトの効力は,地の文だけでなく,if(〜)の中身にも効いてくるのだという事がわかる。
with文の利用を前提に考えると,これは「num」という変数名を2か所で使っている事に問題がある。
もし f() の引数を num ではなく「 n 」など別の名前に変えてみれば,冒頭のコードは問題なく意図したとおりに動作する。
しかし怖いのは,共同作業をしている場合である。
もしもコーディング担当者が f() と obj で別々の人間だったら,どうなるか?
ぐうぜん f() の中身と obj の中身とで同じ変数名を使っていたら,冒頭のようにwith文でのエラーは避けられなくなる。
コード間で名前の衝突が起こってしまうのだ。
こうした共同作業時の問題を考えると,単に担当者レベルで「同じ変数名を使わないようにする」というのは,根本的な解決策として不十分になる。
解決策
多くの人から支持されている結論は,「with文をいっさい使わない」というもの。
- JavaScript: The World's Most Misunderstood Programming Language
Design Errors
JavaScript has its share of design errors, such as the overloading of + to mean both addition and concatenation with type coercion, and the error-prone with statement should be avoided.
上記の訳文:
- JavaScript:世界で最も誤解されたプログラミング言語
設計ミス
JavaScriptにも、他言語同様に設計のミスがあります。
例えば、型変換によって加算と連結の意味が使い分けられる、 + のオーバーロードが挙げられます。
また、エラーを引き起こしやすいwithステートメントは避けられるべきものでした。
…これらの誤りは、プログラミングエラーを引き起こし、・・・
下記ページではwith文の推奨・非推奨を考察している。(RhinoのWithコンストラクタを含む)
また,過去にamachang氏がwithについて一言語っている。
- 「勝手に添削 - JavaScript 入門」を勝手に添削
with はある程度避けるべき、しかし、挙動は押さえるべき
弾さんのおっしゃるとおり、 with は不思議な文法なので、あまり使うべきではないかもしれない。(ちなみに、僕は使ってもいいとは思ってる。)その件については社内勉強会でもかなり白熱した議論になった。
ただ、ここで言っておきたいのは、 with 文の挙動を知らないと読めないソースがあるということだ。人のソースが読めるレベルの JavaScripter を育てるためには with 文の挙動は絶対に必要だ。
なぜなら、 with 文は with 文でしか説明できない現象であるからだ。オブジェクトのプロパティ名という名前空間を、スコープの変数名という名前空間に重ねる唯一の方法であるからだ。
確かに,
with({e:e}) { 〜〜 }
というコードが出てきた時には,この構文を理解していないと付いていけない。
また,強制的にスコープを設定することにより,クロージャ未使用時に発生する問題を回避する,という使い方もある。
- JavaScriptの with文で prototype.jsの bindを省けたが
今の thisの barを 100ms 後に変更したくて次のように書いてはいけない:
setTimeout(function() {this.bar = "foo"}, 100);
なぜなら function のなかの this 擬変数は、今の contextの thisとは別物だから。そのとき thisが何を指してるなんて不定なのだ。
…でも with文を使ってこれを回避できることに気がついた。
with(this) {
setTimeout((function() {bar = "foo"}), 100);
}
- JavaScript でブロックスコープを実現する
JavaScript には基本的にブロックスコープというものが存在しない。どうしてもブロックスコープを扱いたいときは function 式を使ったりする。
ところが、with 文とオブジェクトリテラルを使えばブロックスコープを実現できる。…
さらに,コードが短くて済むのも事実だ。
(Flash初心者のころ,_parent._parent とかの長い相対オブジェクト指定が面倒で,スクリプト中で何も考えずwithを多用していたのが思い出される。…)
状況をまとめると
- with文の特徴は,現在のスコープに対し,別のスコープを擬似的に追加すること。
- メリットは,
- オブジェクト名を省略して,短く書けること。(自分の好きなスコープを引っ張ってこれるので)
- thisの変化を予防できること。
- デメリットは,どの変数を指しているのかわからなくなること。(本来と異なるスコープが混ざり込むことになるので)
という事になる。
これらの点に基づいた結論は,
- ソース規模が大きくなりそうな時や,共同作業の時は,with文の利用を控える。
- with自体が本質ではないような簡易・小規模なコードを書いているような時や,またスコープ強制限定のテクニックを使いたい時は,withの利用もあり得る。
- ただし利用する場合でも,withにまつわる問題を意識しておくことが必要。
といった所だろう。