CSSにおけるセレクタの固有性(詳細度)の計算式

固有性(specificity)の計算式


CSS3 ではセレクタの下記手順で固有性(specificity)を求めます(CSS2では否定セレクタが存在しないだけで計算式は同様)。

  • ID属性の数を a とする)
  • 他の属性セレクタ、擬似クラスの数を b とする
  • タイプセレクタ(要素)の数を c とする
  • 否定セレクタは引数と同じ数として計算される
  • 擬似要素を無視する。
  • a-b-cを連結して固有性(specificity)を求める

仕様では "#id" の固有性を 100 のように表しているため、b の数が10を超えると10進数の繰上げが発生しそうですが、連結なので繰り上げは発生しません。
例えば、「a=1, b= 11, c= 15」なら固有性は 1-11-15 のように連結されます。16進数で表すなら "1AF" となり、基数に制限のない数値と解釈してもいいかもしれません。

サンプルによる説明

<style type="text/css">
/* 固有性 = 1-0-0 */
#Sample { background-color: #fee; }

/* 固有性 = 0-11-0 */
.a.b.c.d.e.f.g.h.i.j.k.l { background-color: #eef; }
</style>
</head>
<body>
<div id="Sample" class="a b c d e f g h i j k l">#Sample.a.b.c.d.e.f.g.h.i.j.k.l</div>

固有性が 1-0-0 であるIDセレクタが優先されます。

DOMオブジェクトとスクリプトエンジンオブジェクト間の循環参照における IE6 SP2- のメモリリークパターン

概要

IE6 SP2- には下記条件でページの unload 時にメモリリークするバグが存在します。

  • 「DOMオブジェクト」と「スクリプトエンジンオブジェクト」間という異なるエンジン間で循環参照している
  • クロージャがローカル変数に格納されているDOMオブジェクトを参照可能な状態にある(参照可能なだけで条件が成立し、クロージャ内でローカル変数を使用しなくてもメモリリークパターンとなる)
  • Microsoft Internet Explorer 6 SP2-でscriptコードを実行している(IE6 SP3 で修正済)

メモリリークするコード

代表的な循環参照パターンは下記の通りです。

// DOM-script間の循環参照パターン[1] (メモリリークする)
(function () {
  var element = document.getElementById('Sample');
  
  element.onclick = function () {
    // element(DOM) -> onclick(DOM) -> function(script) -> element(DOM)
    // クロージャでローカル変数(element)への参照を保持する為、メモリリークする
  };
})();

// DOM-script間の循環参照パターン[2] (メモリリークする)
(function () {
  var element = document.getElementById('Sample');
  
  element.attachEvent('onclick', function () {
    // element(DOM) -> onclick(DOM) -> function(script) -> element(DOM)
    // クロージャでローカル変数(element)への参照を保持する為、メモリリークする
  });
})();

// DOM-script間の循環参照パターン[3] (メモリリークしない)
document.onclick = function () {
  // document(DOM) -> onclick(DOM) -> function(script) -> element(DOM)
  // グローバルオブジェクト(document)への参照を保持してもメモリリークしない
};

// DOM-script間の循環参照パターン[4] (メモリリークしない)
document.attachEvent('onclick', function () {
  // document(DOM) -> onclick(DOM) -> function(script) -> element(DOM)
  // グローバルオブジェクト(document)への参照を保持してもメモリリークしない
});

// script-script間の循環参照パターン[1] (メモリリークしない)
var obj = {};
obj.prop = obj; // 同一のscriptエンジン間の循環参照はscriptエンジンがメモリ管理する為、メモリリークしない

// DOM-DOM間の循環参照パターン[1] (メモリリークしない)
document.document.body.firstChild.parentNode; // document(DOM) -> body(DOM) -> firstChild(DOM) -> parentNode(DOM) === body(DOM)
// 同一のDOMプロセッサ間の循環参照はDOMプロセッサがメモリ管理する為、メモリリークしない(そもそも、何もしなくても初めから循環参照しているので、これでメモリリークしたら大変)
})();

