スポンサーリンク

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文をいっさい使わない」というもの。

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にも、他言語同様に設計のミスがあります。

例えば、型変換によって加算と連結の意味が使い分けられる、 + のオーバーロードが挙げられます。

また、エラーを引き起こしやすいwithステートメントは避けられるべきものでした。

…これらの誤りは、プログラミングエラーを引き起こし、・・・


下記ページではwith文の推奨・非推奨を考察している。(RhinoのWithコンストラクタを含む)

また,過去にamachang氏がwithについて一言語っている。

with はある程度避けるべき、しかし、挙動は押さえるべき


弾さんのおっしゃるとおり、 with は不思議な文法なので、あまり使うべきではないかもしれない。(ちなみに、僕は使ってもいいとは思ってる。)その件については社内勉強会でもかなり白熱した議論になった。


ただ、ここで言っておきたいのは、 with 文の挙動を知らないと読めないソースがあるということだ。人のソースが読めるレベルの JavaScripter を育てるためには with 文の挙動は絶対に必要だ。


なぜなら、 with 文は with 文でしか説明できない現象であるからだ。オブジェクトのプロパティ名という名前空間を、スコープの変数名という名前空間に重ねる唯一の方法であるからだ。

確かに,

	with({e:e})
	{
		〜〜
	} 

というコードが出てきた時には,この構文を理解していないと付いていけない。



また,強制的にスコープを設定することにより,クロージャ未使用時に発生する問題を回避する,という使い方もある。

今の thisの barを 100ms 後に変更したくて次のように書いてはいけない:

setTimeout(function() {this.bar = "foo"}, 100);

なぜなら function のなかの this 擬変数は、今の contextの thisとは別物だから。そのとき thisが何を指してるなんて不定なのだ。


…でも with文を使ってこれを回避できることに気がついた。

with(this) {
setTimeout((function() {bar = "foo"}), 100);
}

JavaScript には基本的にブロックスコープというものが存在しない。どうしてもブロックスコープを扱いたいときは function 式を使ったりする。

ところが、with 文とオブジェクトリテラルを使えばブロックスコープを実現できる。…


さらに,コードが短くて済むのも事実だ。
Flash初心者のころ,_parent._parent とかの長い相対オブジェクト指定が面倒で,スクリプト中で何も考えずwithを多用していたのが思い出される。…)




状況をまとめると

  • with文の特徴は,現在のスコープに対し,別のスコープを擬似的に追加すること。
  • メリットは,
    • オブジェクト名を省略して,短く書けること。(自分の好きなスコープを引っ張ってこれるので)
    • thisの変化を予防できること。
  • デメリットは,どの変数を指しているのかわからなくなること。(本来と異なるスコープが混ざり込むことになるので)

という事になる。



これらの点に基づいた結論は,

  • ソース規模が大きくなりそうな時や,共同作業の時は,with文の利用を控える。
  • with自体が本質ではないような簡易・小規模なコードを書いているような時や,またスコープ強制限定のテクニックを使いたい時は,withの利用もあり得る。
  • ただし利用する場合でも,withにまつわる問題を意識しておくことが必要。

といった所だろう。