Kanasan.JS JavaScript 第 5 版読書会 #72009年04月28日 00時36分

Kanasan.JS JavaScript 第 5 版読書会 #7 に行ってきました (当日のチャットログ)。範囲は前回に引き続き CSS の操作、そしてイベント周りと、一般に「JavaScript」といったとき話題になりやすい部分です。参加者のブログ記事は「JavaScriptでCSSとイベントを扱う from Kanasan.JS | Blog.37to.net」のほか読書会のページからたどれます。

display: inline-block;

サイ本では解説されていませんが、CSS の display プロパティの値 inline-block に関して話が盛り上がりました。inline-block 及びそれがどのようなレイアウトに使えるかについては以下で解説されています。

ところで、上記ページのサンプルはいずれも Firefox 2 以下で -moz-inline-box を使っています。しかし、-moz-inline-box が指定された要素の子要素は XUL のボックスモデルに従って整形されるため、そのままでは期待した表示になりません。ヨモツネットのサンプルではこの問題への対策として子要素に幅を指定していますが、別のアプローチとして子要素に -moz-box-flex プロパティを指定するという方法もあります。実際にこの方法を用いたページ送りのサンプルを作ってみました。HTML と CSS は次のようになります。

<ul>
  <li><a href="http://example.org/">1</a></li>
  <li><a href="http://example.org/">10</a></li>
  <li><a href="http://example.org/">100</a></li>
  <li><a href="http://example.org/">1000000000000</a></li>
</ul>
/* デフォルトのマージン、パディングが影響しないように。 */
ul, li {
  margin: 0;
  padding: 0;
}

ul {
  text-align: center;
}

li {
  display: -moz-inline-box; /* Firefox 2 以下用。 */
  display: inline-block;
  /* 内容の幅が大きいとき自動的に自分の幅も広がるよう、width ではなく
   * min-width を用いる。Firefox 2 以下では -moz-inline-box な要素の
   * min-width がボーダーボックスの最小幅として解釈されてしまうが、
   * ここではボーダー、パディングの幅が 0 なので影響しない。
   */
  min-width: 3em;
}
/* IE 6/7 では、デフォルトで inline でない要素は inline-block を
 * 指定した後、さらに inline を指定しないと inline-block 表示にならない。
 * margin をつけるのは inline-block 間の空白文字が表示されないため。
 */
* + html li {
  display: inline;
  margin: 0 0.2em;
}
* html li {
  display: inline;
  width: 3em; /* IE 6 は min-width 非対応だが width で代用可。 */
  margin: 0 0.2em;
}

li a {
  display: block;
  border: 0.1em solid;
  /* Firefox 2 以下でマージンボックスの幅が包含ブロック (この場合は
   * -moz-inline-box な要素の内容ボックス) の幅いっぱいに広がるように。
   */
  -moz-box-flex: 1;
}
/* IE 6 でボーダーボックス全体がリンクとして機能するように。 */
* html li a {
  height: 0;
}

この方法の利点として、-moz-inline-box な要素の幅が % 単位で指定されていても内容の幅がそれに追従してくれるといったことが挙げられます。

ちなみに、IE 7 以下での inline-block 表示に display: inline; zoom: 1; を用いず、いったん inline-block を指定して別の規則で inline を指定していたり、ひとつの規則内に _width: 3em; /margin: 0 0.2em; とまとめて書くのではなく、* + html ... { margin: ...; } * html ... { width: ...; margin: ...; } と IE 用の規則に分けて書いていたりするのは、できるだけ CSS の書式に則りたいという個人的な趣味によるものです。

MacIE 5 にも対応しようとすれば IE 6/7 向けの指定を /*\*/ ~ /**/ で囲む必要があるのかもしれませんが、確認できる環境がないのでそこまではしていません。Firefox 2 もすでにサポート期限が切れているので、最低限リンクが機能すればレイアウトが期待通りでなくてもかまわないという姿勢もありでしょう。

この例では -moz-inline-box な要素の子要素がブロック要素だったので子要素に -moz-box-flex を指定しましたが、Takazudo Clipping* のサンプルのように -moz-inline-box な要素の内容がテキストノードだったりインライン要素だったりした場合には、-moz-inline-box な要素に -moz-box-pack: center; を指定することで内容を中央ぞろえにできます。

このように、inline-block 表示はちょっと無理をすれば大抵のブラウザで可能ですが、別に無理をしなくてもいいときもあります。ページ送りに関していえば、各項目の幅をそろえなくてもいい (「1」の項目と「10」の項目の幅が異なってもいい)、かつ項目内で行の折り返しが発生してもいい (または内容が数字だけなので折り返しが発生しない) というのなら、そもそも inline-block を使う必要がありません。(IE 6 での不具合を避けるために inline-block を指定することはあるかもしれませんが。)