メモリリークしないコード

循環参照しないパターンにする事でメモリリークパターンではなくなります。

// DOM-script間の循環参照しないパターン[1] (メモリリークしない)
(function () {
  var element = document.getElementById('Sample');
  
  element.onclick = function () {
    // element(DOM) -> onclick(DOM) -> function(script) -> element(script)
    // element = null; が実行されるまでは循環参照し、null が代入された時点から循環参照しなくなる
  };
  
  element.ondblclick = function () {
    // element(DOM) -> onclick(DOM) -> function(script) -> element(script)
    // element = null; が実行されるまでは循環参照し、null が代入された時点から循環参照しなくなる
  };
  
  element = null; // 使い終わったローカル変数に null を代入する事でスクリプトエンジンのオブジェクトにする
})();

// DOM-script間の循環参照しないパターン[2] (メモリリークしない)
(function () {
  var element = document.getElementById('Sample');

  function handleClick (event) {
    // handleClick(script) -> element(DOM)
    // window unload するまでは循環参照している
  }

  function handleUnload () {                      // (※jQuery にも同様の処理が入っています)
    element.detachEvent('onclick', handleClick);  // 「handleClick(script) -> element(DOM)」間の参照を切る事で循環参照しなくなる
    window.detachEvent('onunload', handleUnload); // 自身の循環参照も切っておく(理由は同上)
    element = null;                               // ついでに element(DOM) も element(script) にしておく
  }

  element.attachEvent('onclick', handleClick);
  window.attachEvent('onunload', handleUnload);
})();

// DOM-script間の循環参照しないパターン[3] (メモリリークしない)
(function () {
  function handleClick (event) {
    console.log(event);
    // 初めから循環参照していない
  }

  function init () {
    var element = document.getElementById('Sample');

    element.attachEvent('onclick', handleClick); // element(DOM) -> onclick(DOM) -> handleClick(script)
    // handleClick はローカル変数(element)を参照不可能な為、循環参照しない
  }
  
  init();
})();

クロージャのメモリ消費量

メモリリークではありませんが、クロージャには「クロージャが保持しているローカル変数は window unload するまでメモリに残り続ける」という性質があり、工夫することでメモリを節約出来ます。
前節の「メモリリークしないコード」ではメモリ消費量は次のようになり、パターン[3] が最もメモリに優しいコードになります。

  • パターン[1], [2] は window unload するまでクロージャがローカル変数 element の参照を保持する為、メモリを消費する
  • パターン[3] は window unload するまでクロージャが保持するローカル変数が存在しない為、メモリを消費しない

Function#call で循環参照しないパターン

一時的に参照を保持したいが、下位スコープまで影響を広げたくない場合に Function.prototype.call を利用すると解決できる場合があります。

'use script';

// DOM-script間の循環参照パターン[5] (メモリリークする)
(function (window) {
  window.attachEvent('onlock', function (event) {
    // window(DOM) -> onlock(DOM) -> function(script) -> window(DOM)
    // クロージャでローカル変数(window)への参照を保持する為、メモリリークする
  });
}(this));

// DOM-script間の循環参照しないパターン[4] (メモリリークしない)
(function () {
  this.attachEvent('onclick', function (event) {
    // window(DOM) -> onlock(DOM) -> function(script)
    // クロージャで参照可能なローカル変数(DOM)が存在しない為、循環参照せず、メモリリークしない
  });
}.call(this)); // Function.prototype.call は下位スコープには影響しない

結論

現在では IE6 SP2- に対応する機会が失われましたが、「DOM-script間の循環参照はバグを誘発しやすい」という理由からメモリパターンは回避する向きがあるようです。
また、前述のようにクロージャで参照を保持するコードはメモリに優しくない為、出来るだけクロージャを生産せず、クロージャが保持する参照を節約すると良いと思います。

typeof null === 'object' は ECMAScript 3 の仕様バグ

ECMAScript 3 の typeof 演算子

typeof演算子は対象の型を返す演算子ですが、null に適用すると "object" が返ってきます。

