プロトタイプ=見本となるオブジェクト
JavaScriptを始めたころはprototype
オブジェクトが何なのかよく分からなかったけど、その名の通り「見本」あるいは「原型」なんだと分かれば、コンストラクタ関数とprototype
オブジェクトの関係は単純だと思える。
例として、人を表すオブジェクトを考えてみる。姓を表すlastName
プロパティと、名を表すfirstName
プロパティ、そしてそれらをつなげた文字列を返すgetFullName
メソッドを定義する。
var person = { firstName: '右京', lastName: '杉下', getFullName: function () { return this.lastName + ' ' + this.firstName; } };
さらに違う人物を表すオブジェクトを作ることになったとする。
var another = { firstName: '享', lastName: '甲斐', getFullName: function () { return this.lastName + ' ' + this.firstName; } };
さらに違う人物を……、いや、これだと何度も同じこと(getFullName
の関数)を書かなくてはいけない。こういうときは、適当な関数を用意し、そのprototype
プロパティに見本となるオブジェクトを代入するとよい。new
演算子を使ってその関数を呼び出すことで、そのprototype
オブジェクトの複製とでもいうべきオブジェクトが作られる。
var person = { firstName: '右京', lastName: '杉下', getFullName: function () { return this.lastName + ' ' + this.firstName; } }; // 適当な関数を用意して(このような関数は一般に大文字で始まる名前が付けられる) function Person() {} // 見本(prototype)となるオブジェクトを設定する Person.prototype = person; // personの複製のようなオブジェクトを作る var another = new Person(); // 姓・名は上書きする another.firstName = '享'; another.lastName = '甲斐'; // getFullName関数はそのままpersonと同じのを使えばよいので上書きしない // 実行してみる alert(another.getFullName()); // "甲斐 享" // さらにもう一人 var another1 = new Person(); another1.firstName = '尊'; another1.lastName = '神戸'; alert(another1.getFullName()); // "神戸 尊"
getFullName
の関数を書くのは1回きりで済むようになった。でも、これだと少し困ることがある。それはnew Person()
で作ったオブジェクトのプロパティを上書きするのを忘れたときに起こる。
var another2 = new Person(); another2.firstName = '薫'; // lastNameを上書きするのを忘れた! alert(another2.getFullName()); // "杉下 薫" (ノ∀`)
lastName
プロパティを上書きするのを忘れた結果、prototype
オブジェクト(=== person
オブジェクト)のlastName
プロパティがそのまま使われてしまった。
問題は2つある。
- 見本となるオブジェクトが、見本として相応しくない具体的な値(
firstName = "右京"
,lastName = "杉下"
)を持っている - 新しいオブジェクトに対してプロパティを設定する処理を抽象化できていない
まずは前者に対応する。見本として相応しい値は状況によって異なる(例えばPerson
がチャットの参加者を表すのであれば「名無し」が良いかもしれない)けど、ここでは空文字列(''
)にしておく。これで少なくともちぐはぐな名前になってしまうことは防げる。
var person = { firstName: '', lastName: '', getFullName: function () { return this.lastName + ' ' + this.firstName; } }; function Person() {} Person.prototype = person; var another2 = new Person(); another2.firstName = '薫'; // lastNameを上書きするのを忘れた! alert(another2.getFullName()); // " 薫" ( ̄- ̄ )
次は後者だが、これはPerson
関数内で行うようにすればよい。関数がnew
演算子によって実行される場合、this
にその関数によって生成される新しいオブジェクト(prototype
オブジェクトの複製とでもいうべきオブジェクト)がセットされる。
var person = { firstName: '', lastName: '', getFullName: function () { return this.lastName + ' ' + this.firstName; } }; function Person(firstName, lastName) { if (firstName != null) { this.firstName = firstName; } if (lastName != null) { this.lastName = lastName; } } Person.prototype = person; var ukyo = new Person('右京', '杉下'); var toru = new Person('亨', '甲斐'); alert(toru.getFullName()); // "甲斐 亨"
これでオブジェクトを作るたびにプロパティを上書きする処理を書かなくて済むようになった。このように、新しいオブジェクトの初期化処理を行う関数はコンストラクタ関数と呼ばれる。
あとはperson
オブジェクトだ。person
オブジェクトを直接触る機会はなさそうなので、Person.prototype
に直接代入する形に書き直してしまう。
function Person(firstName, lastName) { if (firstName != null) { this.firstName = firstName; } if (lastName != null) { this.lastName = lastName; } } Person.prototype = { firstName: '', lastName: '', getFullName: function () { return this.lastName + ' ' + this.firstName; } };
これでよし。
巷のJavaScriptの解説では、コンストラクタとprototype
オブジェクトに関して次のような例を示す。
function Person(firstName, lastName) { if (firstName != null) { this.firstName = firstName; } if (lastName != null) { this.lastName = lastName; } } Person.prototype.getFullName = function () { return this.lastName + ' ' + this.firstName; };
いきなりこのコードを見せられると、コンストラクタ関数がメインで、prototype
オブジェクトがそのおまけのように見えると思う。見本となるオブジェクト(prototype
オブジェクト)が先立つと考えた方が理解しやすいと思う。
また、prototype
は構文の何かに見えるかもしれない。実際は単なるオブジェクトで、そのプロパティへの代入でしかない。すべての関数には初めからprototype
オブジェクトが付いているので、そのオブジェクトを見本となるオブジェクトに仕立て上げてもよい。
prototype
オブジェクトのconstructor
プロパティは、便宜的なものでしかない。constructor
プロパティを使うような処理を書いたり、そのような処理が含まれたスクリプトを読み込む場合はともかく、言語仕様としてprototype
オブジェクトにconstructor
プロパティを定義していないからと言って何か問題が起きるわけではない。
// ... 省略 Person.prototype = { constructor: Person, // new Person().constructor === Person となるようにする // ... 省略
でもまあ、付けておくといいと思う。
は、実際には複製ではない。「プロトタイプチェーン」をキーワードに調べるとよい。prototype
オブジェクトの複製とでもいうべきオブジェクト
(ひどい)
IEで関数名を取得する
IE以外のブラウザであれば、関数.name
で関数名を取得することができる。
var func = function hoge() { } func.name; // => "hoge"
事実上標準でES6にも取り入れられそう。
しかしIEではバージョン11においても関数.name
は定義されていない。そこで、Function.prototype.toString
の結果を使って関数名を取得するコードを書くことにした。
今回は対象がIEだけだけど、複数のブラウザで確かめてみるとFunction.prototype.toString
の実装はまちまちだった(ES5仕様書にも実装依存と書いてある)。後述のテスト用ページを複数のブラウザで見比べると分かりやすい。
- Firefoxでは、仮引数リストの前の「
(
」(開き括弧)より前の部分は正規化される(コメントは削除され、エスケープされたキーワードと識別子は変換される)。 - Chromeでは、
function
キーワードをエスケープするとエラーになる。その他はFirefoxと同じ。 - IEでは、最初の
function
キーワードから最後の「}
」(閉じ波括弧)まで完全にソースコードの通りの文字列を得られる。
今回の目的において、コメントやエスケープを考慮しなければならないIEの実装が一番厄介だった。
- ソースコード
- テスト用ページ(Function.prototype.getNameを定義するバージョン)
- テスト用ページ(Function.prototype.nameを定義するバージョン・IE8以下は無視)
考慮漏れがありそうけど、今回はこれで切り上げることにした。
文字列SetとしてのDOMTokenList
DOMTokenListを文字列専用のSetとして使えそうと思ったけど、空白を含む文字列を追加できないので使いづらかった(DOMTokenListを得られるのはclassListかrelListで、どちらもセパレータが空白)。
var set = document.createElement('div').classList; set.add('foo'); set.add('bar'); set.add('bar'); // 同じトークンは追加されない console.log(set.toString()); // "foo bar" try { // 空白を含む文字列を追加できない set.add('hoge fuga'); } catch (e) { console.error(e); // InvalidCharacterError } set.remove('bar'); set.toggle('baz'); console.log(Array.prototype.map.call(set, function (token) { return token.toUpperCase(); }).join('\n')); /* FOO BAZ */
// DOMTokenListを使ってuniqのようなこと var strList = 'puyo piyo hoge fuga puyo fuga hoge piyo fuga'.split(' '); var set = document.createElement('div').classList; strList.forEach(function (str) { set.add(str); }); // DOMTokenList→Array var uniqStrList = Array.apply(null, set); console.log(uniqStrList.join(', ')); // "puyo, piyo, hoge, fuga"
Setとは違い、Array.prototypeのメソッドを転用できる。
というか、文字列だけならObjectを使えばいいじゃん!
var strList = 'puyo piyo hoge fuga puyo fuga hoge piyo fuga'.split(' '); var uniqStrList = Object.keys(strList.reduce(function (table, str) { table[str] = 1; return table; }, {})); console.log(uniqStrList.join(', ')); // 順序は実装次第
FirefoxにおけるArray#concatは strict mode function 相当
Array.prototype
の大半のメソッドは、配列以外のオブジェクト(NodeListなどの擬似配列など)にも転用できるように汎用的に定義されている。Array.prototype.concat
もその一つだけど、他のメソッドとは異なりthis
値に配列っぽさは求められておらず、どんなオブジェクトでもよい。this
値は引数と同じように処理される。
Array.prototype.concat.call(1, 2, 3, 4, 5); //=> [1, 2, 3, 4, 5] // ? Array.prototype.concat.call("A", "B", "C"); //=> ["A", "B", "C"] // ?
ただし、通常の関数 (non-strict mode function) ではthis
値は必ずオブジェクトになる。数値はNumberオブジェクトに、文字列はStringオブジェクトにラップされてしまう。?
と書いたのはそのこと。Firefox以外のブラウザでは次のような結果になる。
var array = Array.prototype.concat.call(1, 2, 3, 4, 5); typeof array[0]; //=> "object" typeof array[1]; //=> "number"
Firefoxではtypeof array[0];
の戻り値は"number"
になる。this
値はラップされない。strict mode function として定義されているのだろうか。
If
Annotated ES5this
is evaluated within strict mode code, then thethis
value is not coerced to an object.
function nonStrictFn() { return this; } function strictFn() { "use strict"; return this; } typeof nonStrictFn.call(9); //=> "object" typeof strictFn.call(9); //=> "number"
Generic method
FirefoxではString.prototype
やArray.prototype
のメソッドを転用しやすいように Generic method が用意されている。例えば、Array.prototype.concat.call
の代わりにArray.concat
と書ける。
Array.concat(1, 2, 3, 4, 5); //=> [1, 2, 3, 4, 5]
このとき、戻り値の一つ目の要素がオブジェクトにラップされていたら混乱を招くと思う。これがArray.prototype.concat
の実装に影響を与えているんだと思う。
以前書いたFirefox以外の実装にも Generic method を定義するためのスクリプトもFirefoxの実装に合わせることにした。もちろん、Array.prototype.concat
の方は弄らない。
転用の必要性
そもそも、Array.prototype.concat
は転用する必要がないように思える。[].concat(item1, item2, ...)
で事足りる。
var array; array = [].concat(1, 2, 3, 4, 5); //=> [1, 2, 3, 4, 5] typeof array[0]; //=> "number"
Array.prototype
自体も配列 (Array.isArray(Array.prototype) === true
) なので、Array.prototype.length
が弄られていないという前提の下、次のようにも書ける。
// Function#callを使わない Array.prototype.concat(1, 2, 3, 4, 5); //=> [1, 2, 3, 4, 5]
CSSセレクタ4の字句解析器を書いた
CSS4 セレクタエンジンの事始めとして。字句解析の範囲では、CSS3 とそれほど変わらない。違いは Reference combinator で使う「/」と、セレクタの subject を指定する「!」くらい。
ユニットテストは後で書きます……。
以下はメモ。
トークン種別は少なめに
- Identifier
- String
- Invalid
- Number
- WhiteSpace
- Punctuator
の6種類。CSS セレクタには Descendant combinator があるので Whitespace を捨てられない。
charCodeAt オンリー
セレクタはたいてい短いので正規表現を使ってもコストは気にならないだろうけど、いずれは CSS パーサを作るつもりなので、正規表現を使わずに書くことにした。
1文字1文字を比較していくわけだけど、その1文字を文字列として扱ったり数値として扱ったりするのは煩わしいので、全て数値として扱うことにした。つまり、charCodeAt のみを使い、charAt は使わない。文字コードは配列に格納していき、最後に String.fromCharCode(null, 文字コード配列)
で文字列に戻す。パフォーマンスも問題なさそう。
間接的な戻り値
文字列を読み進める関数は、次のように成功か失敗かを表す bool 値を戻り値で返し、実際に読み込んだ文字コードは第一引数の配列に格納するようにした。
function readNonascii(codes) { var code = text.charCodeAt(offset); if (code > 127) { codes.push(code); offset++; return true; } return false; }
Java や C# ではこの手のメソッドがよくある気がする。一見ダサい。普通は次のように、単に読み込んだ文字コードを返すだけだと思う。
// 他の関数で複数の文字コードを読む場合もあるので、戻り値を配列 or null で統一 function readNonascii_another() { var code = text.charCodeAt(offset); if (code > 127) { offset++; return [code]; } return null; }
これなら読み取りに成功したかどうかは戻り値が truthy であることを確認すれば分かる。でも、使うときは前者の関数の方がコードがすっきりする。
var codes = []; // 前者の場合 if (readNonascii(codes)) { // ... } // 後者の場合 var result; // 前者とは違い変数が必要 if ((result = readNonascii_another())) { codes = codes.concat(result); // 前者と比べて1行増える // ... }
ということで、前者のタイプの関数も場合によっては悪くないと思う。
NaN を扱う
Java では charAt(index)
の index が範囲外だと例外が投げられる(CharSequence (Java Platform SE 7 ))けど、JavaScript では charAt なら空文字列、charCodeAt なら NaN が返されるだけ。
なので、現在のインデックス値が文字列の長さ未満かどうかの確認(if (offset < limit)
)を少し削れる。例えば次のように。
// before if (offset < limit) { code = text.charCodeAt(offset); if (code === 10/*\n*/) { offset++; // ... codes.push(code); return true; // ... offset--; } } // after code = text.charCodeAt(offset); // offset < limit で、戻り値が NaN かもしれない if (code === 10/*\n*/) { // NaN なら false になるだけなので問題ない offset++; // ... codes.push(code); return true; // ... offset--; }
ただし、!==
を使う場合や、ある数値間の範囲外であることを確かめる場合はうまくいかない。初めに code &&
を付けなくてはいけない(NaN は falsy)。
うーん、やっぱりこれはよくない感じがする。