経緯
JavaScript での開発中、あるオブジェクトの生成で、新しく専用のクラスを作ってインスタンスとするか、既存のクラスからインスタンスを作ってそのプロパティに色々突っ込むか、で悩み、色々調べた所、Io という面白そうな言語を見つけ、ヒントになったと同時にプロトタイプベースに興味も持ったので書きました。
Io っぽく JavaScript を書く
Io とは
io language
公式サイト。
プログラミング言語「Io」の参考情報 - JavaScript勉強会
参考情報をまとめてくれている。
今現在、自分の中で最高の言語です。めっちゃ好き。
実装
まず核となるオブジェクトを作ります。名前は Object
が良かったけど被るのでしょうがないです。変数宣言は省略してます。
Stuff = Object.create(null, {
super: {
enumerable: true,
value: null
}
});
今この Stuff
には super
以外に何もデータがありません。試しに
Stuff + ""
を実行してみると、
TypeError: No default value
とのエラーを吐きます。文字列変換時に呼ばれる valueOf
メソッドが Stuff
に見つからないので当たり前です。では、いろいろメソッドを追加してみます。
// 自分を継承しているオブジェクトを生成するメソッド
Stuff.clone = function() {
return Object.create(this, {
super: {
enumerable: true,
value: this
}
});
};
// プロパティ・メソッドを追加するメソッド
Stuff.assign = function(...args) {
return Object.assign(this, ...args);
};
// 継承関係を判定するメソッド
Stuff.kindof = function(obj) {
return this === obj || (!!this.super && this.super.kindof(obj));
};
これでよくある継承やらをやってみます。
A = Stuff.clone();
A.assign({
hoge() {
console.log(0);
}
});
/* new 的なやつ */
a = A.clone();
a.hoge(); // 0
/* extends 的なもの */
B = A.clone();
B.assign({
/* メソッド追加 */
fuga() {
return "fuga";
},
/* オーバーライド的なやつ */
hoge() {
/* super.hode の呼び出し */
B.super.hoge.apply(this, arguments);
console.log(1);
}
});
b = B.clone();
b.hoge(); // 0 1
b.kindof(A); // true
a.kindof(B); // false
Object.setPrototypeOf
などを使った場合でも結局は似たような形になると思います。
この例だと、b
を作るのにわざわざ B
という「設計図」を作る必要はありません。new
的なやつと extends
的なやつは実質同じことをしており、クラスにするべきかインスタンスにするべきかを考える必要はなくなります。
また、非標準である(ES2015で標準化された?)__proto__
を使えば以下のようにも書けます。
Stuff = {
__proto__: null
};
A = {
__proto__: Stuff,
hoge() {
console.log(0);
}
};
B = {
__proto__: A,
fuga() {
return "fuga";
},
hoge() {
B.__proto__.hoge.apply(this, arguments);
console.log(1);
}
};
つまり
上の __proto__
を使った例で、オブジェクトの生成・継承に関数を一切使っていないのがわかるでしょうか? つまり、JavaScript では特別なことをせずとも継承が容易に出来るのです。
ここまですっきり書けるのは、JavaScript がそもそもプロトタイプベースだからに他ならず、当たり前といえば当たり前です。Io を真似ることで、JavaScript がプロトタイプベースであることを意識しやすくなったと言えます。
じゃあ JavaScript の「new」とは何なのか
「JavaScript はプロトタイプベースのオブジェクト指向言語である」とよく見ますが、new
とか prototype
とか __proto__
とか constructor
とか、ついには ES2015 の class
構文とか色々な要素がごちゃごちゃしすぎている気がします。
var o = new Foo();
これを呼び出したとき、 JavaScript は、
var o = new Object();
o.[[Prototype]] = Foo.prototype;
Foo.call(o);
実際にはこれ(あるいはこのような何か)を行っており、
o.someProp;
後にこうすると、 o
が someProp
プロパティを持っているかどうかを調べ、もし持っていなければ Object.getPrototypeOf(o).someProp
を、そこにも存在しなければ Object.getPrototypeOf(Object.getPrototypeOf(o)).someProp
を…と調べていきます。
継承とプロトタイプチェーン - JavaScript | MDN
そもそも prototype
と __proto__
が混在しているのが混乱の原因かと思われます。このような状況から、JavaScript をプロトタイプベースと呼んでいいのか議論があるようです。タイトルの「純粋な」もその意味です。
JavaScriptはプロトタイプベースのオブジェクト指向プログラミング言語ではない!? - JavaScript勉強会
「純粋な」プロトタイプベースで書くことについて
メリット
- シンプル。考えることが少ない。
- JavaScript はもともとプロトタイプベースなのでそれを引き出しているとも言える。
デメリット
- ビルトインオブジェクトが JavaScript 式のプロトタイプベースなので整合性が取れない。
- IDE の型判定などが効かない。
未検証
- 速度。プロトタイプいじるのは速度的にどうなんだろう。
- 保守性。結局はうまく設計しないとごちゃごちゃになる。
その他
- ビルトインオブジェクトとうまくやる方法も考えたい。
-
Proxy
とか使ったらもっと面白くなりそう。 - プロトタイプベースでかける altJS とかあったら面白そう。
結論
- JS は闇
- Io はいいぞ