console.log(typeof null === 'object'); // true

では、null は Object 型なのか、というとそうではなくて仕様バグだったりします。

Changed 3 weeks ago by brendan

You know, this all came about because of rushing in early May 1995, which led to a leak of type tag representation shared by null and object types. But null means "no object", so it didn't raise hackles until it was too late to fix in Netscape 2, and after that we were loath to "fix" it and "break the web".

That argument only applies more in degree of web population now.

We have other fish to fry. This one was has been swallowed already. Let's not change typeof null for ES4 and work on more vital issues.

/be

#250 ((Resolved) "typeof null") – ECMAScript Bugs – Trac

簡単に説明すると、「納期に押されてやっちまったぜ!」的なコメントだそうで…。

ECMAScript 5.1 の typeof 演算子

ECMAScript 5.1 でも typeof 演算子の挙動は変わりませんが、Object型 の解説が寄り詳しくなったので意訳してみました。

typeof 演算子の結果
Undefined型 "undefined"
Null型 "object"
Boolean型 "boolean"
Number型 "number"
String型 "string"
Object型 (ネイティブオブジェクトで Call を持たないもの) "object"
Object型 (ネイティブオブジェクトで Call を持つもの) "function"
Object型 (ホストオブジェクト) "undefined", "boolean", "number", "string" を除く処理系定義

ES.next の typeof 演算子

ECMAScript 5.1 の後継に ES.next があり、上手くいけば ECMAScript 6 として仕様策定される見込みとなっています。
ES.next ではとうとう typeof 演算子の仕様バグが修正されました。

console.log(typeof null === 'null'); // true

現実的な対策

ES.next が来れば全て解決しますが、それまでは typeof 演算子の不可解な挙動とつきあっていくしかありません。
幸い、Null型は null という単一の値しか持ちませんので、厳密等価演算子で null型かどうかを判定することが出来ます。

var a = null;
console.log(a === null); // true

Date() と new Date() は等価ではない

概要

Native Object のコンストラクタの中には関数呼び出しとコンストラクタ呼び出しが同じ動作になるものがあります。例えば、Array()new Array() と等価です。

new Array(1, 2, 3); // [1, 2, 3]
Array(1, 2, 3);     // [1, 2, 3]

対して、Date()new Date() と等価ではありません。

new Date(2011, 0, 1, 0, 0); // Sat Jan 01 2011 00:00:00 GMT+0900 (Japan Standard Time)
Date(2011, 0, 1, 0, 0);     // 現在時刻 (Google Chrome 12, Firefox4, Opera 11.50, IE8 で確認)

関数呼び出しの Date() は引数を無視して現在時刻を返しているようです。

ECMAScript 3 では

ECMAScript 3 では次のように書かれています。

15.9.2 関数として呼ばれる Date コンストラク

コンストラクタとしてではなく関数として Date が呼出されるとき、それは現在時間 (UTC) のあらわす文字列を返す。
NOTE 関数呼出し Date(...) と、同じ引数を持つオブジェクト生成式 new Date(...) は、等価ではない。

http://www2u.biglobe.ne.jp/~oz-07ams/prog/ecma262r3/15-9_Date_Objects.html#section-15.9.2

なるほど。確かに Date は現在時刻を返しています。次の小節を読んでみます。

15.9.2.1 Date ( [ year [, month [, date [, hours [, minutes [, seconds [, ms ] ] ] ] ] ] ] )

すべたの引数は選択的である; 供給された引数は受け付けるが、それ以外は無視される。文字列が生成され、式 (new Date()).toString() によるものと同様の結果が返される。

http://www2u.biglobe.ne.jp/~oz-07ams/prog/ecma262r3/15-9_Date_Objects.html#section-15.9.2.1

"供給された引数は受け付ける" とありますが、受け付けた引数はどのように処理されるのでしょう…?私には矛盾しているように思えます。

(2011/07/31 15:47追記)

@rikuba さんからリプライを頂きました。ありがとうございます。