なお、zoom: 1; という指定はいわゆる CSS ハックの一環として用いられることが多いですが、このときそのまま zoom: 1; と書いてはいけません。/zoom: 1_zoom: 1* html ... { zoom: 1; } など、必ず IE のみに適用されるような書き方をしましょう。これは、WebKit も zoom プロパティをサポートしており、そうしたハックを必要としないブラウザに想定外の影響を与えるのを防ぐためです。もちろん、実際にズーム効果を求めて zoom プロパティを使用する場合はこの限りではありません。

rect 関数の書式

CSS の clip プロパティに rect 関数を指定することで、絶対配置の要素の一部だけ切り抜いて表示できます。rect 関数の引数には切り抜く矩形の各辺の位置を示す値を指定しますが、各引数の区切り文字には注意が必要です。CSS 2 だと、本文中ではスペース区切りなのにサンプルではコンマ区切りになっています。CSS 2.1 だと本文もコンマ区切りに統一され、製作者はコンマ区切りを使うべきであり、ユーザーエージェントはコンマ区切りをサポートしなければならないが、同時にスペース区切りをサポートしても良いということになっています。実際は IE 7 以下がスペース区切りしか受け付けてくれないので、スペース区切りを使うことになるでしょう。

このほかにも clip プロパティと rect 関数の挙動は CSS 2 と CSS 2.1 とで大きく変化しており、「clip CSS 表示と配置 (World Wide Web Guide)」に詳しくまとめられています。

getComputedStyle と currentStyle の違い

ある要素の実際の CSS プロパティの値を調べるのに、IE 以外のブラウザでは getComputedStyle メソッドが、IEでは currentStyle プロパティが使えます。しかし、この両者から得られる値は、前者が使用値 (used value) であるのに対し後者が指定値 (specified value) であるという違いがあります。二つの値に関しては以下の文書が参考になります。

JavaScript から CSS の値を操作するときは、指定値よりも使用値を得られたほうが嬉しい場面が多い気がします。

イベントハンドラ内容属性とスコープ

HTML 要素の属性としてイベントハンドラを記述した場合、そのイベントハンドラを実行する際のスコープにはその要素を表す DOM 要素ノードオブジェクト (これは this から参照できる値でもあります) が含まれます。this.value と書く代わりに単に value と書いても同様に動くということです。サイ本にはこうした動作に関して何も標準が存在しないと書かれていますが、HTML 5 草案ではイベントハンドラを実行する手順が明文化されています。

IE のメモリリーク

IE のメモリリーク問題は、IE 6/7 で部分的には解決されました (IE 6 は更新プログラムを適用した場合)。「部分的」という理由は以下の記事で解説されています。

IE 8 では COM という基盤技術に手を入れることで循環参照によるメモリリーク問題を根本的に解決したようです。

雑感

ブラウザ上で何かしようとするとネックになってくるのが IE の存在です。CSS に関しては IE 8 で大きく前進しましたが、イベントモデルをはじめとする DOM 周りはいまだ独自路線のままです。IE 9 でこれらの点が改善され、Range や XPath といった高度な機能を手軽に扱えるよう、Web 開発者の一人一人が積極的に発言し Microsoft に声を伝えていく必要があると思います。

ちなみに、今回の範囲である DOM Events や DOM Style については、WEB+DB PRESS Vol. 46/47 の連載『JavaScript + ブラウザ探検』でも解説しています。よろしければそちらもご覧ください。

Kanasan.JS Greasemonkey チュートリアル読書会2009年03月22日 19時28分

ちょうど 1 ヶ月前、2 月 22 日に開催された Kanasan.JS Greasemonkey チュートリアル読書会 (参加者ブログ一覧) に私も参加していました。時間がたちすぎて自分のメモからも情報を読み出せなくなりかけているのですが、何とかここに再現してまとめたいと思います。全体のまとめは「Firebugで作るGreasemonkeyスクリプト~入門と実践(From Kanasan.JS) | Blog.37to.net」に詳しいので、自分の気になった点に関してだけです。

HTML の解析

Greasemonkey スクリプトを書くには対象サイトの HTML を解析する必要があります。多くの人は Firebug を使っているようですが、私が使っているのは DOM Inspector です。これは単に昔から DOM Inspector を使っていたという慣れの問題ですが、一応 Firebug に比べて chrome 文書 (ブラウザ画面などを構成する文書) やアクセシブルノードの解析ができるといった利点もあります。

XPath での文字列置換

XPath には translate 関数がありますが、これは Perl の tr 演算子と同じで、ある 1 文字を別の 1 文字に置き換えることしかできません。ある部分文字列を別の部分文字列に置き換えたいときは、JavaScript と組み合わせたほうが早いです。Firefox 3 では EXSLT に対応しているので XSLT 中の XPath では正規表現による置換が行えますが、DOM 3 XPath を通じての拡張関数の利用はできないようです。

