JavaScriptでの配列オブジェクト判定問題とは
JavaScriptの「実行時型判定」は、機能が弱いことが知られているが、特に配列のチェックに於いて回避の難しい問題を起こすことが知られている。次の例を見て頂きたい。まず次のファイル(p2.html)を用意する
<html> <script> parent.test([1,2,3]); </script> </html>
そしてp2.htmlと同じフォルダに次のファイル(p1.html)を作成する。
<html> <script> function test(arr) { alert( [1,2,3] instanceof Array ); alert( arr instanceof Array ); } </script> <body> <iframe src="p2.html"></iframe> </body> </html>
この2つ目のalertは、trueを返さない。これが問題だ。
この問題はもっと短くこの様に再現出来る。
var iframe = document.createElement('iframe'); document.body.appendChild(iframe); OtherArray = window.frames[0].Array; var arr = new OtherArray(1,2,3); // [1,2,3] alert( arr instanceof Array ); // false ってなんでやねん! alert( arr.constructor === Array ); // false ってなんでやねん!
何故こういう事になるのかというと、JavaScriptの基本オブジェクトが各フレーム毎に異なるインスタンスとして作成されるかららしい。なんのこっちゃ。 その事を順を追って説明したい。
JavaScriptのプリミティブ型とは?
前述の通り、JavaScriptの型チェック機能は、非常に弱いことが知られている。JavaScriptの型チェックとして typeof演算子という物が用意されているが、これはオブジェクト型判定には使えないシロモノだ。
typeof null; // "object" typeof []; // "object"
この様にtypeof演算子は、具体的な型が何かを調べることは出来無い。
JavaScriptは、全ての値がオブジェクトで実装されている訳ではなく、いくつかのプリミティブ型とオブジェクト型が併存する形で実装されている。JavaScriptのプリミティブ型には、5種類存在する。これらはオブジェクト型と性質が異なる
- undefined
- null
- boolean
- number
- string
これらのプリミティブ型はtypeof演算子を使うことでその型を判定する事が出来る。
alert( typeof 1 ); // "number" alert( typeof "hello" ); // "string" alert( typeof true ); // "boolean"
このtypeof演算子を使って判定できるのは、プリミティブ型だけだ。オブジェクト型は、それがオブジェクト型だという事がわかるだけで、そのオブジェクト型がどのクラスに所属しているのかまでは調べることが出来無い。
alert( typeof new Object() ); // "object" alert( typeof new Number(1) ); // "object" alert( typeof new String("hello") ); // "object" alert( typeof new Boolean(true) ); // "object" alert( typeof [] ); // "object" alert( typeof {} ); // "object"
よってtypeofだけでは機能が不十分であり、オブジェクト指向の多態性を駆使した動的な処理を実装することが出来無い。
参照 ECMAScript Language Specification - 11.4.3 The typeof Operator
何がユーザーを混乱させているのだろうか。ややこしいのは、boolean ・ number ・ string の3つのプリミティブ型に、オブジェクト型として同等の型が用意されていることだ。これらは比較演算子で比較処理される際、自動的に型変換されて比較される。自動的な為にユーザからはその違いがはっきり意識されないが、そもそもまったく別物だ。次の例を見て欲しい。
alert( new Number(1) == 1 ); // true alert( new String("hello") == "hello" ); // true alert( new Boolean(true) == true ); // true
この様にオブジェクト型とプリミティブ型は、比較演算子のオペランドとして利用される際に自動的にプリミティブ型に変換され、変換された上で比較される。自動で処理される為にユーザーからはその違いがわかりにくいが、多態性を駆使した処理を実装するときには、この違いをはっきりと意識する必要がある。
次にように型を調べてみると、異なる型ということがわかる。
alert( typeof new Number(1) == typeof 1 ); // false; alert( typeof new Boolean(true) == typeof true ); // false; alert( typeof new String("hello") == typeof "hello" ); // false;
ちなみにこの暗黙の型変換を行わずに値を判定する事も出来る。 これを厳密等号(Strict Equal Operator)と呼び、===と記述する。
alert( new Number(1) === 1 ); // false alert( new String("hello") === "hello" ); // false alert( new Boolean(true) === true ); // false参照 ECMAScript Language Specification 11.9.4 The Strict Equals Operator ( === )
オブジェクト型の判定
さて、今回の我々のテーマである渡された値が配列型かどうかを判定する方法だが、判定する為にはオブジェクト型判定が必要となる。オブジェクト型判定の為にconstructorプロパティーを利用することが出来る。このプロパティは、全てのオブジェクトインスタンス上で利用可能であり、そのオブジェクトインスタンスが作られたコンストラクタ関数を返す事になっている。alert( new Number(1).constructor == Number ); // true function Dog( name ){ this.name; } alert( new Dog("pochi").constructor == Dog ); // true
オブジェクト型の判定にはinstanceof演算子を利用することも出来る。instanceof演算子を使うと、オブジェクトの継承関係も加味しつつオブジェクト型の判定を行うことが出来る。
alert( new Number(1) instanceof Number ); // true function Dog( name ){ this.name; } function RedDog( name ){ Dog.apply( this, arguments ); this.name; } RedDog.prototype = new Dog(); function BlueDog( name ){ Dog.apply( this, arguments ); this.name; } // BlueDog.prototype = new Dog(); alert( new RedDog("pochi") instanceof Dog ); // true alert( new BlueDog("taro") instanceof Dog ); // false
継承に関してはHow to “properly” create a custom object in JavaScript? - Stack Overflowを参照のこと
instanceof演算子は、prototypeの内容を追跡する機能がある。 prototypeの内容は、オブジェクトの継承関係に依って連鎖した状態になっており、この事をプロトタイプチェーン(Prototype Chaining)と呼ぶ。このinstanceof演算子は、このプロトタイプチェーンを追跡する機能を持っている。
オブジェクト型判定の大問題
そもそもコンパイル時の型という物を持たないJavaScriptで、型チェックを行うという事はどういうことなのだろうか。このことを考えるに当たり、次のサンプルを見て頂きたい。まず lib1.js として次のファイルを作成する。
function Test(name) { this.name = name; alert( "Test object is created." ); } alert( "Test is loaded." );page2.js
<html> <script src="lib1.js"></script> <script> parent.test(new Test("taro")); </script> </html>page1.js
<html> <script src="lib1.js"></script> <script> function test(test) { // true alert( new Test( "hello" ) instanceof Test ); // false! ってなんでやねん! alert( test instanceof Test ); } </script> <body> <iframe src="page2.html"></iframe> </body> </html>
上記の実験プログラムの2つのalertは、両方共trueを表示するだろうか。否。実際には、page1.jsの1つ目のalertでtrueを表示し、2つ目のalertでfalseを表示する。何故だろうか。
上記実験プログラムでは、page1.htmlと、そのiframe内のpage2.htmlは、別々に読み込まれる。当然lib1.js も、 page1.html と page2.html のそれぞれが、別々に読み込むことになる。よって、lib1.jsの中のTestクラスも2回読み込まれ、二回実行される。そしてそれぞれpage1.htmlで読み込まれたTestクラスと、page2.htmlで読み込まれたTestクラスは、2つの異なるクラスとして存在する事になる。
よってpage1.html がいうところの Testクラスと、page2.html がいうところの Testクラスは、別人ということになる。よって test instanceof Test の評価結果はfalseとなる。
上記実験プログラムで、Testクラスというユーザー定義クラスを複数のフレーム間で受け渡す際に発生する問題を表している。だがこの問題は、JavaScriptのシステムクラスでも発生する。 JavaScriptのシステムクラスも、上記の実験プログラム同様に各フレームごとに別々に読み込まれる仕様になっている為「あのフレームでいうところのArrayクラスは、このフレームでいうところのArrayクラスとは別物」という、極めて直感に反する結果となる。これが配列オブジェクト判定問題の核心である。
解決策
解決策としていくつかの方法が知られている。- ダック・タイピング
- Object.toString()を使う方法
- Array.isArray()を使う方法
- スクリプトの読み込みを一箇所でまとめる方法。
ダックタイピングを使う
ダック・タイピングとは (英語文献の検索へのリンク)(※日本語のリンクは貼らない。日本語のダック・タイピングに関する説明は、どれもこれも間違いが酷く読むに値しない。これに関する情報は、飽くまでも英語で読むべきだ。)
ダッグ・タイピングとは、ある特定の処理を、ある対象となるオブジェクトに行うに当たり、はっきりと明示的な継承関係を持たないオブジェクトに対しても、あるメソッドのセットを全て持ったオブジェクトインスタンスであれば、そのオブジェクトは当該処理に対する互換性があると見なして、処理を行う事だ。日本語のWikipediaで説明される様に、スクリプト言語に特有の性質の事では決して無い。コンパイル言語でも同様の考え方を実装する事は可能だ。
ここでは、決してダックタイピングをそのまま適用する事を指している訳ではなく、この考え方を応用して、間接的にArrayクラスを検出する事を指している。例えば、Arrayクラスが、length プロパティを持っている事、sliceメソッドを持っている事など、間接的な判断基準を使ってArrayクラスかどうかを判定する方法を指している。
length プロパティーの存在を使って Arrayオブジェクトか判定するのが一般的だが、Stringオブジェクトも lengthプロパティーを持っている為、なかなかうまくない。sliceやspliceの存在などもあわせてチェックする必要がある。
Object.prototype.toString() を使う方法
function isArray( o ) { return Object.prototype.toString.call( o ) == "[Object Array]"; }
これはどことなく裏技的な匂いがする方法だが、JavaScriptの正式な仕様ECMA262で定義された動作で、推奨されているらしい。※ IE7とIE9では[Object Object] を返すバグがあるという情報もあったので注意が必要。
参照 ECMAScript Language Specification - 15.2.4.2 Object.prototype.toString ( )
参照 javascript - How to detect if a variable is an array - Stack Overflow
Array.isArrayを使う方法
配列検知の問題を解決する為の方法が、新しい仕様で策定されているらしい。参照 ECMAScript Language Specification - ECMA-262 Edition 5.1
スクリプトの読み込みを一箇所でまとめる方法
筆者は、色々と複雑なテクニックを使うことに対して保守的だ。使うライブラリが複雑になればなる程、使うライブラリが増えれば増える程、バグが入り込む可能性も高くなる。よって、問題と直面した時、出来るだけ技を使わないで、必要な動作を実装したいと考える。出来れば、特殊なテクニックは使いたくない。そこで、筆者が考えた事は、スクリプトの読み込みを出来る限りひとつのフレームで行うことだ。この対策には、グローバル変数を使わない事が重要になる。他のフレームで処理を行う時は、その関数(やその関数を保管しているオブジェクト)をその該当フレームに受け渡した上で、利用する。
lib2.js
function init( global ) { global.proc1 = function() { }; global.proc2 = function() { }; }
などというようにライブラリを定義した上で
<html> <head> <script src="lib2.js"></script> <script> var global = {}; init( global ); // getGlobal() を他のフレームから // 呼び出す事でライブラリ・インスタンスを取得する。 function getGlobal(){ return global; } </script> </head> <body> </body> </html>という様にして getGlobal() を他のフレームから呼び出す事でライブラリ・インスタンスを取得する。こうすれば、フレーム間でシステムライブラリが共有されていなくとも正しく動作するようになる。この方が方法論としては無難だ。
さていかがだろうか。 筆者は、プログラミングでこの程度のことを知っている事は当然だと思う。人は、当然で当たり前なことをわざわざ文章に書きしたためたいとは思わないものではないか。よってブログにもわざわざ自明で当たり前で面白くない技術系の話しなど書きたいと感じない。筆者はどちらかというと、語学や音楽が苦手で、こちらの方がずっと驚きに満ちた事があると感じる。よってこのおかあつ日記は、語学の事ばかりが書かれる事になる。
だいたい上記のことは(一番最後の方法を除けば)全て英語で検索すれば、いくらでも書いてある。プログラミングなんか勉強しないで、語学を勉強したほうが、結果的にプログラミング力は伸びるのではないだろうか。
参考文献