rikuba
原文は「any arguments supplied are accepted but are completely ignored」なので誤訳だと思います。「完全に無視される」ではないかと。

原文(ECMAScript 3)では次のように書かれています。

15.9.2.1 Date([ year [, month[, date[, hours [, minutes[, seconds[, ms]]]]]]])

All of the arguments are optional; any arguments supplied are accepted but are completely ignored. A string is created and returned as if by the expression (new Date()).toString().

http://bclary.com/2004/11/07/#a-15.9.2.1

下記は意訳です。

15.9.2.1 Date([ year [, month[, date[, hours [, minutes[, seconds[, ms]]]]]]])

全ての引数はオプションです。供給された全ての引数を受け入れますが、完全に無視されます。文字列(String)が生成され、式 (new Date()).toString() の評価値が返されます。

http://bclary.com/2004/11/07/#a-15.9.2.1

結論

Date()new Date() と等価ではありません。その点ははっきりしていますが、Date() で受け付けた引数の扱いがよくわかりません。謎です…。

Date() 呼び出し時の全ての実引数は無視され、(new Date()).toString() に等しいString値を返します。

MDN ではfor文の第一要素を初期化式(initial-expression)と説明している

概要

for文の第一要素で変数宣言するコードは次のようになります。

for (var i = 0; i < 10; i++) {
  alert(i);
}

この時、var i = 0 が「式」であるかのように誤解されることがあるようです。

ECMAScript 3 では

ECMAScript 3 では ExpressionNoInVariableDeclarationListNoIn の2つにわけて説明されています。

for (ExpressionNoIn; Expression ; Expression ) Statement
for ( var VariableDeclarationListNoIn; Expression ; Expression ) Statement
http://www2u.biglobe.ne.jp/~oz-07ams/prog/ecma262r3/12_Statements.html#section-12.6

VariableDeclarationListNoIn では変数文 (VariableStatement) と同じように機能します。これは「式」ではありません。

for (var i = 0, length = 10; i < length; i++) { // 第一要素は VariableDeclarationListNoIn
  alert(i);
}

ExpressionNoIn では「式」をカンマ演算子で列挙できます。

var i, length;                              // VariableStatement

for (i = 0, length = 10; i < length; i++) { // 第一要素は ExpressionNoIn
  alert(i);
}

MDN では

MDNでfor文の第一要素の説明を見てみます。

構文
for ([initial-expression]; [condition]; [final-expression])
   statement
Parameters

initial-expression
(代入式を含む) 式または変数宣言。たいていは、カウンタ変数を初期化するために使われます。この式では、var キーワードを用いて新しい変数を任意で宣言してもかまいません。これらの変数はループにローカルなものではありません。すなわち、これらは for ループが属するスコープと同じスコープ内にあります。この式の結果は捨て去られます。

https://developer.mozilla.org/ja/JavaScript/Reference/Statements/for

"式または変数宣言" は正当な説明だと思いますが、大本は initial-expression とあるので変数宣言も式であると勘違いされる可能性があります。
また、"この式では、var キーワードを用いて新しい変数を任意で宣言してもかまいません。" という記述は極めてわかりにくく、「var キーワードを使って代入式を宣言している」ようにも読み取れます。

JavaScript第5版』では

JavaScript第5版』(いわゆるサイ本)には次のように書かれています。

6.8 for文

...(中略)...
for文の書式は次のとおりです。

for (initialize; test; increment)
  statement

for文の働きは、for文と同じ処理を行うwhile文と比較するとよくわかると思います。

initialize;
while(test) {
  statement
  increment;
}

ループを行う前に initialize 式を1回だけ評価します。通常、initialize 式は代入などの副作用を伴います。JavaScript では、var文を使って変数を宣言することもできます。ループカウンタ変数の宣言と初期化が同時にできて便利です。...

JavaScript第5版 P90

