とことん標準に拘ったCSS切り替えスクリプト(その3)の続き。今回からは具体的な設計に入ります。
「CSSの集合」をオブジェクトとして考えてみました。これを暫定的に「CSSList」と呼ぶことにします。つまり、例えばCSSを全て無効にする場合には:
CSSList.disableAll();
このような形式になります(disableAllはCSSを全て無効にするメソッド)。
そんな感じである程度煮詰めたのですが、CSSの集合、というか「スタイルシートのリスト」を扱うオブジェクトは既に存在するではありませんか。そう、document.styleSheetsです。先程の例はこうなります:
document.styleSheets.disableAll();
このように既存のオブジェクトの拡張を行った方が直感的かと思われました。実装するには次のようにします。
document.styleSheets.disableAll = function(){
for (var i=0,len=this.length; i<len; i++)
this.item(i).disabled = true;
};
こんな調子でメソッドをどんどん加えていけば良いのですが、少し脱線して遊んでみました。
今更ですが、document.styleSheetsはオブジェクトです。ということはそれを作成した「鋳型」を参照できるかも知れません:
var StyleSheetList = document.styleSheets.constructor
こいつが関数オブジェクトであったなら占めたもの:
if (StyleSheetList instanceof Function) {
StyleSheetList.prototype.disableAll = function(){/*..*/};
}
document.styleSheets.disableAll();
このように、インターフェイスを拡張できます。ただ、今回の場合インスタンスがdocument.styleSheets唯一つしかあり得ないので、悲しいことに、document.styleSheetsに直接メソッドを追加する方法と何の違いもありません。何れにしろ「鋳型」、というかコンストラクタを参照できるかどうかは実装依存なので、ウェブページ用のスクリプトには使用できません。
因みにMozillaの場合、Elementインターフェイスを持ったオブジェクトを作成するコンストラクタは「Element」で参照できますから、こんなことができたりします:
Element.prototype.suicide = function(){
this.parentNode.removeChild(this);
};
//使用例
document.body.suicide();
脱線終わり。
というわけで、スタイルシートのリストを表すdocument.styleSheetsというオブジェクトを中心に、CSS切り替え機能を考えていくことになりました。主役は常にdocument.styleSheetsです。
オブジェクト指向なんて私は知りませんが、このようにオブジェクトを中心に考えると、JavaScriptは実に「しっくり」きます。「スタイルシートのリスト」を扱うオブジェクトが無かったら、自作していたでしょう(というかしました)。それだけの話です。
しかし自作した方が安全ではあります。
document.styleSheetsにメソッドを追加するとエラーになるブラウザがあるかも知れないこのような不安があるのです。この辺りを一応念頭に置きつつ、今後考えてゆくことになります。
とことん標準に拘ったCSS切り替えスクリプト(その2)の続き。これまでW3C DOMに準拠しているかどうかという観点で見てきましたが、今回はHTMLの仕様書で定義されている外部スタイルシートの「意味」を壊さないよう工夫すべし、という話です。
固定スタイルシート(永続スタイルシートとも呼ばれる)は、title属性が無く、rel属性が"stylesheet"なlink要素でリンクされた外部スタイルシートです。例:
<link rel="stylesheet" type="text/css" href="foo.css" />
固定スタイルシートは、その時有効になっている他のスタイルシートと共に必ず有効になっていなければなりません。HTML文書制作者はそのような振る舞いを期待して、title属性の無いlink要素でスタイルシートにリンクしている筈です。その存在意義から考えて、基本的に固定スタイルシートの切り替えを許可すべきではありません。
具体的には、全てのスタイルシートを無効にするときに限り、固定スタイルシートを無効にすることを許可し、全てのスタイルシートが無効になっている状態から任意のスタイルシートを有効にした際に、固定スタイルシートを有効にすることを強制します。それ以外のケースでは変更を許可しないようにします(常に有効)。
優先スタイルシートは、title属性でスタイル名を付けられ、rel属性が"stylesheet"なlink要素でリンクされたた外部スタイルシートです。例:
<link rel="stylesheet" type="text/css" title="foo" href="f1.css" />
<link rel="stylesheet" type="text/css" title="foo" href="f2.css" />
優先スタイルシートの有効/無効は、同じスタイル名を持つ他のスタイルシートと連動していなければなりません。この例でいえば、fooというスタイル名を持つf1.cssを有効(無効)にしたなら、同じくfooというスタイル名を持つf2.cssも同時に有効(無効)にしなければなりません。
CSS切り替えスクリプト制作者にとっては、優先スタイルシートと代替スタイルシートを区別する意味は殆どありません。rel属性の値がalternate stylesheetになっているだけです。切り替えに関しては優先スタイルシートと同様にスタイル名単位で扱います。
ただ、デフォルトの状態に戻す際に「お節介をするか否か」によっては優先と代替の区別は重要なのですが、この件に関しては別の機会に。
title属性がなく、rel属性が"altenate stylesheet"になっているlink要素でリンクされたスタイルシート、そのような似非代替スタイルシートについては、検知した際にエラーでも投げてしまいましょう。わらい。
style要素として埋め込まれたスタイルシート(特にtitle属性がある場合)は、固定と優先、どちらに属するのでしょうか。
これは不明としか言えないのですが、スタイル名を付けてグループ化して良いという記述が仕様書にはないので、title属性があろうとなかろうと、これを優先スタイルシートして扱うのは勝手な意味の付加になってしまいます。従って個人的には、style要素のスタイルシートは固定スタイルシートとして扱って良いと思います。(別に「も」で始まって「ら」で終わるブラウザを非難しているわけではありません。「とことん」拘るわけですから。)
JavaScriptのプロトタイプチェインについての、自分用の覚書です。殴り書きなのでくどいし推敲もしていません。用語も少し変かも。コンストラクタが生成したオブジェクトを便宜上インスタンスと書いたりその他色々。
function C(){} // コンストラクタ
function CC(){} // コンストラクタ
CC.prototype = new C;
var cc = new CC;
ここで、CCのインスタンスccのmプロパティ、cc.mを参照すると何が起こるだろうか。
まず、cc.mは存在しないから、ccの内部プロパティ[[Prototype]]からmを探す。これをcc.__proto__とすれば、即ちcc.__proto__.mを探す。但し実際にはcc.__proto__は不可視なので注意。
ここで、cc.__proto__には、インスタンスであるccがnewで生成されるその時、コンストラクタCCのprototypeプロパティが参照しているオブジェクト(この例ではCのインスタンスになっている)がセットされる。従って、cc.__proto__.mは、Cのインスタンスのmプロパティを参照することになる。
これも存在しないので、今度はcc.__proto__の[[Prototype]]内部プロパティからmを探す。cc.__proto__.__proto__と表記しよう。
ここで、cc.__proto__はCのインスタンスであった。従って、cc.__proto__.__proto__は「new C」された時点におけるコンストラクタCのprototypeプロパティが参照しているオブジェクトである。従って:
CC.prototype = new C;
が評価された以降に、C.prototypeが書き換えられていない限りにおいて、cc.__proto__.__proto__.m はC.prototype.mでアクセスすることが出来る。
以上より、各コンストラクタのprototypeプロパティが書き換えられていない限りにおいて、次のようにプロパティの検索が行われる。
cc.m
→ cc.__proto__.m [= CC.prototype.m]
→ (new C).__proto__.m [= C.prototype.m]
__proto__は[[Prototype]]内部プロパティ(不可視)である。cc.__proto__は、newによってccが生成された時点においてCC.prototypeと同じオブジェクトを参照している。全く同じことだが (new C).__proto__もその時点においてC.prototypeと同じオブジェクトを参照している。プロトタイプチェインによるプロパティの検索には、不可視の__proto__が参照されるが、コンストラクタのprototypeプロパティが参照されるわけではない。コンストラクタのprototypeプロパティが参照されるのは、インスタンスが生成される際だけである。
ダラダラ書いたけれど、ポイントは二つか。
[[Prototype]]プロパティが経由される(不可視)。コンストラクタのprototypeプロパティではない。prototypeプロパティが参照されるのは、インスタンスが生成される時だけである。この時、インスタンスの[[Prototype]]プロパティの参照先が、コンストラクタのprototypeプロパティの参照先と同一化される。確かめる。
function C(){}
var c = new C;
C.prototype = {x: 'x'};
c.x; // undefined
function C(){}
C.prototype = {x: 'x'};
var c = new C;
c.x; // 'x'
自明。コンストラクタのprototypeプロパティ自身を書き換えることの危険性というかナンセンスさというか、その辺が示唆されている。次、プロトタイプチェインを繋げたつもりが繋がっていない例:
function C(data){
this.data = data;
}
C.prototype.getData = function(){return this.data};
function CC(data){
this.data = data;
}
CC.prototype.prototype = C.prototype;
var cc = new CC;
cc.getData // undefined
cc.getDataのプロトタイプチェインを辿ってみる。
cc.getData(見つからない)cc.__proto__.getData = CC.prototype.getData (見つからない)CC.prototype.__proto__.getData = Object.prototype.getData (見つからない)Object.prototype.__proto__の値はnullであるから、プロトタイプチェイン終了、undefinedが返却される書き方を変えると、cc.getDataはこの場合、最終的にcc.__proto__.__proto__.__proto__までアクセスしてnullで終わってundefinedが返却される。くどいけれど実際にはCC.prototypeは参照しない点に注意。この場合たまたま
cc.__proto__とCC.prototypeの参照先が同じだから便宜上等号で結んでいる。
cc.__proto__.__proto__.__proto__
= CC.prototype.__proto__.__proto__
= Object.prototype.__proto__
= null
__proto__で表記している[[Prototype]]内部プロパティは、デフォルトではObject.prototypeへの参照になっている点に注意。newする際に[[Prototype]]内部プロパティがコンストラクタのそれの参照先に変更されると考えてよいだろう。そんなわけで:
CC.prototype.prototype = C.prototype;
ではなくて:
CC.prototype.__proto__ = C.prototype;
としたいわけだが、__proto__は不可視であってnewでインスタンスを生成する際(正確にはコンストラクタ関数の[[Construct]]内部プロパティが呼び出される際)以外には参照先を変更することは出来ない。
チェックポイント:
__proto__で表記してきた[[Prototype]]内部プロパティは、デフォルトでObject.prototypeオブジェクトを参照している。Object.prototypeの[[Prototype]]内部プロパティ(Object.prototype.__proto__)のみ、デフォルトで値がnullである(変更不可)。典拠として、ECMAScript262の邦訳を参照できる。
Oの[[Get]]メソッドがプロパティ名Pで呼出されると、次のステップがとられる
OがPという名前のプロパティを持っていなければ、ステップ 4 へ進む。- そのプロパティの値を取得する。
- Result(2) を返す。
Oの[[Prototype]]がnullならば、undefinedを返す。[[Prototype]]の[[Get]]メソッドを、プロパティ名Pで呼び出す。- Result(5) を返す。
FunctionオブジェクトFの[[Construct]]プロパティが呼出されるとき、次のステップが取られる:
- 新しい Native ECMAScript オブジェクトを生成する。
- Result(1) の
[[Class]]プロパティを"Object"に設定する。Fのprototypeプロパティの値を取得する。- Result(3) がオブジェクトならば、Result(1) の
[[Prototype]]プロパティを Result(3) に設定する。- Result(3) がオブジェクトでなければ、Result(1) の
[[Prototype]]プロパティを、セクション 15.2.3.1 で述べるオリジナルのObjectprototypeオブジェクトに設定する。Fの[[Call]]プロパティを呼び出す。Result(1) をthis値として提供し、引数値として[[Construct]]に渡された引数リストを提供する。- Type(Result(6)) が Object ならば、Result(6) を返す。
- Result(1) を返す。
DOM入門に仕様書を推奨する記事。
W3Cの勧告原文や和訳をあたってみれば間違いが無かろうと思いはしたものの、どうにも敷居が高くて挫折した。
適宜覚書: DOMツツキ より
私も挫折……というか、最初は仕様書を読んでも今一ピンときませんでした。使用例が載っていないからです。でも、それなら自分で「使用例」を作ってみれば良いのではないでしょうか。interface Documentなんてのが出てきたら、それを実際に参照してみたり作成してみたりするわけです。更にその「attributes」や「methods」も同様に。
何と言っても「勧告原文」をきちんと読めるようになるのが最良だと私は考えます。辛いかも知れませんが、その辛さを超えれば応用の幅が、他のアプローチによる理解に比べて比較になりません。次々新しい勧告が公開されるわけですから。
あるDOMのインターフェイスを理解しようとする際に私が必ずチェックするポイントは3つあります:
「1. 何を抽象化しているのか」を理解できれば、「attributes」や「methods」が自然に理解できます。言い換えると、何故その「attributes」や「methods」が存在するのかが理解できます。例えばElementインターフェイスはその名の通りXMLやHTMLの「要素」を抽象化したインターフェイスですが、
Element.setAttributeは属性を追加するメソッドであり、当然あるべきメソッドであると言えます。ちょっと違和感のあるものもありますけれども。
「2. 他のインターフェイスを継承しているか否か」を知らなければ、使用可能な「attributes」「methods」の全てが分かりません。例えばElementインターフェイスはNodeインターフェイスを継承しています。Nodeインターフェイスは要素、属性などの文書構成要素を一般的に抽象化したもので、cloneNodeという複製用のメソッドがあり、Elementインターフェイスを持ったオブジェクトでもこのメソッドを利用できます。
「3. どうやって作成或いは参照するのか」を知らなければ、そもそもそのインターフェイスを持ったオブジェクトを利用できません。例えばElementインターフェイスを持ったオブジェクトならば、
Document.createElementや
Document.createElementNSメソッドで作成できますし、
NodeList.item(index)等々で参照できます。仕様書を読む上で分かりにくいと思われるのはそれらの作成、参照方法を調べる方法です。手っ取り早い方法をとるなら「IDL Definition」を片っ端から調べます。
Attr getAttributeNode(in DOMString name);
これはElementインターフェイスのIDL DefinitionにおけるgetAttributeNodeメソッドについての定義です。メソッド名の前にAttrとありますが、これはAttrインターフェイスを持ったオブジェクトを返却するという意味です。
Attr createAttribute(in DOMString name)
raises(DOMException);
これはDocumentインターフェイスのIDL DefinitionにおけるcreateAttributeメソッドについての定義です。同じくメソッド名の前にAttrと書かれています。
しかしこの方法では分からないものもあります。例えばHTMLHtmlElementインターフェイス(DOM Level 1 HTMLに定義されています)。しかしこれはHTML要素を抽象化したインターフェイスであり、Elementインターフェイスを継承していることが分かれば作成、参照方法も自然に理解できる筈です。つまりHTMLという名前を持ったElementを作成、参照すれば良いのです。
interface HTMLDocument : Document {
attribute DOMString title;
readonly attribute DOMString referrer;
readonly attribute DOMString domain;
readonly attribute DOMString URL;
attribute HTMLElement body;
readonly attribute HTMLCollection images;
readonly attribute HTMLCollection applets;
readonly attribute HTMLCollection links;
readonly attribute HTMLCollection forms;
readonly attribute HTMLCollection anchors;
attribute DOMString cookie;
void open();
void close();
void write(in DOMString text);
void writeln(in DOMString text);
Element getElementById(in DOMString elementId);
NodeList getElementsByName(in DOMString elementName);
};
一行目:
interface HTMLDocument : Document
これはDocumentインターフェイスを継承したHTMLDocumentインターフェイスであることを表し、そのattribute、method群の厳密な定義がカーリブラケット{}内に記述されます。
二行目:
attribute DOMString title;
これはtitleという名前のattribute(JavaScriptではプロパティと呼ばれる)であることを表し、その値がDOMString型(JavaScriptではString型)であることを示しています。
三行目:
readonly attribute DOMString referrer;
これはreffererという名前のattributeであることを表し、readonly、つまり書き込み出来ません。値はDOMString型です。
十三行目:
void open();
これはopenという名前のmethodであることを表し、返り値はvoid、何も返しません。
十五行目:
void write(in DOMString text);
これはwriteという名前のmethodであることを表し、何も返しません。in DOMString textという部分が引数を定義します。この場合引数はDOMString型で、textで表記されています。このtextが何を意味するかは、writeメソッドの説明を読まなければ分かりません。引数が複数ある場合はカンマで区切られて列挙されます。
十七行目:
Element getElementById(in DOMString elementId);
これはgetElementByIdという名前のmethodであることを表し、Elementインターフェイスを持ったオブジェクトを返すことを表しています。引数はDOMString型で、elementIdで表されています。このelementIdの意味はgetElementByIdメソッドの説明を読まなければ分かりません。