<?xml version="1.0" encoding="utf-8"?>
<?xml-stylesheet type="application/xml" href=""?>
<root xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
      xmlns:regexp="http://exslt.org/regular-expressions"
      xsl:version="1.0">
  <xsl:value-of select="regexp:replace('Hello, XPath world!', 'XPath', '', 'RegExp')"/>
</root>

document.evaluate の第 5 引数

document.evaluate メソッドの返り値である XPathResult オブジェクトを、別の evaluate メソッドの呼び出し時に第 5 引数として渡すと、その XPathResult オブジェクトが再利用されます。しかし、「再利用される」とは具体的にどうなることかという説明は DOM 3 XPath 仕様中になく、そもそも may be reused (強調は筆者による) なので確実に再利用されるという保証もありません。Firefox (Gecko) と Opera では第 5 引数に渡したオブジェクトが返り値となりますが、Safari (WebKit) では常に新しいオブジェクトを生成して返すようです。ちなみに Firefox では第 5 引数を指定すると意図しない結果になることがあります。

var result = document.evaluate("'foo'", document, null,
                               XPathResult.STRING_TYPE, null);
document.evaluate("'bar'", document, null,
                  XPathResult.STRING_TYPE, result);
result.stringValue;
// Firefox 3.6a1pre => "foobar" ("bar" を期待していたのに!)
// Opera 9.64       => "bar"
// Safari 3.1.2     => "foo"

順序付きの型と順序なしの型

XPathResult には結果の型として順序付きのもの (ORDERED_NODE_ITERATOR_TYPEORDERED_NODE_SNAPSHOT_TYPEFIRST_ORDERED_NODE_TYPE) と順序なしのもの (UNORDERED_NODE_ITERATOR_TYPEUNORDERED_NODE_SNAPSHOT_TYPEANY_UNORDERED_NODE_TYPE) があります。ソースコードを見る限り、WebKit では順序付きかどうかで処理を分けていますが、Gecko では順序なしでも順序付きと同等に扱っているようです。

雑感

初めての Kanasan さん抜きでの Kanasan.JS でしたが、主催の 37to さんをはじめスタッフの方々のおかげでスムーズな進行だったと思います。テーマのほうも初めてだったせいか初参加の方も多く、懇親会ではいろいろと興味深い話が聞けました。スタッフ、参加者の皆さんありがとうございました。「ブログを書くまでが勉強会です」ということで 1 ヶ月も Kanasan.JS を続けてしまい申し訳ありません m(_ _)m

WEB+DB PRESS Vol.49 WAI-ARIA2009年03月02日 17時35分

現在発売中の WEB+DB PRESS Vol. 49 にて Accessible Rich Internet Applications (WAI-ARIA) に関する記事を書かせていただきました。2009 年は Web アクセシビリティの年になるという噂をどこからともなく聞きつけてこのテーマにしたのですが、ちょうど WAI-ARIA 1.0 の最終草案も公開されよいタイミングになったのではと勝手に思っています。

WAI-ARIA とは何なのか、詳しいことは雑誌記事を参照していただくとして、簡単に言えば JavaScript で構築した RIA を機械的に認識するための仕様です。たとえば JavaScript でこったボタンを作ったとしましょう。利用者がどうやって「それがボタンである」ということを認識するかといえば、「周りから浮き出ていてマウスカーソルを乗せると色が変わる」といった感覚によっているわけです。

PC 上でグラフィカルなブラウザ経由でしか Web を使わないのならそれでもいいかもしれませんが、Web の閲覧環境というのは千差万別でしてそれでは困ることも多々あります。そこで、グラフィカルな表現に加えて「これはボタン、すなわち何らかの動作のトリガーとなるものである」という情報を埋め込んでおくことで、個々の閲覧環境に応じた表現をとることも可能となるのです。

さて、雑誌記事中では WAI-ARIA で拡張された tabindex 属性にも触れているのですが、それと関係して「Web2.0ナビ: 送信ボタンの上にリンクがあればtabindex=-1に」という記事がありました。フォーム内で、フォームコントロールのみをキーボードアクセス可能にするために、リンクの tabindex 属性に値 -1 を指定しようという内容です。しかし、これをやってはいけません。そのリンクがキーボードアクセス不能になり、アクセシビリティが低下してしまいます。

そもそも tabindex 属性によるアクセス順の指定には、ページ内での絶対的な順序しか指定できない (特定の要素からの相対的な順序の指定ができない) という欠点があり、乱用すれば直感的な操作を妨げてしまいます。tabindex 属性を使う際には以下 2 点に気をつけ、アクセス順を指定する属性としてではなく、フォーカス可能かどうか・キーボードアクセス可能かどうかを指定する属性として使うべきというのが私の考えです。

  1. tabindex 属性の値には基本的に 0 (フォーカス可能かつキーボードアクセス可能) か -1 (フォーカス可能だが直接的にキーボードアクセス可能ではない) かのみを用いる。
  2. HTML 4 で tabindex 属性が定義されている要素 (リンクとフォームコントロール) では基本的に tabindex 属性を用いない。