やや複雑ですが、「initialize 式」と「変数宣言」をわけて解説されていることがわかります。「initialize 式」は英語にすれば "initial-expression" になりますから、MDN の 語源 出典はここにあるのかもしれません。ただ、"initial-expression" に変数宣言も含めるのは『JavaScript第5版』の著者の意図に反していると思います。

MDNから生じた勘違い

下記のような勘違いが生まれたことがありました。(説明の都合上、文面を多少換えています)

  1. for (var i = 0, length = 10; i < length; i++) という書き方もあるよ。」
  2. 「カンマ演算子を使っているのか。」
  3. 「カンマ演算子なんて出てきてないよ。」
  4. 「var a=1, b=2, c=3; をカンマ演算子というのか。勉強になった。」
  5. 「違うよ!全然違うよ!」
  6. MDCのfor文の説明には『for ([initial-expression]; [condition]; [final-expression]) (代入式を含む) 式または変数宣言。』とあるんだけど…。」(※現在のMDNは当時MDCという名前でした)
  7. 「var がついてなければ式。var がついていれば文。」
  8. 「なるほど。勉強になった。」

「変数宣言が式である -> 式であるならカンマ演算子が使えるはず」という流れ。確かに式ならカンマ演算子を使えますからそう解釈する気持ちは理解できます。

結論

for文の第一要素d変数初期化を行う場合、var を伴わない変数宣言は「式」ですが var を伴う変数宣言は「式」ではありません。var を伴う変数宣言を「文」と呼べるかは少し怪しい気もします(「文」なら "VariableStatement" を定義すればよく、わざわざ "var VariableDeclarationListNoIn" を定義しているところをみると「文」と区別されているように思えます)が、少なくとも「式」ではないと思います。

結論としては、MDN の "initial-expression" は「式のみを入れられる」という誤解を生むと思います。

ECMAScript 5 規定の undefined は書き換え不可能

概要

次のようなコードをよく見ます。

undefined = 1;      // 1

(function () {
  var undefined;    // 同名のローカル変数を定義

  alert(undefined); // undefined
})();

ところが、ECMAScript 5.1 規定の undefined は書き換え不可能([[Writable]]: false)です。

15.1.1.3 undefined # Ⓣ Ⓡ
The value of undefined is undefined (see 8.1). This property has the attributes { [[Writable]]: false, [[Enumerable]]: false, [[Configurable]]: false }.

http://es5.github.com/#x15.1.1.3

従って、ローカル変数を定義する必要がなくなります。

undefined = 1;    // 1
alert(undefined); // undefined (OK = Firefox 5 / NG = Google Chrome 12, Opera 11.50, IE8)

ローカル変数 undefined を定義する手法の問題点

ローカル変数 undefined は [[Writable]]: true ですので、ローカル変数定義後に値を書き換えることを許してしまいます。

undefined = 1;      // 1
alert(undefined);   // undefined

(function () {
  var undefined;    // 同名のローカル変数を定義

  undefined = 1;
  alert(undefined); // 1
})();

書き換え不可能なグローバル変数を定義する

ECMAScript 5.1 規定の Object.defineProperty を実装しているブラウザなら書き換え不可能なプロパティ undefined を定義できます。

/**
 * グローバルコード
 */
if (typeof Object.defineProperty === 'function') {
  Object.defineProperty(this, 'undefined', {value: void 0, writable: false, enumerable: false}); // [[Writable]]: false, [[Enumerable]]: false
} else {
  undefined = void 0;
}

undefined = 1;    // 1
alert(undefined); // undefined

Tweetログ

備忘録です。


