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