tabindex="-1" を使う場面の解説は雑誌記事に譲るとして、執筆にあたってつくづく感じたのは日本語資料の充実具合です。WCAG 2.0 が勧告からわずか 1 か月で日本語に訳され、WAI-ARIA 関連文書の日本語訳もそろっているなど、もう関係各所の皆様には足を向けて寝られません。

そんなこんなで 1 年間続けさせていただいた WEB+DB PRESS での連載も今回が最終回です。正直ライブラリ全盛のこの時期に DOM だの何だの書いていて受け入れられるのだろうかとも思いますが、何らかのヒントにでもなれば幸いです。読者の皆様、編集はじめ関係者の皆様、本当にありがとうございました。

SpiderMonkey の判別とブラウザ判別2009年02月01日 18時30分

1 行でブラウザ判別を行うスクリプト (IE 用の日本語紹介記事Firefox、Safari 用の日本語紹介記事) が注目を集めています。それによると Firefox の判別は次の 15 文字で行えるそうです。

//Firefox detector 2/3 by DoctorDan
FF=/a/[-1]=='a'

いったいなぜこれで Firefox が判別できるのか、少し探ってみました。

  1. 何を判別しているのか
  2. SpiderMonkey のソースコードを探る
  3. より短い SpiderMonkey の判別法
  4. 「ブラウザ判別」としての不備
  5. まとめ
  6. 参考文献

何を判別しているのか

実際に探っていく前にひとつ注意しなければいけないことがあります。それは、上記のコードが Firefox を判別するものではないということです。このコードには Web ブラウザ固有のオブジェクトモデルに関する情報が含まれておらず、正確には Firefox 等で使われている JavaScript エンジン「SpiderMonkey」を判別するためのものであるというべきでしょう。

SpiderMonkey のソースコードを探る

上記コードを見るに、SpiderMonkey では正規表現オブジェクトのインデックス -1 が source プロパティに対応するようです。しかしそうなる原因を探るにはどうすればよいのでしょうか。とりあえず -1 プロパティを取得するという JavaScript コードに対して、SpiderMonkey が生成する中間コードを見てみることにします。中間コードは、単体の SpiderMonkey に付属する JavaScript shell で使える dis 関数で確認できます。なお、都合によりここで使用する SpiderMonkey 及び参照する SpiderMonkey のソースコードは、Firefox 3.0 系列に含まれるものと同等のバージョン (SpiderMonkey 1.8) のものとします。

$ js
js> dis(function () /a/[-1]);
flags: LAMBDA EXPR_CLOSURE INTERPRETED
main:
00000:  regexp null
00003:  int8 -1
00005:  getelem
00006:  return
00007:  stop

Source notes:
  0:     5 [   5] pcbase   offset 5

これを見ると、数値的プロパティの取得には getelem 命令が使われることがわかります。そこで、SpiderMoneky のソースコードから、getelem 命令を処理している部分を見ると次のようになっています (一部改変)。

/* jsinterp.c */
4534: BEGIN_CASE(JSOP_GETELEM)
4535:   /* Open-coded ELEMENT_OP optimized for strings and dense arrays. */
4536:   lval = FETCH_OPND(-2);
4537:   rval = FETCH_OPND(-1);
4538:   if (JSVAL_IS_STRING(lval) && JSVAL_IS_INT(rval)) {
            /* 文字列の数値的プロパティに関する処理 */
4548:   }
4549: 
4550:   VALUE_TO_OBJECT(cx, -2, lval, obj);
4551:   if (JSVAL_IS_INT(rval)) {
4552:       if (OBJ_IS_DENSE_ARRAY(cx, obj)) {
                /* 密な配列の数値的プロパティに関する処理 */
4566:       }
4567:       id = INT_JSVAL_TO_JSID(rval);
4568:   } else {
4569:       if (!js_InternNonIntElementId(cx, obj, rval, &id))
4570:           goto error;
4571:   }
4572: 
4573:   if (!OBJ_GET_PROPERTY(cx, obj, id, &rval))
4574:       goto error;
4575: end_getelem:
4576:   regs.sp--;
4577:   STORE_OPND(-1, rval);
4578: END_CASE(JSOP_GETELEM)

ひとつのブロックが BEGIN で始まって END で終わっているような気がしますが、これは C 言語のソースコードです。今の場合、lval には正規表現オブジェクト (へのポインタ)、rval には整数値 -1 が含まれることになります。lval、rval はともに jsval 型ですが、プロパティの操作に当たっては JSObject へのポインタ型と jsid 型の値を使うので、4550 行目で正規表現オブジェクト (へのポインタ) を JSObject へのポインタ型に、4567 行目で整数値を jsid 型に変換しています。

