一.JS は Java に変わるのか!?
ES6 は class、constructor、static、extends、super などのキーワードを有効にしてクラス定義をサポートしました。まるで Java に変わろうとしているようで、ついに prototype を気にしなくてよくなるのでしょうか?
そうではありません。この一連のキーワードを有効にするのは単に「構文ノイズ」を減らし、クラス構造を定義する際の作業量を減らすためです。JS のプロトタイプベース(prototype-based)のオブジェクトシステムをクラスベース(class-based)に簡単に変更することはできません。これは JS 言語設計者の選択であり、不可能です。既存のオブジェクトシステムを根こそぎ取り除いて、Java の那一套を縫い合わせることはできません。
P.S.「構文ノイズ」は老趙から借りてきた言葉です。Python と Java で同じ機能を実装すると、diff 結果が「構文ノイズ」になります。例えば JS 中の function、prototype など、入力するのがつらいものです
また、プロトタイプベースのオブジェクトシステムは比較的柔軟です。例えば JS はランタイムで動的にクラス継承ツリーを変更できます。そのため、空のクラス継承ツリーを事前に定義しておき、必要になった時にクラスメンバーを追加できます。例えば:
// 空の継承ツリー
var SuperType = function() {};
var SubType = function() {};
SubType.prototype = new SuperType();
// 動的強化
void function() {
//...
SuperType.prototype.cl = console.log.bind(console);
new SubType().cl('invoked by subType'); // invoked by subType
}();
この感覚はとても奇妙です。Java と JS が一緒に絵を描きます。Java は精細な彫刻刀でキャンバス上に一丝不苟に清明上河図を完成させ、装裱して壁に掛け、四方を驚かせます。JS は水性ペンで 2 本の曲がりくねった横線を描いて、「描き終わった。これが清明上河図だ」と言います。観客は愚弄されたように感じますが、仕方なく JS の作品も装裱して Java の大作の隣に掛けます。Java は束手而立して分曉を待ちます。这时 JS が忙しく動き始め、ペンを持って真剣に額縁のガラス面上一模一样的作品を完成させます。観客は不可思議に感じます。突然人群中から一つの声が伝わります。「左から 3 番目の家は尖り屋根であるべきだ。你们都画错了」。Java の顔は保ちきれなくなり、急いで別の宣紙を広げて、あの目障りな錯誤を直して重新描こうとします。JS は左上角を指して「ここか?我已经改好了」と言います
JS は命令型言語、動的言語、関数型言語の結合体です。体系全体而言、オブジェクトシステムはプロトタイプ実装によるこの柔軟性が必要です。Java の精彫細刻したクラスベースのオブジェクトシステムを羨む必要はありません。完全に合いません。ES 規範設計者は Java をそのまま真似ることはありませんし、理由もありません。至此、JavaScript と Java は依然として毫も関係ありません。20 年前と同じように
P.S. JS と筆者はどちらも 1995 年に誕生しました。Netscape と JavaScript と Java Applet の丝丝缕缕については、自行で查找してください
二.class がもたらす新特性
ES6 以前の「class」はこのようなものかもしれません:
function Circle(radius) {
if (typeof radius !== 'number') {
throw new TypeError('radius: a number expected');
}
// インスタンス属性
this.radius = radius;
// 静的属性
Circle.count++;
}
Circle.count = 0;
// プロトタイプ属性
Circle.prototype.getArea = function() {
return Math.PI * this.radius * this.radius;
};
new Circle で得られる各インスタンスは 2 つの属性を持ちます。インスタンス属性 radius とプロトタイプ属性 getArea
時々 ES5 特性で実装されたより厳密な「class」を見かけることがあります:
// より厳密な、getter/setter を定義
function CircleEx(radius) {
this.radius = radius;
CircleEx.count++;
}
// 静的属性を定義
Object.defineProperty(CircleEx, 'count', {
get: function() {
// this は CircleEx を指す
// console.log(this);
// CircleEx.count++ は先に get で 0 を返す
// 次に set で CircleEx に_count 属性を追加して 1 を代入
return !this._count ? 0 : this._count;
},
set: function(val) {
this._count = val;
}
});
CircleEx.prototype.getArea = function() {
return Math.PI * this.radius * this.radius;
};
// プロトタイプ属性を定義
// 実際にはインスタンス属性に getter/setter を定義
Object.defineProperty(CircleEx.prototype, 'radius', {
get: function() {
//!!! this は CircleEx インスタンスを指す
// そのプロトタイプオブジェクトではない
// console.log(this);
// 構築関数中の this.radius = radius;
// radius 属性を検索し、get をトリガーし、this._radius を返す
// radius に代入
return this._radius;
},
set: function(val) {
if (typeof val !== 'number') {
throw new TypeError('radius: a number expected');
}
this._radius = val;
}
});
new CircleEx で得られる各インスタンスは 2 つの属性を持ちます。インスタンス属性 _radius とプロトタイプ属性 getArea。radius については、プロトタイプオブジェクト上に定義されたアクセサ(getter/setter)であり、列挙不可です
以前に「時々」このような「class」定義を見かけると言いましたが、それはあまりにも面倒で、大家都懒得使うからです。ES6 class 部分の目標は現状を変え、より便利なクラスメンバー定義方式を提供することです
オブジェクトの定義方式を簡素化
オブジェクトリテラル中で直接 getter/setter を定義できます。関数タイプ属性の定義方式を簡素化します。一般関数、ジェネレーター、動的関数名を含みます。例えば:
// getter/setter
var obj = {
// getter
//!!! getter は引数なし、パラメータを渡せない
get attr() {
console.log('getter');
return this._attr || 0;
},
// setter
//!!! setter は少なくとも 1 つのパラメータを受け取る
set attr(val) {
console.log('setter');
this._attr = val;
},
// 事前計算属性([] 構文で追加された関数属性、動的関数名)
[(function() {return 'print';})()](arg) {
console.log(arg);
},
// 一般メソッド
fn(arg) {
console.log(`arg = ${arg}`);
},
// ジェネレーター
*gen(i) {
while(true) {
yield i++;
}
}
}
冗長な function キーワードがタイプ定義から完全に消えました。[アロー関数](/articles/アロー関数-es6 ノート 6/) よりも簡潔です。JS を初めて学んだ時の欣喜を再び感じます——変数に一対の丸括弧を添えるだけで関数呼び出しになります。なんと簡単粗暴でしょうか?
ES6 強化版オブジェクトリテラルで以前の CircleEx クラスを書き直すと、このようになります:
// CircleEx を書き直し
CircleEx.prototype = {
getArea() {
return Math.PI * this.radius * this.radius;
},
get radius() {
return this._radius;
},
set radius(val) {
if (typeof val !== 'number') {
throw new TypeError('radius: a number expected');
}
this._radius = val;
}
};
注意、以前と異なるのは、此时アクセサ radius は列挙可能で、プロトタイプ属性として出現します。しかし同様に、アクセサ自身は暴露されません。radius に対するすべての操作はアクセサの戻り値に対する操作であり、アクセサ自身ではありません。したがって:
typeof new CircleEx(1).radius === 'number' // true
class 定義を有効化
P.S.「導入」ではなく「有効化」と言うのは、ES6 class 関連のすべてのキーワードは元々予約語であり、ただ今は明確な意味を与えられただけだからです
class 定義中で、constructor は構築関数を表します。static キーワードは一般関数と特殊関数を区別するために使用されます(意味は Java、C++ と同じ)
constructor はオプションで、デフォルトで空の構築関数(constructor() {})を提供します。constructor は必ずリテラル形式でなければなりません。動的関数名にはできません。否则 constructor という名前の一般メソッドが得られ、構築関数にはなりません
class 構文で以前の Circle クラスを書き直すと、以下のようになります:
// CircleEx を書き直し
class MyCircle {
// 構築関数
constructor(radius) {
this.radius = radius;
MyCircle.count++;
}
// インスタンス属性の getter/setter
get radius() {
return this._radius;
}
set radius(val) {
if (typeof val !== 'number') {
throw new TypeError('radius: a number expected');
}
this._radius = val;
}
// プロトタイプ属性
getArea() {
return Math.PI * this.radius * this.radius;
}
// 静的属性
static get count() {
return this._count || 0;
}
static set count(val) {
this._count = val;
}
}
実際の効果がどうであれ、少なくとも見た目ははるかに明確になりました
タイプ保護
タイプ保護が内蔵されています。class 構文で定義されたタイプは、new 演算子を通じて呼び出す必要があります。例えば:
// 一般関数として呼び出すと、エラーになる
MyCircle();
// Uncaught TypeError: Class constructors cannot be invoked without 'new'
この内蔵保護はいくつかの問題を回避できますが、確かに柔軟性を制限します。例えばあるクラスライブラリが提供する API には一般関数として呼び出せるし、構築関数としても使用できる関数が含まれています。class 構文では実現できません
さらに、クラス定義中で引用されたクラス名は外部の力によって変更されません。例えば:
class T {
static get val() {
return 'val';
}
get() {
return T.val;
}
}
// test
var t = new T();
console.log(t.get()); // val
// 外力破壊
T = null;
console.log(t.get()); // val
//! エラー Uncaught TypeError: T is not a constructor
// new T();
ES6 以前にはこのような保護はありませんでした。function で書き直すと、外力破壊に遭った後必ずエラーになります
クラス式(匿名クラス)をサポート
// 匿名クラス
var circle = new class {
constructor(radius) {
this.radius = radius;
}
}(3);
console.log(circle.radius); // 3
匿名クラスはあまり役に立たないようです。一般来说、クラスはオブジェクトテンプレートであり、クラスを通じてオブジェクトの量産を実現できます。Java は匿名���ラスを使って量産する必要のない臨時オブジェクトを作成します。しかし JS には N 種類の方法でオブジェクトを作成できます。匿名クラスを使うのは最も愚かな方法かもしれません
クラス中で定義されたメソッドは設定可能で列挙不可
例えば MyClass.prototype(class 構文で定義された)のすべての属性は列挙不可です。クラス中で定義されたメソッドも列挙不可です
このようにするのは意図的にオブジェクトシステムがプロトタイプに基づく事実を隠蔽しているようです。例えば for...in では以前の MyCircle.prototype を列挙できません。しかし Object.getOwnPropertyNames() を通じていくつかの痕跡を発見できます。例えば:
console.log(Object.getOwnPropertyNames(MyCircle.prototype));
// log print:
// ["constructor", "radius", "getArea"]
これらの属性はすべてプロトタイプオブジェクト上に実在しますが、デフォルトで列挙不可がこの事実を隠蔽しています。おそらく class 構文を使用すると同時に、手動で prototype オブジェクトを操作して、既存のルールを破壊してほしくないのでしょう
おそらくこの prototype を隠蔽する做法が適切でない总觉得しますが、どこが間違っているか言えません。では、プロトタイプ継承の古典的な問題を考えてみましょう:プロトタイプ参照属性はインスタンス間で共有されます。例えば:
function Type() {}
Type.prototype.issue = [1, 2];
// test
var t1 = new Type();
var t2 = new Type();
t1.issue.push(3);
console.log(t2.issue); // [1, 2, 3]
class 構文にはこの問題がありますか?まず class 構文で Type のプロトタイプ上に配列タイプ属性を定義する必要があります。残念ながら根本的にできないことがわかります。直接 prototype にアクセスしない限り。したがって心配する必要はありません。このような「隠蔽」が隠れた問題を引き起こすことはありません。ES6 設計者は私たちよりもはるかに多くを考えています
三.まとめ
爽やかなタイプ定義構文、keep it simple
ES は徐々に構文ノイズを除去しています:
アロー関数 + `class` は `function` を除去
デフォルトパラメータ + 不定パラメータは `arguments` を除去
`class` + `extends` + `super` は `prototype` を除去
関数型言語の角度から見ると、これらの変化は極めて良いものです。数学者が追求しているのはまさに極致簡潔の美です
参考資料
-
《ES6 in Depth》:InfoQ 中文站が提供する無料電子書籍
-
《JavaScript 言語精髓とプログラミング実践》
コメントはまだありません