JavaScriptの動かないコード (中級編) オブジェクトのプロパティ定義にthisを使って失敗するエラー
以下のJavaScriptコードが意図した動作をしないのは,なぜですか。(制限時間1分)
やりたい事:
- 文字列が改行やスペースを含むかどうか,正規表現で調べる。
<input type="button" value="クリックして表示" onClick="f()"> <script language="JavaScript"> // 文字列が特殊文字を含むかどうか判定するためのオブジェクト。 var obj = { /* ---------- メンバ変数 ---------- */ // 改行とスペースとタブ r : "\\r\\n \\t", // [〜]でくくると,「その中のどれか」という意味 r_contain : "[" + this.r + "]", // [^〜]は,「それら以外」という意味 r_not_contain : "[^" + this.r + "]", /* ---------- メソッド ---------- */ // を含むか is_match : function( str ){ return !! str.match( new RegExp( this.r_contain ) ); }, // でない文字を含むか not_match : function( str ){ return !! str.match( new RegExp( this.r_not_contain ) ); } }; function f() { // 改行やスペースを含むかどうかテスト alert( obj.is_match( "abc" ) ); // falseのはず alert( obj.is_match( "abc \n" ) ); // true のはず alert( obj.is_match( "abcd" ) ); // falseのはず alert( obj.is_match( "abcd \n" ) ); // true のはず } </script>
答え
FFでもIEでも,「false, true, false, true」 ではなく「false, false, true, true」 になる。
何かの理由で,正規表現のマッチがうまくいっていない。
原因を理解するために
クラスのprototypeやオブジェクトを定義する際,メソッド定義の中でthisを使ってもよいが,プロパティ定義の中でthisを使うべきではない。
下記の例を見てみよう。
<input type="button" value="クリックして表示" onClick="f()"> <script language="JavaScript"> var obj = { x : 1 + 1, // ←これを y : this.x // ←ここから参照したいのだが・・・ }; function f() { alert( obj.x ); // 2 alert( obj.y ); // undefined になってしまう。 } </script>
新規プロパティ作成時に,定義の中でthisを使って,自分の属するオブジェクトから値を引っ張って来ようとすると失敗する。
どういうふうに失敗しているのか?
下の例でいうと,3つある x のうち,どの x を obj.y が見ているのだろうか。
<input type="button" value="クリックして表示" onClick="f()"> <script language="JavaScript"> // (1)グローバル変数のx var x = 1; var obj = { // (2)同一オブジェクト内のx x : 2, y : this.x // ←ここのxは,どこのxを参照しているのか? }; function f() { // (3)同一実行環境内で定義されたx var x = 3; alert( obj.y ); } </script>
答えは(1)であり,アラートの結果表示されるのは 1 である。
objのプロパティ定義の中では,thisはオブジェクト自身ではなく,一つ上の環境(グローバル)を指していたのだ。
さらに詳しく考えるために,下記のコードを見てみる。
下記で,「値を1に設定します。」というアラートが表示されるのは,いつのタイミングなのか?
ボタンを押したときだろうか。
<input type="button" value="クリックして表示" onClick="f()"> <script language="JavaScript"> // ↓これを・・・ var a = function(){ alert( "値を1に設定します。" ); return 1; }; var obj = { x : a() // ←ここから参照する。しかし,いつ参照を行なっているのか? }; function f() { alert( obj.x ); } </script>
答えは,ページを読み込んだ瞬間。(ボタンを押した瞬間ではない。)
ページ読み込み時にスクリプトエンジンは,上から順にだいたい下記のように解釈していく。
var a = 〜; var obj = {}; obj.x = a(); 〜
obj.x = a(); の行は,obj.x = this.a(); となっていても同じこと。(thisがグローバルを指しているので)
thisでオブジェクト内を指すことができていないのがよくわかる。
冒頭のコードの場合
「this.r」 ではグローバル変数の r を参照してしまうが,グローバルにはそういう変数は存在しない。
よって,this.r の値は undefined になる。
ひどいのはここからなのだが,
undefined を使って正規表現を構築しようとすると
r_contain : "[" + this.r + "]", ↓ 解釈 r_contain : "[undefined]",
となり,なんと文字 "u", "n", "d", … とのマッチ判定が始まってしまう。
冒頭のコードでは,
- "abc" → u,n,d,e,f,i,n,e,d のどれともマッチしない。
- "abc \n" → 同じく。
- "abcd" → めでたく d とマッチ。
- "abcd \n" → 同じく。
というわけで,false, false, true, true が返る結果となった。
解決策
クロージャの性質により,メソッド内では this はちゃんと同一オブジェクトを指してくれるから,プロパティではなくメソッドにすればよい。
objの定義を書き換える。
var obj = { r : "\\r\\n \\t", // メソッドにする r_contain : function(){ return "[" + this.r + "]"; }, // メソッドにする r_not_contain : function(){ return "[^" + obj.r + "]"; }, // メソッド呼び出しにする is_match : function( str ){ return !! str.match( new RegExp( this.r_contain() ) ); }, // メソッド呼び出しにする not_match : function( str ){ return !! str.match( new RegExp( this.r_not_contain() ) ); } };
これなら予期した通り「false, true, false, true」が返る。
今後,正規表現で変な挙動に出会った時には,まさか"undefined"と比較していないかどうか念のため疑ってみたい。
なお,メソッドの戻り値の前に !! を付けているのは,変数の型を論理型に変換するため。
JavaScriptの動かないコード (初級編) if文の分岐がおかしい
http://language-and-engineering.hatenablog.jp/entry/20080915/1221451639