4573 行目のいざプロパティを取得する段では、取得結果を収める変数として rval を再利用しています。正規表現オブジェクトの場合、OBJ_GET_PROPERTY マクロによるプロパティの取得は js_GetProperty 関数 (JS_GetProperty 関数とは別物。大文字の JS から始まる関数は外部 API) を経て js_GetPropertyHelper 関数の呼び出しにつながります。

/* jsobj.c */
3644: JSBool
3645: js_GetPropertyHelper(JSContext *cx, JSObject *obj, jsid id, jsval *vp,
3646:                      JSPropCacheEntry **entryp)
3647: {
3648:     uint32 type;
3649:     int protoIndex;
3650:     JSObject *obj2;
3651:     JSProperty *prop;
3652:     JSScopeProperty *sprop;
3653: 
3654:     /* Convert string indices to integers if appropriate. */
3655:     CHECK_FOR_STRING_INDEX(id);
3656:     JS_COUNT_OPERATION(cx, JSOW_GET_PROPERTY);
3657: 
3658:     type = OBJ_SCOPE(obj)->shape;
3659:     protoIndex = js_LookupPropertyWithFlags(cx, obj, id, 0, &obj2, &prop);
3660:     if (protoIndex < 0)
3661:         return JS_FALSE;
3662:     if (!prop) {
3663:         jsbytecode *pc;
3664: 
3665:         *vp = JSVAL_VOID;
3666: 
3667:         if (!OBJ_GET_CLASS(cx, obj)->getProperty(cx, obj, ID_TO_VALUE(id), vp))
3668:             return JS_FALSE;
3669: 
              /* プロパティキャッシュに関する処理 */
              /* 最終的にプロパティが存在しなかった場合の処理 */
3716:         return JS_TRUE;
3717:     }
3718: 
          /* すでにプロパティが存在する場合の処理 */
          /* プロパティキャッシュに関する処理 */
3733:     return JS_TRUE;
3734: }

3659 行目でプロパティの探索を行いますが、-1 という名前のプロパティは本来正規表現オブジェクトに存在しないので見つからず、変数 prop の値は NULL になります。すると、3667 行目で、正規表現オブジェクトに共通するプロパティ取得時用フック関数 (regexp_getProperty 関数) が、整数 -1 (を表す jsval 型の値) とともに呼び出されるのです。

/* jsregexp.c */
3595: enum regexp_tinyid {
3596:     REGEXP_SOURCE       = -1,
3597:     REGEXP_GLOBAL       = -2,
3598:     REGEXP_IGNORE_CASE  = -3,
3599:     REGEXP_LAST_INDEX   = -4,
3600:     REGEXP_MULTILINE    = -5,
3601:     REGEXP_STICKY       = -6
3602: };

3607: static JSPropertySpec regexp_props[] = {
3608:     {"source",     REGEXP_SOURCE,      RO_REGEXP_PROP_ATTRS,0,0},
3609:     {"global",     REGEXP_GLOBAL,      RO_REGEXP_PROP_ATTRS,0,0},
3610:     {"ignoreCase", REGEXP_IGNORE_CASE, RO_REGEXP_PROP_ATTRS,0,0},
3611:     {"lastIndex",  REGEXP_LAST_INDEX,  REGEXP_PROP_ATTRS,0,0},
3612:     {"multiline",  REGEXP_MULTILINE,   RO_REGEXP_PROP_ATTRS,0,0},
3613:     {"sticky",     REGEXP_STICKY,      RO_REGEXP_PROP_ATTRS,0,0},
3614:     {0,0,0,0,0}
3615: };
3616: 
3617: static JSBool
3618: regexp_getProperty(JSContext *cx, JSObject *obj, jsval id, jsval *vp)
3619: {
3620:     jsint slot;
3621:     JSRegExp *re;
3622: 
          /* 前処理 */
3630:     slot = JSVAL_TO_INT(id);
3631:     if (slot == REGEXP_LAST_INDEX)
3632:         return JS_GetReservedSlot(cx, obj, 0, vp);
3633: 
3634:     JS_LOCK_OBJ(cx, obj);
3635:     re = (JSRegExp *) JS_GetPrivate(cx, obj);
3636:     if (re) {
3637:         switch (slot) {
3638:           case REGEXP_SOURCE:
3639:             *vp = STRING_TO_JSVAL(re->source);
3640:             break;
3641:           case REGEXP_GLOBAL:
3642:             *vp = BOOLEAN_TO_JSVAL((re->flags & JSREG_GLOB) != 0);
3643:             break;
3644:           case REGEXP_IGNORE_CASE:
3645:             *vp = BOOLEAN_TO_JSVAL((re->flags & JSREG_FOLD) != 0);
3646:             break;
3647:           case REGEXP_MULTILINE:
3648:             *vp = BOOLEAN_TO_JSVAL((re->flags & JSREG_MULTILINE) != 0);
3649:             break;
3650:           case REGEXP_STICKY:
3651:             *vp = BOOLEAN_TO_JSVAL((re->flags & JSREG_STICKY) != 0);
3652:             break;
3653:         }
3654:     }
3655:     JS_UNLOCK_OBJ(cx, obj);
3656:     return JS_TRUE;
3657: }

