JavaScriptによるブラウザ判別の実際
1 行でブラウザ判別を行うスクリプト (IE 用の日本語紹介記事、Firefox、Safari 用の日本語紹介記事) を発端に、SpiderMonkey の判別とブラウザ判別: Days on the Moon や JavaScript でのブラウザ判別 - 「仕様の盲点」ではなく、「プロパティの有無」を使う方がマシ - 2009年2月 - Blog - EOFなどの記事があがっています。
ここでは少し実際的なケースごとにサンプルコードを紹介していきたいと思います。(サンプルコードは適当に書いているので、不正確な箇所があるかもしれません。ご了承ください。間違い等ご指摘いただけると助かります。)
なお、これらは汎用的なJavaScriptライブラリを使っていれば意識しなくても良いケースがほとんどです。
分岐する必要がないケース
分岐する必要がないなら、ケースとしてあげる必要はないと思われるかもしれませんが、標準的な書き方ができるのに、ブラウザ依存な書き方をしてしまうケースが良くあります。
(最近はほとんど見かけませんが)document.allなどはその代表例でしょう。
var words; if (document.all) { // IE words = document.all.word } else { words = document.getElementsByName('word'); }
これはもちろん、
var words = document.getElementsByName('word');
と書けます。
ちなみに、モダンブラウザはdocument.allをクローキングしています。Opera 9.5 のテストビルドが document.all のクローキングをサポート - えむもじら
ブラウザの(JavaScriptエンジンの)実装を補完するケース
主にIEはJavaScriptエンジンの実装が他のエンジンに追いついていない面があり、他のエンジンで使える便利で高機能なメソッドが使えないことが多いです。それを自前で実装し、補完する方法があります。
prototype.jsの影響もあって、良く使われるものにArray#forEachがあります。
forEach | MDNより、
if (!Array.prototype.forEach) { Array.prototype.forEach = function(fun /*, thisp*/) { var len = this.length; if (typeof fun != "function") throw new TypeError(); var thisp = arguments[1]; for (var i = 0; i < len; i++) { if (i in this) fun.call(thisp, this[i], i, this); } }; }
ただし、こういったネイティブオブジェクトのprototypeを拡張する方法はそれによる副作用があるため使用には注意が必要です(特に、Objectのprototypeを拡張するのは厳禁といっても過言ではないでしょう。)。近年、prototype.jsからjQueryに人気が移ってきた背景にはこういった互換性の問題があると言われています。
そのため、下記のような汎用的な関数を定義するほうが良いでしょう。
function forEach(arry, func, thisObject){ for (var i = 0,len = arry.length; i < len; ++i) func.call(thisObject, arry[i], i, arry); }
ブラウザの(JavaScriptエンジンの)実装に従って分岐するケース
これはよく使われていて、且つ比較的問題の少ない望ましい方法です。
イベント周りでよく使われています。
function addEvent(node,type,func,useCapture){ if (node.addEventListener) { node.addEventListener(type, func, useCapture); } else if (node.attachEvent) { node.attachEvent('on'+type, func); } else { var _func = node['on'+type]; node['on'+type] = function(){ if (typeof _func === 'function')_func(); func(); } } }
何度も使うことが明らかなら、ブラウザ分岐はメイン処理の外に出したほうが効率的かもしれません。
var addEvent = (function (){ if (document.addEventListener) { return function(node,type,func,useCapture){ node.addEventListener(type, func, useCapture); }; } else if (document.attachEvent) { return function(node,type,func){ node.attachEvent('on'+type, func); } } else { return function(node,type,func){ var _func = node['on'+type]; node['on'+type] = function(){ if (typeof _func === 'function')_func(); func(); } }; } })();
メソッドだけでなくプロパティも同様です。
node.onclick = function(evt){ if (!evt && window.event) evt = window.event; var _node = evt.target || evt.srcElement; alert(node == _node);// true };
こういった分岐の際は、標準的な実装を優先するほうが一般的です。ただ、シェアを考えてIEの独自実装を優先するという選択もありかもしれません。
ブラウザのレンダリングエンジンの実装の違いを元に分岐するケース
例えば、条件付コメントを使って、position:fixedをサポートしないIE6用に要素の位置を固定する(JavaScriptで要素の位置をスクロールに追従させる)といったことがあります。
<!--[if IE 6]> <script type="text/javascript"> (function(){ var node; window.onscroll = function(){ if (!node) { node = document.getElementById('fixedElement'); node.style.position = 'absolute'; } node.style.top = document.documentElement.scrollTop + 'px'; }; })(); </script> <[endif]-->
なお、position:fixedの実現方法はいろいろあり、ここにあげたコードはあまり良いものとはいえません。
ブラウザのバグを回避するために分岐するケース
これは最も厄介です。バグに応じて適切な処理をすることが要求されるため、あまり一般化できません。
例えば、Safari3用のoAutoPagerizeは、document.implementation.createHTMLDocumentでDocumentを生成するとブラウザが落ちることがあるという致命的なバグのため、これを回避するようにしなければいけません。
こういった実装しているけどバグがある場合は、どのブラウザを使っているかという情報を元に分岐しなければいけません。
oAutoPagerizeのソースより。(window.getMatchedCSSRulesはWebKit(&JavaScriptCore,V8)で実装されているメソッド、typeof CharacterData != 'function' はGoogle Chromeの分岐)
var isSafari = window.getMatchedCSSRules && typeof CharacterData != 'function'; if (isSafari) return createDocumentFromString(html); var htmlDoc = document.implementation.createHTMLDocument ? document.implementation.createHTMLDocument('hogehoge') : document.implementation.createDocument(null, 'html', null);
ここではSafariのバグを例に挙げましたが、やはりバグで苦労するのはIEであることが経験上多いですね。(ただ、IEは「なんとかできる」ことが多いのが特徴的です。逆にOperaの場合はなんともならないことが多かったり……)
おまけ:ブラウザ分岐に失敗するケース
ページ内のID属性はグローバルオブジェクトとして登録されます。そのため、
<a href='/opera' id="opera">opera</a>
こういったHTMLがあるとwindow.operaは各ブラウザ(Firefoxなどは互換モード時のみ)で定義されてしまうことになります。
また、Firefoxはエラーコンソールに下記の警告を出します。
グローバルスコープで ID または name 属性値により要素を参照しています。代わりに W3C 標準の document.getElementById() を使用してください。
同様に、imgやform要素のname属性はdocumentに登録されるので、
<input type="button" name="evaluate" value="evaluate">
といった要素があれば、やはりdocument.evaluateは定義済みとなります。
滅多にあるケースではないですが、一応注意が必要です。こういった問題を少しでも回避したい場合、scriptをheadに書いて、bodyが読み込まれる前に判定をしておくのが良さそうですが、それが出来る環境ならそもそもそういったマークアップを回避できるかもしれません…。