think49
GC12 のグローバル変数 undefined は [[Writable]]: false ではない模様。 undefined = 1; alert(undefined); // 1
think49
Object.defineProperty(this, 'undefined', {configurable: true}); // TypeError: Cannot redefine property: defineProperty (GC12)
think49
残念ながら、既存の undefined に [[Writable]]: false を付加させるのは無理みたい。書き換え可能なのに definedProperty で再定義できないとは/
Object.defineProperty(this, "undefined", { writable: false }); の間違いではないでしょうかー?
configurable:falseのときにwritableをtrueからfalseにのみ書き換え可能なのはsemantics的な理由もあるけどもっと実用的な理由としてはObject.sealした後にfreezeしても問題ないようにというのが. とECMA262厨発言をしつつ.
think49
うむー。何か勘違いしてるかもしれないですが、MDC によると writable: false は規定値なので140文字の都合上省略してしまいました。仕様は読んでなかったので間違ってるかも…。
think49
実際は {value: void 0, writable: false, enumerable: false} ですが、期待通りに動作しませんでした。
think49
で、configurable: true が既存のプロパティ定義に必要なのかなと当たりをつけて試しましたが、動作せず。 「さて、どうしたものか?」って状況ですー。
undefinedがwirtable:falseでない点については仕様違反であっています. 今のcodeだと, GCの場合undefined = 1のおかげでtypeof undefiendが"number"になって, そちらのpathに入っていないですー.
think49
defineProperty の Attributes の解説は か。HasProperty が false を返す場合は何もしないわけだから false または undefined が規定値になる、と。
属性値のみ書き換える場合はGenericDescriptorを指定するといいです. この場合writableの値だけ変更したいのであれば, defineProperty(this, "undefined", { writable : false })ですね.
think49
ああ、わかりましたです。期待通りに動かないのは "undefind" の typo が原因ですね…。OTL お騒がせしました。
think49
GenericDescriptor はまだよくわかっていないのですが、例示されたコードから推測するに未指定のプロパティは既存の値をそのまま使う仕様になってる、という理解で合ってるでしょうか?
正確にはGenericDescriptorではないのですが><(writableがあるので), {writable: false}の場合, configurable, enumerableはabsentになります.
ものすごーく大雑把に言うと, absentは対象がすでに定義済みproperty descriptorの時は影響を与えないようにする値になりますー. つまり対象descriptorの値そのままということですねー.
think49
なるほど。ES5 も併せて読んでおぼろげながら理解できました。ありがとうございます。
think49
あれ…、となると MDN の writable の項の "Defaults to false." は間違いな気がする。
think49
やっぱり間違いなので先の3件ツイートは削除…。よく読まなきゃ。
think49
"Table 7 — Default Attribute Values" にデフォルト値がのってた。/
8.6.1 Property Attributes - Annotated ES5
think49
var obj = {};
Object.defineProperty(obj, 'hoge', {value: 'hoge'}); // [[Writable]]: false (default)
obj.hoge = 1;
obj.hoge; // hoge
think49
ようやく理解できた!規定値になる条件は「既存プロパティの再定義でなく、Attributes でプロパティを指定しなかった時」なんだなー。

DOM Events の「バブルアップ」の語源は MDN

バブルアップの語源

DOM L2 Events には親要素にイベントが伝播していくイベントバブル動作(Event bubbling)があり、これを「バブルアップ」と解説されているサイトをたまに見ますが、語源はMDNにあったようです。
(英語版には "bubble up" とあり、翻訳により発生した語句ではないことがわかります。)

上記の例では、modifyText() が addEventListener() を用いて登録された click イベントのリスナーになっています。table 中のどこをクリックしても、そのハンドラまでバブルアップし、modifyText() が実行されます。

https://developer.mozilla.org/ja/DOM/element.addEventListener#.e4.be.8b

DOM L2 Events では

DOM L2 Events では "上方向の伝播(upward propagation)" と説明されています。

そのとき,バブル動作イベントは,EventTargetの親連鎖を上位の方向にたどり,それに続く各EventTarget上に登録されたイベントリスナに対して検査を行うことによって見出される付加的なイベントリスナを誘発する。この上方向の伝播は,Documentまでそれを含んで継続される。

http://www.y-adagio.com/public/standards/tr_dom2_events/events.html#Events-flow-bubbling

感想

"上方向の伝播(upward propagation)" に表記を直した方が個人的にはわかりやすいと思います。
ただでさえイベントバブル動作は誤解されやすいというのに「バブルアップ」という新しい用語を作っても混乱が増すだけかなーと。