regexp_getProperty 関数内では、3630 行目で変数 slot に id の値である -1 が収められます。3638 行目でそれが定数 REGEXP_SOURCE と一致することで、-1 というプロパティ名に対し source プロパティと同じ値が返ってくるのです。

source に対する -1 のような、プロパティを表す整数を tiny id と呼びます。SpiderMonkey では、プロパティごとにゲッタ / セッタ用フック関数を設定するのではなく、特定のオブジェクトの全プロパティに共通するゲッタ / セッタ用フック関数を使うと、JavaScript コードからの tiny id によるアクセスがプロパティ名によるアクセスと同一視されてしまうそうです。

より短い SpiderMonkey の判別法

外部から tiny id を使ってプロパティにアクセスできるのは正規表現オブジェクトに限りません。文字列の場合は length プロパティに対する tiny id として -1 が割り当てられ、それにより外部から参照できます。これを使えば、結果が真偽値となるような SpiderMonkey の判別は次の 12 文字で可能です。

SM=""[-1]==0

「ブラウザ判別」としての不備

最初に紹介したページに掲載されている判別法は、いずれもブラウザの JavaScript エンジンの差異を用いたものです。これらは JavaScript エンジンの内部実装に大きく依存するものであり、エンジンのちょっとした変更で期待した動作をしなくなる可能性も否定できません。実際、そこで Firefox 3 の判別用と紹介されている以下のコードの結果は、Firefox 3.1 では false となります。SpiderMonkey は現在、関数オブジェクトでは古びて非効率的な方法を捨て、プロパティごとにゲッタ用フック関数を指定するようにしたからです。

//Firefox 3 by me:-
FF3=(function x(){})[-5]=='x'

また、一般にブラウザによってスクリプトの動作が違うといったとき、その原因がスクリプトエンジンであることはあまりありません。多くの場合はレイアウトエンジン (ここでは DOM 等オブジェクトモデルの提供もレイアウトエンジンの役割に含めます) の動作の違いによるものです。Safari と Google Chrome はともにレイアウトエンジンに WebCore を用いていますが、スクリプトエンジンはそれぞれ JavaScriptCore と V8 と異なっています。WebCore に起因する Safari の問題を回避するのに JavaScriptCore の判別を使っていれば、Google Chrome が同じ問題を持っていたときに対処できません。

特定ブラウザでの問題に対処するときは、プロパティやメソッドの存在確認や動作確認により処理を切り分けるべきです。それで判定できないときはブラウザ判別を行うことになりますが、そこで調べるのはブラウザではなくブラウザのレイアウトエンジンです。navigator.userAgent プロパティから調べるのなら、"Firefox" や "Safari" ではなく "Gecko/" や "WebKit" といった文字列の有無で判断しましょう。いずれにしても、問題がレイアウトエンジンにあるのなら、上記ページで紹介されているようなスクリプトエンジンに基づくブラウザ判別を使ってはいけません

まとめ

  • SpiderMonkey では、プロパティを表す tiny id によりプロパティにアクセスできることがあります。
  • 問題解決のためのブラウザ判別にはプロパティ / メソッドの存在確認や動作確認を用いるか、それが無理ならレイアウトエンジンを調べましょう。
  • スクリプトエンジンの判別はレイアウトエンジンの判別になりません。

参考文献

フォームコントロールのデフォルト値2009年01月19日 23時25分

WebKit のコントロール値キャッシュ対策

日本野望の会-Yabooo.org > Safari/Webkitのおせっかいキャッシュとその対策。」にコメントしたはずなのですが、いつの間にかコメントが消えているようなので推敲の上再掲。

上記のページで問題にしているのは、Safari において bfcache を無効にしていても、動的に追加したフォームコントロールの値がキャッシュされた値に書き換えられてしまうことです。これに対し、文書中のコントロールをすべて記憶し、文書のアンロード時にそれらの名前 (name 属性の値) を変更することで解決を図っています。

しかし、書き換えられるのは一時的な値のみで、コントロールのデフォルト値 (フォームをリセットしたときに設定される値) まで変化するわけではありません。ならば、コントロールが文書中に挿入されたときに、その値をデフォルト値に設定してやればすむことではないでしょうか。この考えに基づいた解決策が以下になります。

// bfcache を無効化しているときにこの問題は発生する。
window.onunload = window.onunload || function () {};

