スポンサーリンク

JavaScriptの動かないコード(中級編)callで,コンテキスト引数にnull・undefinedや,falseなどプリミティブ型の値を渡した時のthisの挙動


以下のJavaScriptコードが意図した動作をしないのは,なぜですか。(制限時間1分)


やりたい事:

  • 「真です。」と,一回だけ表示する。
  • 関数オブジェクトをcallして,引数が真と評価されるものだけアラート。
<script>

// 関数オブジェクト
var func = function(){

	// if文で真と評価されるのであれば
	if( this ){

		// アラート表示する
		alert( "真です。" );
		
	};
};

// コンテキストをさまざまな値に変えながらfuncを実行
func.call( 1 ); // 「1」はif文でtrueとして評価される
func.call( 0 );
func.call( false );
func.call( null );
func.call( undefined );

</script>

発生する問題

「真です。」というアラートが1回だけ表示されると思ったのに,
5回も表示される。

関数オブジェクトの中身のif文で,
すべての値がtrueとして評価されてしまっている。

問題の原因

この挙動の原因は,callの第一引数に渡された値が,場合によって他の適切な値に変換されてしまうためだ。


たとえばnullやundefinedを渡した場合,callするとthisの中身はnull等ではなくwindowオブジェクト(グローバル)にすりかわってしまう。


ためしに,冒頭のコードで alert("真です。") を alert( typeof this ) と書き換えてみよう。

すると [Object]と表示される。nullやundefinedが渡っていかないのだ。

.sort.call(null)の深淵 - 素人がプログラミングを勉強していたブログ
http://javascripter.hatenablog.com/en...

  • fun.call(null) === window


javascript:alert([].sort.call(null)) == [object window] の謎 - Togetterまとめ
http://togetter.com/li/6099

  • call(null) は call(グローバル) と同じ


applyとcallの使い方を丁寧に説明してみる - あと味
http://taiju.hatenablog.com/entry/201...

  • undefinedやnullを指定すると、暗黙的にグローバルオブジェクトに変換されます。


どうしてそうなるかというと,
thisの値としてnullを入れることは,JavaScriptの仕様上できないから。

コンテキストがnullとなることは不可能なので,かわりの値が自動的に入れ替わる。

call - Applying a Function to Null in Javascript - Stack Overflow
http://stackoverflow.com/questions/11...

  • In non-strict mode, this cannot be null, so it's replaced with the global object instead.


また,callの第一引数にtrue, false, 0, 1 などのプリミティブ型を渡した場合には,オブジェクトとしてオートボクシングされてしまう。

if文の中にそのまま渡すと,if( [Object] ) なので,真として評価されてしまうのだ。

Function.prototype.call() - JavaScript | MDN
https://developer.mozilla.org/en-US/d...

  • primitive values will be boxed.

この現象が引き起こす問題

「callを使ってコンテキストthisにさまざまな値を代入したい」というシチュエーションは,どんな時に生じるか?


たとえば,配列オブジェクトを拡張し,新規メソッドを独自に定義したい時がある。

配列の要素には何でも入るので,callの第一引数にもいろんな値がわたる。


そして,冒頭のコードのような問題が発生する。

callに渡したはずの値と,関数内で評価される値が異なるという結末を生む。


配列の拡張をする場合の落とし穴のサンプルコードを見てみる。

// 配列オブジェクトにhoge_eachという新メソッドを定義。
// hoge_eachは第一引数に無名関数を受け取る。
Array.prototype.hoge_each = function( func ){

	// この配列の全要素に対してイテレート
	for( var i = 0; i < this.length; i ++ ){

		// 要素をコンテキストとしながら無名関数を実行
		func.call( this[ i ] );

	}
};


// 配列を定義
var arr = [ 0, 1, "fuga", "", true, false, null ];


// 配列の各要素に対して,alertを実行。
arr.hoge_each(function(){

	// call関数のおかげで,コンテキストthisには
	// 配列の各要素が代入されている。

	// 真偽値で真として評価される値であれば
	if( this ){
		alert( this );
	}

});

配列のイテレータとして,eachを自前で実装したとする。

そしてeach内の無名関数では,thisが配列の各要素を表すとする。


この実装はまずい。

配列の要素としてnullとかundefinedを許可できなくなってしまうからだ。

解決策

callの第一引数に渡す値は,「正体が確定しており,nullやプリミティブではない安定した値」を入れるようにしよう。

そして,もしnullなどの任意の値を渡したいのであれば,callまたはapplyの第二引数に渡すようにすれば問題を回避できる。

JavaScriptのcallとapplyの違いを,一発で記憶して忘れない方法(メソッド引数が個別なのか配列なのかの違いを暗記する方法)
http://computer-technology.hateblo.jp/entry/20141221/p1

関連する記事:

JavaScriptで,クラスを継承する方法 (複数のサブクラスから共通クラスのプロトタイプを参照する)
http://language-and-engineering.hatenablog.jp/entry/20100924/p1


HTML5の「Web Workers API」を,別ファイルを使わずページ単体で利用するサンプル (createObjectURLがあれば,1ファイルでマルチスレッドのJSコーディングが可能)
http://language-and-engineering.hatenablog.jp/entry/20140331/HTMLfiveWebWorke...


迷路をウネウネと生成する,ライフゲーム的なJavaScript (アスキーアートで反復描画アニメーション)
http://language-and-engineering.hatenablog.jp/entry/20140624/CreateMeiroJavaS...


JavaScriptでの例外設計 (throw,try-catch-finally構文のイメージと利用パターン)
http://language-and-engineering.hatenablog.jp/entry/20101029/p1