if (navigator.userAgent.indexOf("WebKit") != -1) {
  // 動的 (DOMContentLoaded イベント発生後) に
  // 追加されるコントロールのみを処理の対象とする。
  document.addEventListener("DOMContentLoaded", function () {
    var states = {
      radio:    "checked",
      checkbox: "checked",
      option:   "selected",
      OPTION:   "selected"
    };
    var defaults = {
      value:    "defaultValue",
      checked:  "defaultChecked",
      selected: "defaultSelected"
    };
    function resetState(control) {
      // option 要素ノードに type プロパティは存在しないので、
      // その場合は nodeName プロパティを参照する。
      var state = states[control.type || control.nodeName] || "value";
      control[state] = control[defaults[state]];
    }
    document.body.addEventListener("DOMNodeInserted", function (event) {
      var target = event.target;
      // target はテキストノードの場合もあるので、
      // tagName プロパティではなく nodeName プロパティを用いる。
      var name = target.nodeName.toLowerCase();
      if (name == "input" || name == "textarea" || name == "option") {
        resetState(target);
      } else if (target.nodeType == target.ELEMENT_NODE) {
        var controls = target.querySelectorAll("input, textarea, option");
        Array.prototype.forEach.call(controls, resetState);
      }
    }, false);
  }, false);
}

DOMContentLoaded イベントに対応しているのは Safari 3.1 以上なので、同じく Safari 3.1 以上で利用可能な Selectors API の querySelectorAll メソッドを使っています。Safari 3.0 にも対応させたいときは、DOMContentLoaded の使用を避け、querySelectorAll の代わりに getElementsByTagName なり XPath なりを使う必要があります。配列拡張の forEach メソッドに関しては Safari 3.0 でも使えるので問題ありません。

注意点として、以下のように value プロパティだけ設定したコントロールを追加すると、その値が空になってしまうことがあります。defaultValue プロパティも同時に設定するか、setAttribute メソッドで value 属性の値を設定しなければなりません。

// NG
var input = document.createElement("input");
input.type = "text";
input.value = "foo";
container.appendChild(input);
// OK
var input = document.createElement("input");
input.type = "text";
input.defaultValue = input.value = "foo";
container.appendChild(input);

コントロール値に関するプロパティ

上で触れたように、コントロールの値または状態を取得するプロパティには、現在の値を表すものとデフォルトの値を表すものがあります。これらのプロパティは DOM 2 HTML で以下のように定義されています。

value プロパティ (input 要素)
type 属性の値が "text"、"file"、"password" のいずれかならば、コントロールの現在の値を表す。このとき、このプロパティの値を変更しても value 属性の値は変更されない。
type 属性の値が "button"、"hidden"、"submit"、"reset"、"image"、"checkbox"、"radio" のいずれかならば、value 属性の値を表す。
defaultValue プロパティ (input 要素)
type 属性の値が "text"、"file"、"password" のいずれかならば、value 属性の値を表す。
value プロパティ (textarea 要素)
コントロールの現在の値を表す。このプロパティの値を変更しても要素の内容は変更されない。
defaultValue プロパティ (textarea 要素)
要素の内容を表す。
checked プロパティ (input 要素)
type 属性の値が "radio" または "checkbox" ならば、コントロールの現在の状態を表す。このプロパティの値を変更しても checked 属性の存在は変更されない。
defaultChecked プロパティ (input 要素)
type 属性の値が "radio" または "checkbox" ならば、checked 属性の存在を表す。
selected プロパティ (option 要素)
コントロールの現在の状態を表す。このプロパティの値を変更しても selected 属性の存在は変更されない。
defaultSelected プロパティ (option 要素)
selected 属性の存在を表す。

各ブラウザの実装は以下のようになっています。下の表で、A は要素の属性、P は現在値を表すプロパティ、DP はデフォルト値を表すプロパティを意味します。type が text、設定が A、取得が P の行は、type 属性の値が "text" である input 要素に対して、setAttribute メソッドを用いて value 属性の値を "foo" に設定し、その要素を文書中に挿入した後、value プロパティを用いて取得した値を表しています。type が option、設定が DP、取得が A の行は、option 要素に対して、defaultSelected プロパティの値を true に設定し、その要素を文書中に挿入した後、getAttribute メソッドを用いて取得した selected 属性の値です。(IE 8 RC1 はバージョン 8.0.6001.18344 時点のもの。いずれも標準モードの場合。)

フォームコントロールの値または状態の設定・取得結果一覧
type 設定 取得 IE 7 IE 8 RC1 Firefox 3 Safari 3.1 Opera 9.6
text A A "foo" "foo" "foo" "foo" "foo"
P "foo" "foo" "foo" "foo" "foo"
DP "" "" "foo" "foo" "foo"
P A "foo" "foo" null null null
P "foo" "foo" "foo" "foo" "foo"
DP "" "" "" "" ""
DP A "" null "foo" "foo" "foo"
P "" "" "foo" "foo" "foo"
DP "foo" "foo" "foo" "foo" "foo"
password A A "foo" "foo" "foo" "foo" "foo"
P "foo" "foo" "foo" "foo" "foo"
DP "" "" "foo" "foo" "foo"
P A "foo" "foo" null null null
P "foo" "foo" "foo" "foo" "foo"
DP "" "" "" "" ""
DP A "" null "foo" "foo" "foo"
P "" "" "foo" "foo" "foo"
DP "foo" "foo" "foo" "foo" "foo"
checkbox A A false "checked" "checked" "checked" "checked"
P false true true true true
DP false true true true true
P A false "" null null null
P false true true true true
DP false false false false false
DP A true "checked" "" "" "true"
P true true true true true
DP true true true true true
radio A A false "checked" "checked" "checked" "checked"
P false true true true true
DP false true true true true
P A false "" null null null
P false true true true true
DP false false false false false
DP A true "checked" "" "" "true"
P true true true true true
DP true true true true true
submit A A "foo" "foo" "foo" "foo" "foo"
P "foo" "foo" "foo" "foo" "foo"
DP "" "" "foo" "foo" "foo"
P A "foo" "foo" "foo" "foo" "foo"
P "foo" "foo" "foo" "foo" "foo"
DP "" "" "foo" "foo" "foo"
DP A "クエリ送信" "Submit Query" "foo" "foo" "foo"
P "クエリ送信" "Submit Query" "foo" "foo" "foo"
DP "foo" "foo" "foo" "foo" "foo"
reset A A "foo" "foo" "foo" "foo" "foo"
P "foo" "foo" "foo" "foo" "foo"
DP "" "" "foo" "foo" "foo"
P A "foo" "foo" "foo" "foo" "foo"
P "foo" "foo" "foo" "foo" "foo"
DP "" "" "foo" "foo" "foo"
DP A "リセット" "Reset" "foo" "foo" "foo"
P "リセット" "Reset" "foo" "foo" "foo"
DP "foo" "foo" "foo" "foo" "foo"
button A A "foo" "foo" "foo" "foo" "foo"
P "foo" "foo" "foo" "foo" "foo"
DP "" "" "foo" "foo" "foo"
P A "foo" "foo" "foo" "foo" "foo"
P "foo" "foo" "foo" "foo" "foo"
DP "" "" "foo" "foo" "foo"
DP A "" null "foo" "foo" "foo"
P "" "" "foo" "foo" "foo"
DP "foo" "foo" "foo" "foo" "foo"
image A A "foo" "foo" "foo" "foo" "foo"
P "foo" "foo" "foo" "foo" "foo"
DP "foo" "foo" "foo" "foo" "foo"
P A "foo" "foo" "foo" "foo" "foo"
P "foo" "foo" "foo" "foo" "foo"
DP "foo" "foo" "foo" "foo" "foo"
DP A "foo" "foo" "foo" "foo" "foo"
P "foo" "foo" "foo" "foo" "foo"
DP "foo" "foo" "foo" "foo" "foo"
file A A "" null "foo" "foo" "foo"
P "" "" "" "" ""
DP "" "" "foo" "foo" "foo"
P A "" null 例外発生 null null
P "" "" 例外発生 "" ""
DP "" "" 例外発生 "" ""
DP A "" null "foo" "foo" "foo"
P "" "" "" "" ""
DP "foo" "foo" "foo" "foo" "foo"
hidden A A "foo" "foo" "foo" "foo" "foo"
P "foo" "foo" "foo" "foo" "foo"
DP "" "" "foo" "foo" "foo"
P A "foo" "foo" "foo" "foo" "foo"
P "foo" "foo" "foo" "foo" "foo"
DP "" "" "foo" "foo" "foo"
DP A "" null "foo" "foo" "foo"
P "" "" "foo" "foo" "foo"
DP "foo" "foo" "foo" "foo" "foo"
textarea A A "foo" "foo" "foo" "foo" "foo"
P "foo" "foo" "foo" "foo" "foo"
DP "" "" "foo" "foo" "foo"
P A "foo" "foo" "" "" ""
P "foo" "foo" "foo" "foo" "foo"
DP "" "" "" "" ""
DP A "" "" "foo" "foo" "foo"
P "" "" "foo" "foo" "foo"
DP "foo" "foo" "foo" "foo" "foo"
option A A true "selected" "selected" "selected" "selected"
P true true true true true
DP false false true true true
P A true "selected" null null null
P true true true true true
DP false false false false false
DP A true "selected" "" "" "true"
P true true true true true
DP true true true true true

IE 7 以下での getAttribute / setAttribute メソッドはプロパティの取得・設定と基本的に同じです。ファイル選択コントロールの value プロパティに値を代入しても単に無視されるだけですが、Firefox では例外が発生します。このことと、デフォルトの状態を表すプロパティを true に設定したときの論理型属性の値、そして IE を除けば、各ブラウザとも同じ結果になっています。

まとめ

フォームコントロールのデフォルトの値または状態を用いると、スクリプトが簡単に書けるようになることがあります。