new
のためにあなたを縛り付けるクラスとサブクラス
class
Point
{
constructor
(
x
,
y
)
{
this
.
x
=
x
;
this
.
y
=
y
;
}
toString
()
{
return
`(
${
this
.
x
}
,
${
this
.
y
}
)`
;
}
}
class
ColorPoint
extends
Point
{
constructor
(
x
,
y
,
color
)
{
super
(
x
,
y
);
this
.
color
=
color
;
}
toString
()
{
return
super
.
toString
()
+
' in '
+
this
.
color
;
}
}
クラスの使用
> const cp = new ColorPoint(25, 8, 'green');
> cp.toString();
'(25, 8) in green'
> cp instanceof ColorPoint
true
> cp instanceof Point
true
内部的には、ES6のクラスは根本的に新しいものではありません。主に、昔ながらのコンストラクタ関数を作成するためのより便利な構文を提供します。typeof
を使用すると、それがわかります。
> typeof Point
'function'
クラスはECMAScript 6でこのように定義されます。
class
Point
{
constructor
(
x
,
y
)
{
this
.
x
=
x
;
this
.
y
=
y
;
}
toString
()
{
return
`(
${
this
.
x
}
,
${
this
.
y
}
)`
;
}
}
このクラスは、ES5のコンストラクタ関数と同じように使用します。
> var p = new Point(25, 8);
> p.toString()
'(25, 8)'
実際、クラス定義の結果は関数です。
> typeof Point
'function'
ただし、クラスは関数呼び出しではなく、new
を介してのみ呼び出すことができます(この背後にある根拠は後で説明します)。
> Point()
TypeError: Classes can’t be function-called
クラス定義のメンバー間に区切り文字はありません。たとえば、オブジェクトリテラルのメンバーはカンマで区切られますが、これはクラス定義のトップレベルでは違法です。セミコロンは許可されていますが、無視されます。
class
MyClass
{
foo
()
{}
;
// OK, ignored
,
// SyntaxError
bar
()
{}
}
セミコロンは、セミコロンで終了するメンバーを含む可能性のある将来の構文に備えて許可されています。カンマは、クラス定義がオブジェクトリテラルとは異なることを強調するために禁止されています。
関数宣言は巻き上げられます:スコープに入ると、その中で宣言された関数は、宣言がどこで行われたかに関係なく、すぐに利用可能になります。つまり、後で宣言された関数を呼び出すことができます。
foo
();
// works, because `foo` is hoisted
function
foo
()
{}
対照的に、クラス宣言は巻き上げられません。したがって、クラスは、実行がその定義に到達して評価された後にのみ存在します。事前にアクセスすると、ReferenceError
が発生します。
new
Foo
();
// ReferenceError
class
Foo
{}
この制限の理由は、クラスが、任意の値を持つextends
句を持つことができるためです。その式は、適切な「場所」で評価する必要があり、その評価を巻き上げることはできません。
巻き上げがないことは、あなたが思うほど制限的ではありません。たとえば、クラス宣言の前にある関数は、そのクラスを参照できますが、関数を呼び出す前にクラス宣言が評価されるまで待つ必要があります。
function
functionThatUsesBar
()
{
new
Bar
();
}
functionThatUsesBar
();
// ReferenceError
class
Bar
{}
functionThatUsesBar
();
// OK
関数と同様に、クラス定義には2つの種類、つまりクラスを定義する2つの方法があります。クラス宣言とクラス式です。
関数式と同様に、クラス式は匿名にできます。
const
MyClass
=
class
{
···
};
const
inst
=
new
MyClass
();
また、関数式と同様に、クラス式には内部でのみ表示される名前を付けることができます。
const
MyClass
=
class
Me
{
getClassName
()
{
return
Me
.
name
;
}
};
const
inst
=
new
MyClass
();
console
.
log
(
inst
.
getClassName
());
// Me
console
.
log
(
Me
.
name
);
// ReferenceError: Me is not defined
最後の2行は、Me
がクラス外の変数にならないが、内部で使用できることを示しています。
クラス本体にはメソッドのみを含めることができますが、データプロパティを含めることはできません。データプロパティを持つプロトタイプは、一般的にアンチパターンと見なされているため、これはベストプラクティスを強制するだけです。
constructor
、静的メソッド、プロトタイプメソッド クラス定義でよく見られる3種類のメソッドを見てみましょう。
class
Foo
{
constructor
(
prop
)
{
this
.
prop
=
prop
;
}
static
staticMethod
()
{
return
'classy'
;
}
prototypeMethod
()
{
return
'prototypical'
;
}
}
const
foo
=
new
Foo
(
123
);
このクラス宣言のオブジェクト図は次のようになります。理解するためのヒント:[[Prototype]]
はオブジェクト間の継承関係であり、prototype
は値がオブジェクトである通常のプロパティです。プロパティprototype
は、作成するインスタンスのプロトタイプとしてその値を使用するnew
演算子に関してのみ特別です。
まず、擬似メソッドconstructor
。このメソッドは特別であり、クラスを表す関数を定義します。
> Foo === Foo.prototype.constructor
true
> typeof Foo
'function'
これはクラスコンストラクタ
と呼ばれることもあります。通常のコンストラクタ関数にはない機能があります(主に、super()
を介してスーパークラスのコンストラクタを呼び出す機能、これについては後で説明します)。
次に、静的メソッド。静的プロパティ(またはクラスプロパティ)は、Foo
自体のプロパティです。メソッド定義の前にstatic
を付けると、クラスメソッドが作成されます。
> typeof Foo.staticMethod
'function'
> Foo.staticMethod()
'classy'
3つ目は、プロトタイプメソッド。Foo
のプロトタイププロパティは、Foo.prototype
のプロパティです。それらは通常メソッドであり、Foo
のインスタンスによって継承されます。
> typeof Foo.prototype.prototypeMethod
'function'
> foo.prototypeMethod()
'prototypical'
ES6クラスを時間内に完成させるために、それらは意図的に「最大限に最小限」になるように設計されました。そのため、現在では静的メソッド、ゲッター、セッターのみを作成できますが、静的データプロパティを作成することはできません。言語に追加するための提案があります。その提案が受け入れられるまで、使用できる2つの回避策があります。
まず、静的プロパティを手動で追加できます。
class
Point
{
constructor
(
x
,
y
)
{
this
.
x
=
x
;
this
.
y
=
y
;
}
}
Point
.
ZERO
=
new
Point
(
0
,
0
);
Object.defineProperty()
を使用して読み取り専用プロパティを作成できますが、割り当てのシンプルさが気に入っています。
次に、静的ゲッターを作成できます。
class
Point
{
constructor
(
x
,
y
)
{
this
.
x
=
x
;
this
.
y
=
y
;
}
static
get
ZERO
()
{
return
new
Point
(
0
,
0
);
}
}
どちらの場合も、読み取ることができるプロパティPoint.ZERO
を取得します。最初の場合は、毎回同じインスタンスが返されます。2番目の場合は、毎回新しいインスタンスが返されます。
ゲッターとセッターの構文は、ECMAScript 5のオブジェクトリテラルとまったく同じです。
class
MyClass
{
get
prop
()
{
return
'getter'
;
}
set
prop
(
value
)
{
console
.
log
(
'setter: '
+
value
);
}
}
MyClass
は次のように使用します。
> const inst = new MyClass();
> inst.prop = 123;
setter: 123
> inst.prop
'getter'
式を角括弧に入れると、式を介してメソッドの名前を定義できます。たとえば、次のFoo
の定義方法はすべて同等です。
class
Foo
{
myMethod
()
{}
}
class
Foo
{
[
'my'
+
'Method'
]()
{}
}
const
m
=
'myMethod'
;
class
Foo
{
[
m
]()
{}
}
ECMAScript 6のいくつかの特殊なメソッドには、シンボルであるキーがあります。計算されたメソッド名を使用すると、そのようなメソッドを定義できます。たとえば、オブジェクトにキーがSymbol.iterator
であるメソッドがある場合、それは反復可能です。つまり、その内容はfor-of
ループや他の言語メカニズムで反復処理できます。
class
IterableClass
{
[
Symbol
.
iterator
]()
{
···
}
}
メソッド定義の前にアスタリスク(*
)を付けると、それはジェネレーターメソッドになります。とりわけ、ジェネレーターは、キーがSymbol.iterator
であるメソッドを定義するのに役立ちます。次のコードは、それがどのように機能するかを示しています。
class
IterableArguments
{
constructor
(...
args
)
{
this
.
args
=
args
;
}
*
[
Symbol
.
iterator
]()
{
for
(
const
arg
of
this
.
args
)
{
yield
arg
;
}
}
}
for
(
const
x
of
new
IterableArguments
(
'hello'
,
'world'
))
{
console
.
log
(
x
);
}
// Output:
// hello
// world
extends
句を使用すると、既存のコンストラクタ(クラスを介して定義されたかどうかに関係なく)のサブクラスを作成できます。
class
Point
{
constructor
(
x
,
y
)
{
this
.
x
=
x
;
this
.
y
=
y
;
}
toString
()
{
return
`(
${
this
.
x
}
,
${
this
.
y
}
)`
;
}
}
class
ColorPoint
extends
Point
{
constructor
(
x
,
y
,
color
)
{
super
(
x
,
y
);
// (A)
this
.
color
=
color
;
}
toString
()
{
return
super
.
toString
()
+
' in '
+
this
.
color
;
// (B)
}
}
繰り返しますが、このクラスは期待どおりに使用されます。
> const cp = new ColorPoint(25, 8, 'green');
> cp.toString()
'(25, 8) in green'
> cp instanceof ColorPoint
true
> cp instanceof Point
true
クラスには2種類あります。
Point
は、extends
句がないため、基底クラスです。ColorPoint
は派生クラスです。super
を使用する方法は2つあります。
constructor
)は、スーパークラスの呼び出しを行うために、関数呼び出し(super(···)
)のように使用します(A行)。static
の有無にかかわらず)は、スーパープロパティを参照するために、プロパティ参照(super.prop
)またはメソッド呼び出し(super.method(···)
)のように使用します(B行)。サブクラスのプロトタイプは、ECMAScript 6ではスーパークラスです。
> Object.getPrototypeOf(ColorPoint) === Point
true
つまり、静的プロパティが継承されます。
class
Foo
{
static
classMethod
()
{
return
'hello'
;
}
}
class
Bar
extends
Foo
{
}
Bar
.
classMethod
();
// 'hello'
静的メソッドをスーパー呼び出しすることもできます。
class
Foo
{
static
classMethod
()
{
return
'hello'
;
}
}
class
Bar
extends
Foo
{
static
classMethod
()
{
return
super
.
classMethod
()
+
', too'
;
}
}
Bar
.
classMethod
();
// 'hello, too'
派生クラスでは、this
を使用する前にsuper()
を呼び出す必要があります。
class
Foo
{}
class
Bar
extends
Foo
{
constructor
(
num
)
{
const
tmp
=
num
*
2
;
// OK
this
.
num
=
num
;
// ReferenceError
super
();
this
.
num
=
num
;
// OK
}
}
暗黙的にsuper()
を呼び出さずに派生コンストラクタを離れると、エラーも発生します。
class
Foo
{}
class
Bar
extends
Foo
{
constructor
()
{
}
}
const
bar
=
new
Bar
();
// ReferenceError
ES5と同様に、オブジェクトを明示的に返すことで、コンストラクタの結果を上書きできます。
class
Foo
{
constructor
()
{
return
Object
.
create
(
null
);
}
}
console
.
log
(
new
Foo
()
instanceof
Foo
);
// false
そうする場合は、this
が初期化されているかどうかは関係ありません。つまり、この方法で結果を上書きする場合は、派生コンストラクタでsuper()
を呼び出す必要はありません。
基底クラスにconstructor
を指定しない場合は、次の定義が使用されます。
constructor
()
{}
派生クラスの場合、次のデフォルトコンストラクタが使用されます。
constructor
(...
args
)
{
super
(...
args
);
}
ECMAScript 6では、最終的にすべての組み込みコンストラクタをサブクラス化できます(ES5には回避策がありますが、これらには重大な制限があります)。
たとえば、(ほとんどのエンジンでスタックトレースを持つという機能を継承する)独自の例外クラスを作成できるようになりました。
class
MyError
extends
Error
{
}
throw
new
MyError
(
'Something happened!'
);
また、インスタンスがlength
を適切に処理するArray
のサブクラスを作成することもできます。
class
Stack
extends
Array
{
get
top
()
{
return
this
[
this
.
length
-
1
];
}
}
var
stack
=
new
Stack
();
stack
.
push
(
'world'
);
stack
.
push
(
'hello'
);
console
.
log
(
stack
.
top
);
// hello
console
.
log
(
stack
.
length
);
// 2
Array
のサブクラス化は通常、最適なソリューションではないことに注意してください。多くの場合、独自のクラス(インターフェースを制御する)を作成し、プライベートプロパティでArrayに委任する方が優れています。
このセクションでは、ES6クラスのプライベートデータを管理するための4つのアプローチについて説明します。
constructor
の環境にプライベートデータを保持するアプローチ#1と#2は、コンストラクター向けにES5ですでに一般的でした。アプローチ#3と#4はES6で新しく導入されました。各アプローチを使用して、同じ例を4回実装してみましょう。
実行例は、カウンター(初期値はcounter
)がゼロに達するとコールバックaction
を1回呼び出すクラスCountdown
です。2つのパラメーターaction
とcounter
は、プライベートデータとして保存する必要があります。
最初の実装では、クラスコンストラクターの環境にaction
とcounter
を保存します。環境とは、JavaScriptエンジンが、新しいスコープが入力されるたび(関数呼び出しやコンストラクター呼び出しなどを介して)に生成されるパラメーターとローカル変数を格納する内部データ構造のことです。以下がコードです。
class
Countdown
{
constructor
(
counter
,
action
)
{
Object
.
assign
(
this
,
{
dec
()
{
if
(
counter
<
1
)
return
;
counter
--
;
if
(
counter
===
0
)
{
action
();
}
}
});
}
}
Countdown
の使用は次のようになります。
> const c = new Countdown(2, () => console.log('DONE'));
> c.dec();
> c.dec();
DONE
利点
欠点
この手法の詳細については、「Speaking JavaScript」の「コンストラクターの環境におけるプライベートデータ(クロックフォードプライバシーパターン)」を参照してください。
次のコードは、プレフィックスにアンダースコアを付けて名前がマークされたプロパティにプライベートデータを保持します。
class
Countdown
{
constructor
(
counter
,
action
)
{
this
.
_counter
=
counter
;
this
.
_action
=
action
;
}
dec
()
{
if
(
this
.
_counter
<
1
)
return
;
this
.
_counter
--
;
if
(
this
.
_counter
===
0
)
{
this
.
_action
();
}
}
}
利点
欠点
WeakMapsを使用する優れた手法があり、最初のアプローチ(安全性)の利点と2番目のアプローチ(プロトタイプメソッドを使用できること)の利点を組み合わせています。この手法を次のコードで示します。プライベートデータを格納するために、WeakMaps _counter
と_action
を使用します。
const
_counter
=
new
WeakMap
();
const
_action
=
new
WeakMap
();
class
Countdown
{
constructor
(
counter
,
action
)
{
_counter
.
set
(
this
,
counter
);
_action
.
set
(
this
,
action
);
}
dec
()
{
let
counter
=
_counter
.
get
(
this
);
if
(
counter
<
1
)
return
;
counter
--
;
_counter
.
set
(
this
,
counter
);
if
(
counter
===
0
)
{
_action
.
get
(
this
)();
}
}
}
2つのWeakMaps _counter
と_action
のそれぞれは、オブジェクトをそれぞれのプライベートデータにマッピングします。WeakMapsの仕組みにより、オブジェクトがガベージコレクションされるのを妨げることはありません。外部にWeakMapsを隠しておけば、プライベートデータは安全です。
さらに安全にするには、WeakMap.prototype.get
とWeakMap.prototype.set
を変数に保存し、(メソッドではなく)それらを動的に呼び出すことができます。
const
set
=
WeakMap
.
prototype
.
set
;
···
set
.
call
(
_counter
,
this
,
counter
);
// _counter.set(this, counter);
そうすれば、悪意のあるコードがこれらのメソッドをプライベートデータを盗聴するようなメソッドに置き換えても、コードに影響はありません。ただし、保護されるのはコードの後に実行されるコードに対してのみです。自分のコードより前に実行される場合は、何もできません。
利点
欠点
プライベートデータの別の保存場所は、キーがシンボルであるプロパティです。
const
_counter
=
Symbol
(
'counter'
);
const
_action
=
Symbol
(
'action'
);
class
Countdown
{
constructor
(
counter
,
action
)
{
this
[
_counter
]
=
counter
;
this
[
_action
]
=
action
;
}
dec
()
{
if
(
this
[
_counter
]
<
1
)
return
;
this
[
_counter
]
--
;
if
(
this
[
_counter
]
===
0
)
{
this
[
_action
]();
}
}
}
各シンボルは一意であるため、シンボル値のプロパティキーが他のプロパティキーと衝突することはありません。さらに、シンボルは外部からはいくらか隠されていますが、完全ではありません。
const
c
=
new
Countdown
(
2
,
()
=>
console
.
log
(
'DONE'
));
console
.
log
(
Object
.
keys
(
c
));
// []
console
.
log
(
Reflect
.
ownKeys
(
c
));
// [ Symbol(counter), Symbol(action) ]
利点
欠点
Reflect.ownKeys()
を使用して、オブジェクトのすべてのプロパティキー(シンボルを含む!)をリストできます。JavaScriptでのサブクラス化は、2つの理由で使用されます。
instanceof
でテスト)は、スーパークラスのインスタンスでもあります。サブクラスのインスタンスはスーパークラスのインスタンスのように動作しますが、より多くのことができると期待されています。実装継承のためのクラスの有用性は限定的です。なぜなら、単一継承しかサポートしていないからです(クラスには最大1つのスーパークラスを持つことができます)。そのため、複数のソースからツールメソッドを継承することはできません。それらはすべてスーパークラスから取得する必要があります。
では、どうすればこの問題を解決できるでしょうか?例を使って解決策を探ってみましょう。Employee
がPerson
のサブクラスであるエンタープライズの管理システムを考えてみましょう。
class
Person
{
···
}
class
Employee
extends
Person
{
···
}
さらに、ストレージとデータ検証のためのツールクラスがあります。
class
Storage
{
save
(
database
)
{
···
}
}
class
Validation
{
validate
(
schema
)
{
···
}
}
次のようにツールクラスを含めることができると便利です。
// Invented ES6 syntax:
class
Employee
extends
Storage
,
Validation
,
Person
{
···
}
つまり、Employee
がStorage
のサブクラスであり、それがValidation
のサブクラスであり、それがPerson
のサブクラスであるようにする必要があります。Employee
とPerson
は、そのようなクラスのチェーンで1回だけ使用されます。しかし、Storage
とValidation
は複数回使用されます。それらを、スーパークラスを埋めるクラスのテンプレートとして使用したいのです。そのようなテンプレートは、抽象サブクラスまたはミックスインと呼ばれます。
ES6でミックスインを実装する1つの方法は、入力をスーパークラスとし、出力をそのスーパークラスを拡張するサブクラスとする関数と見なすことです。
const
Storage
=
Sup
=>
class
extends
Sup
{
save
(
database
)
{
···
}
};
const
Validation
=
Sup
=>
class
extends
Sup
{
validate
(
schema
)
{
···
}
};
ここでは、extends
句のオペランドが固定識別子ではなく、任意の式であるという利点を活用しています。これらのミックスインを使用すると、Employee
は次のように作成されます。
class
Employee
extends
Storage
(
Validation
(
Person
))
{
···
}
謝辞。私が知る限り、この手法が最初に登場したのは、Sebastian MarkbågeによるGistです。
これまで見てきたのは、クラスの要点です。内部で何が起こっているかに興味がある場合にのみ、読み進めてください。クラスの構文から始めましょう。以下は、ECMAScript 6仕様のセクションA.4に示されている構文をわずかに変更したものです。
ClassDeclaration:
"class" BindingIdentifier ClassTail
ClassExpression:
"class" BindingIdentifier? ClassTail
ClassTail:
ClassHeritage? "{" ClassBody? "}"
ClassHeritage:
"extends" AssignmentExpression
ClassBody:
ClassElement+
ClassElement:
MethodDefinition
"static" MethodDefinition
";"
MethodDefinition:
PropName "(" FormalParams ")" "{" FuncBody "}"
"*" PropName "(" FormalParams ")" "{" GeneratorBody "}"
"get" PropName "(" ")" "{" FuncBody "}"
"set" PropName "(" PropSetParams ")" "{" FuncBody "}"
PropertyName:
LiteralPropertyName
ComputedPropertyName
LiteralPropertyName:
IdentifierName /* foo */
StringLiteral /* "foo" */
NumericLiteral /* 123.45, 0xFF */
ComputedPropertyName:
"[" Expression "]"
2つの注目点
class
Foo
extends
combine
(
MyMixin
,
MySuperClass
)
{}
eval
またはarguments
にすることはできません。重複するクラス要素名は許可されていません。名前constructor
は、ゲッター、セッター、またはジェネレーターメソッドではなく、通常のメソッドにのみ使用できます。TypeException
をスローします。
class
C
{
m
()
{}
}
new
C
.
prototype
.
m
();
// TypeError
クラス宣言は、(変更可能な)letバインディングを作成します。次の表は、特定のクラスFoo
に関連するプロパティの属性を示しています。
書き込み可能 | 列挙可能 | 構成可能 | |
---|---|---|---|
静的プロパティFoo.* |
true |
false |
true |
true |
false |
false |
false |
true |
false |
false |
true |
プロトタイププロパティFoo.prototype.* |
true |
false |
true |
注釈
クラスには、名前付き関数式と同様に、レキシカル内部名があります。
名前付き関数式には、レキシカル内部名があることをご存知かもしれません。
const
fac
=
function
me
(
n
)
{
if
(
n
>
0
)
{
// Use inner name `me` to
// refer to function
return
n
*
me
(
n
-
1
);
}
else
{
return
1
;
}
};
console
.
log
(
fac
(
3
));
// 6
名前付き関数式の名前me
は、現在関数を保持している変数に影響されない、レキシカルにバインドされた変数になります。
興味深いことに、ES6クラスには、メソッド(コンストラクターメソッドと通常のメソッド)で使用できるレキシカル内部名もあります。
class
C
{
constructor
()
{
// Use inner name C to refer to class
console
.
log
(
`constructor:
${
C
.
prop
}
`
);
}
logProp
()
{
// Use inner name C to refer to class
console
.
log
(
`logProp:
${
C
.
prop
}
`
);
}
}
C
.
prop
=
'Hi!'
;
const
D
=
C
;
C
=
null
;
// C is not a class, anymore:
new
C
().
logProp
();
// TypeError: C is not a function
// But inside the class, the identifier C
// still works
new
D
().
logProp
();
// constructor: Hi!
// logProp: Hi!
(ES6仕様では、内部名はClassDefinitionEvaluationの動的セマンティクスによって設定されます。)
謝辞:クラスに内部名があることを指摘してくれたMichael Ficarraに感謝します。
ECMAScript 6では、サブクラス化は次のようになります。
class
Person
{
constructor
(
name
)
{
this
.
name
=
name
;
}
toString
()
{
return
`Person named
${
this
.
name
}
`
;
}
static
logNames
(
persons
)
{
for
(
const
person
of
persons
)
{
console
.
log
(
person
.
name
);
}
}
}
class
Employee
extends
Person
{
constructor
(
name
,
title
)
{
super
(
name
);
this
.
title
=
title
;
}
toString
()
{
return
`
${
super
.
toString
()
}
(
${
this
.
title
}
)`
;
}
}
const
jane
=
new
Employee
(
'Jane'
,
'CTO'
);
console
.
log
(
jane
.
toString
());
// Person named Jane (CTO)
次のセクションでは、前の例で作成されたオブジェクトの構造を調べます。その後のセクションでは、jane
がどのように割り当てられて初期化されるかを調べます。
前の例では、次のオブジェクトが作成されます。
プロトタイプチェーンとは、[[Prototype]]
関係(これは継承関係)を介してリンクされたオブジェクトです。図では、2つのプロトタイプチェーンを見ることができます。
派生クラスのプロトタイプは、それが拡張するクラスです。この設定の理由は、サブクラスがスーパークラスのすべてのプロパティを継承するようにしたいからです。
> Employee.logNames === Person.logNames
true
基本クラスのプロトタイプは、Function.prototype
であり、これも関数のプロトタイプです。
> const getProto = Object.getPrototypeOf.bind(Object);
> getProto(Person) === Function.prototype
true
> getProto(function () {}) === Function.prototype
true
つまり、基本クラスと、それらから派生したすべてのクラス(それらのプロトタイプ)は関数です。従来のES5関数は、本質的に基本クラスです。
クラスの主な目的は、このプロトタイプチェーンを構築することです。プロトタイプチェーンはObject.prototype
(そのプロトタイプはnull
)で終わります。これにより、Object
は(インスタンスとinstanceof
演算子に関する限り)すべての基本クラスの暗黙のスーパークラスとなります。
この設定の理由は、サブクラスのインスタンスプロトタイプに、スーパークラスのインスタンスプロパティをすべて継承させたいからです。
ちなみに、オブジェクトリテラルで作成されたオブジェクトも、プロトタイプとしてObject.prototype
を持っています。
> Object.getPrototypeOf({}) === Object.prototype
true
クラスコンストラクタ間のデータフローは、ES5でのサブクラス化の標準的な方法とは異なります。内部的には、おおよそ次のようになります。
// Base class: this is where the instance is allocated
function
Person
(
name
)
{
// Performed before entering this constructor:
this
=
Object
.
create
(
new
.
target
.
prototype
);
this
.
name
=
name
;
}
···
function
Employee
(
name
,
title
)
{
// Performed before entering this constructor:
this
=
uninitialized
;
this
=
Reflect
.
construct
(
Person
,
[
name
],
new
.
target
);
// (A)
// super(name);
this
.
title
=
title
;
}
Object
.
setPrototypeOf
(
Employee
,
Person
);
···
const
jane
=
Reflect
.
construct
(
// (B)
Employee
,
[
'Jane'
,
'CTO'
],
Employee
);
// const jane = new Employee('Jane', 'CTO')
インスタンスオブジェクトは、ES6とES5では異なる場所で作成されます。
super()
を介して呼び出され、これがコンストラクタ呼び出しをトリガーします。new
のオペランドで作成されます。スーパークラスコンストラクタは関数呼び出しを介して呼び出されます。前のコードでは、2つの新しいES6機能を使用しています。
new.target
は、すべての関数が持つ暗黙のパラメータです。コンストラクタ呼び出しチェーンにおいて、その役割はスーパメソッド呼び出しチェーンにおけるthis
に似ています。new
(B行のように)を介して直接呼び出された場合、new.target
の値はそのコンストラクタになります。super()
(A行のように)を介して呼び出された場合、new.target
の値は、呼び出しを行っているコンストラクタのnew.target
になります。undefined
になります。つまり、関数が関数として呼び出されたか、コンストラクタとして(new
を介して)呼び出されたかを判断するために、new.target
を使用できます。new.target
は周囲の非アロー関数のnew.target
を参照します。Reflect.construct()
を使用すると、最後のパラメータを介してnew.target
を指定しながらコンストラクタ呼び出しを行うことができます。このサブクラス化方法の利点は、通常のコードで組み込みコンストラクタ(Error
やArray
など)をサブクラス化できることです。後述のセクションで、なぜ異なるアプローチが必要だったのかを説明します。
念のため、ES5でのサブクラス化の方法は次のとおりです。
function
Person
(
name
)
{
this
.
name
=
name
;
}
···
function
Employee
(
name
,
title
)
{
Person
.
call
(
this
,
name
);
this
.
title
=
title
;
}
Employee
.
prototype
=
Object
.
create
(
Person
.
prototype
);
Employee
.
prototype
.
constructor
=
Employee
;
···
this
が最初に初期化されていないということは、super()
を呼び出す前に何らかの方法でthis
にアクセスするとエラーがスローされることを意味します。this
が初期化されると、super()
を呼び出すとReferenceError
が発生します。これにより、super()
を2回呼び出すのを防ぐことができます。return
文なしで)暗黙的に戻る場合、結果はthis
になります。this
が初期化されていない場合、ReferenceError
がスローされます。これにより、super()
を呼び出すのを忘れるのを防ぐことができます。undefined
およびnull
を含む)を返す場合、結果はthis
になります(この動作はES5以前との互換性を維持するために必要です)。this
が初期化されていない場合、TypeError
がスローされます。this
が初期化されているかどうかは関係ありません。extends
句 extends
句がクラスの設定にどのように影響するかを調べましょう(仕様のセクション14.5.14)。
extends
句の値は、「構築可能」(new
を介して呼び出し可能)である必要があります。ただし、null
は許可されています。
class
C
{
}
C
のプロトタイプ:Function.prototype
(通常の関数と同様)C.prototype
のプロトタイプ:Object.prototype
(これはオブジェクトリテラルで作成されたオブジェクトのプロトタイプでもあります)
class
C
extends
B
{
}
C
のプロトタイプ:B
C.prototype
のプロトタイプ:B.prototype
class
C
extends
Object
{
}
C
のプロトタイプ:Object
C.prototype
のプロトタイプ:Object.prototype
最初のケースとのわずかな違いに注意してください。extends
句がない場合、クラスは基本クラスであり、インスタンスを割り当てます。クラスがObject
を拡張する場合、それは派生クラスであり、Object
がインスタンスを割り当てます。結果のインスタンス(およびそのプロトタイプチェーン)は同じですが、到達方法が異なります。
class
C
extends
null
{
}
C
のプロトタイプ:Function.prototype
C.prototype
のプロトタイプ:null
このようなクラスを使用すると、プロトタイプチェーンでObject.prototype
を回避できます。
ECMAScript 5では、ほとんどの組み込みコンストラクタはサブクラス化できません(いくつかの回避策は存在します)。
その理由を理解するために、標準的なES5パターンを使用してArray
をサブクラス化してみましょう。すぐにわかるように、これはうまくいきません。
function
MyArray
(
len
)
{
Array
.
call
(
this
,
len
);
// (A)
}
MyArray
.
prototype
=
Object
.
create
(
Array
.
prototype
);
残念ながら、MyArray
をインスタンス化すると、正しく機能しないことがわかります。インスタンスプロパティlength
は、配列要素を追加しても反応して変化しません。
> var myArr = new MyArray(0);
> myArr.length
0
> myArr[0] = 'foo';
> myArr.length
0
myArr
が適切な配列になるのを妨げる2つの障害があります。
最初の障害:初期化。(A行で)コンストラクタArray
に渡すthis
は完全に無視されます。つまり、Array
を使用してMyArray
用に作成されたインスタンスを設定することはできません。
> var a = [];
> var b = Array.call(a, 3);
> a !== b // a is ignored, b is a new object
true
> b.length // set up correctly
3
> a.length // unchanged
0
2番目の障害:割り当て。Array
によって作成されるインスタンスオブジェクトはエキゾチックです(通常のオブジェクトにはない機能を持つオブジェクトに対するECMAScript仕様で使用される用語)。それらのプロパティlength
は、配列要素の管理を追跡および影響を与えます。一般に、エキゾチックオブジェクトは最初から作成できますが、既存の通常のオブジェクトをエキゾチックオブジェクトに変換することはできません。残念ながら、これはArray
がA行で呼び出された場合に実行する必要があることです。MyArray
用に作成された通常のオブジェクトをエキゾチックな配列オブジェクトに変換する必要があります。
ECMAScript 6では、Array
のサブクラス化は次のようになります。
class
MyArray
extends
Array
{
constructor
(
len
)
{
super
(
len
);
}
}
これは機能します。
> const myArr = new MyArray(0);
> myArr.length
0
> myArr[0] = 'foo';
> myArr.length
1
ES6のサブクラス化へのアプローチが、前述の障害をどのように解消するかを調べてみましょう。
Array
がインスタンスを設定できないという問題は、Array
が完全に構成されたインスタンスを返すことによって解消されます。ES5とは対照的に、このインスタンスはサブクラスのプロトタイプを持っています。次のES6コードは、B行でスーパメソッド呼び出しを行っています。
class
Person
{
constructor
(
name
)
{
this
.
name
=
name
;
}
toString
()
{
// (A)
return
`Person named
${
this
.
name
}
`
;
}
}
class
Employee
extends
Person
{
constructor
(
name
,
title
)
{
super
(
name
);
this
.
title
=
title
;
}
toString
()
{
return
`
${
super
.
toString
()
}
(
${
this
.
title
}
)`
;
// (B)
}
}
const
jane
=
new
Employee
(
'Jane'
,
'CTO'
);
console
.
log
(
jane
.
toString
());
// Person named Jane (CTO)
スーパー呼び出しがどのように機能するかを理解するために、jane
のオブジェクト図を見てみましょう。
B行で、Employee.prototype.toString
は(A行で始まる)オーバーライドしたメソッドへのスーパー呼び出し(B行)を行います。メソッドが格納されているオブジェクトを、そのメソッドのホームオブジェクトと呼びましょう。たとえば、Employee.prototype
はEmployee.prototype.toString()
のホームオブジェクトです。
B行のスーパー呼び出しには、3つのステップが含まれます。
toString
であるメソッドを探します。そのメソッドは、検索が開始されたオブジェクト、またはプロトタイプチェーンの後のオブジェクトで見つかる可能性があります。this
を使用してそのメソッドを呼び出します。そうする理由は、スーパー呼び出しされたメソッドが同じインスタンスプロパティ(この例では、jane
の独自のプロパティ)にアクセスできる必要があるためです。スーパープロパティの取得(super.prop
)または設定(super.prop = 123
)のみを行っている場合でも(メソッド呼び出しではなく)、ゲッターまたはセッターが呼び出される可能性があるため、ステップ#3でthis
が(内部的に)役割を果たす可能性があります。
これらのステップを3つの異なる方法(ただし、同等)で表現してみましょう。
// Variation 1: supermethod calls in ES5
var
result
=
Person
.
prototype
.
toString
.
call
(
this
)
// steps 1,2,3
// Variation 2: ES5, refactored
var
superObject
=
Person
.
prototype
;
// step 1
var
superMethod
=
superObject
.
toString
;
// step 2
var
result
=
superMethod
.
call
(
this
)
// step 3
// Variation 3: ES6
var
homeObject
=
Employee
.
prototype
;
var
superObject
=
Object
.
getPrototypeOf
(
homeObject
);
// step 1
var
superMethod
=
superObject
.
toString
;
// step 2
var
result
=
superMethod
.
call
(
this
)
// step 3
バリエーション3は、ECMAScript 6がスーパー呼び出しを処理する方法です。このアプローチは、関数の環境が持つ2つの内部バインディングによってサポートされています(環境はスコープ内の変数のためのいわゆるバインディングというストレージスペースを提供します)。
[[thisValue]]
:この内部バインディングはECMAScript 5にも存在し、this
の値を格納します。[[HomeObject]]
:環境の関数のホームオブジェクトを参照します。super
を使用するすべてのメソッドが持つ内部スロット[[HomeObject]]
を介して入力されます。バインディングとスロットの両方がECMAScript 6で新しくなりました。super
はどこで使用できますか? プロトタイプチェーンが関係する場合はいつでもスーパープロパティを参照すると便利です。そのため、オブジェクトリテラルとクラス定義内のメソッド定義(ジェネレータメソッド定義、ゲッターとセッターを含む)で使用できます。クラスは派生しているかどうか、メソッドは静的であるかどうかは関係ありません。
プロパティを参照するためにsuper
を使用することは、関数宣言、関数式、ジェネレータ関数では許可されていません。
super
を使用するメソッドは移動できません super
を使用するメソッドは移動できません。このようなメソッドは、作成されたオブジェクトに結び付けられる内部スロット[[HomeObject]]
を持っています。代入を介して移動した場合でも、元のオブジェクトのスーパープロパティを参照し続けます。将来のECMAScriptバージョンでは、そのようなメソッドも転送できる方法があるかもしれません。
ECMAScript 6では、組み込みコンストラクタのもう1つのメカニズムが拡張可能になりました。メソッドがクラスの新しいインスタンスを作成する場合があります。サブクラスを作成した場合、メソッドは自分のクラスのインスタンスを返す必要がありますか、それともサブクラスのインスタンスを返す必要がありますか?いくつかの組み込みのES6メソッドでは、いわゆる種パターンを介してインスタンスを作成する方法を構成できます。
例として、Array
のサブクラスである SortedArray
を考えてみましょう。このクラスのインスタンスに対して map()
を呼び出した場合、不要なソートを避けるために、Array
のインスタンスを返すようにしたいとします。デフォルトでは、map()
はレシーバー (this
) のインスタンスを返しますが、種 (species) パターンを使用すると、それを変更できます。
次の 3 つのセクションでは、例の中で 2 つのヘルパー関数を使用します。
function
isObject
(
value
)
{
return
(
value
!==
null
&&
(
typeof
value
===
'object'
||
typeof
value
===
'function'
));
}
/**
* Spec-internal operation that determines whether `x`
* can be used as a constructor.
*/
function
isConstructor
(
x
)
{
···
}
標準の種パターンは、Promise.prototype.then()
、Typed Array の filter()
メソッド、およびその他の操作で使用されます。その仕組みは次のとおりです。
this.constructor[Symbol.species]
が存在する場合、それを新しいインスタンスのコンストラクターとして使用します。Array
) を使用します。JavaScript で実装すると、このパターンは次のようになります。
function
SpeciesConstructor
(
O
,
defaultConstructor
)
{
const
C
=
O
.
constructor
;
if
(
C
===
undefined
)
{
return
defaultConstructor
;
}
if
(
!
isObject
(
C
))
{
throw
new
TypeError
();
}
const
S
=
C
[
Symbol
.
species
];
if
(
S
===
undefined
||
S
===
null
)
{
return
defaultConstructor
;
}
if
(
!
isConstructor
(
S
))
{
throw
new
TypeError
();
}
return
S
;
}
通常の配列は、種パターンをわずかに異なる方法で実装します。
function
ArraySpeciesCreate
(
self
,
length
)
{
let
C
=
undefined
;
// If the receiver `self` is an Array,
// we use the species pattern
if
(
Array
.
isArray
(
self
))
{
C
=
self
.
constructor
;
if
(
isObject
(
C
))
{
C
=
C
[
Symbol
.
species
];
}
}
// Either `self` is not an Array or the species
// pattern didn’t work out:
// create and return an Array
if
(
C
===
undefined
||
C
===
null
)
{
return
new
Array
(
length
);
}
if
(
!
IsConstructor
(
C
))
{
throw
new
TypeError
();
}
return
new
C
(
length
);
}
Array.prototype.map()
は、ArraySpeciesCreate(this, this.length)
を介して返す配列を作成します。
Promise は、Promise.all()
などの静的メソッドに対して種パターンの変形を使用します。
let
C
=
this
;
// default
if
(
!
isObject
(
C
))
{
throw
new
TypeError
();
}
// The default can be overridden via the property `C[Symbol.species]`
const
S
=
C
[
Symbol
.
species
];
if
(
S
!==
undefined
&&
S
!==
null
)
{
C
=
S
;
}
if
(
!
IsConstructor
(
C
))
{
throw
new
TypeError
();
}
const
instance
=
new
C
(
···
);
これは、プロパティ [Symbol.species]
のデフォルトのゲッターです。
static
get
[
Symbol
.
species
]()
{
return
this
;
}
このデフォルトのゲッターは、組み込みクラスの Array
、ArrayBuffer
、Map
、Promise
、RegExp
、Set
、および %TypedArray%
によって実装されます。これらは、これらの組み込みクラスのサブクラスによって自動的に継承されます。
デフォルトの種をオーバーライドするには、2 つの方法があります。任意のコンストラクターを使用するか、null
を使用するかです。
静的ゲッターを介してデフォルトの種をオーバーライドできます (A 行)。
class
MyArray1
extends
Array
{
static
get
[
Symbol
.
species
]()
{
// (A)
return
Array
;
}
}
結果として、map()
は Array
のインスタンスを返します。
const
result1
=
new
MyArray1
().
map
(
x
=>
x
);
console
.
log
(
result1
instanceof
Array
);
// true
デフォルトの種をオーバーライドしない場合、map()
はサブクラスのインスタンスを返します。
class
MyArray2
extends
Array
{
}
const
result2
=
new
MyArray2
().
map
(
x
=>
x
);
console
.
log
(
result2
instanceof
MyArray2
);
// true
静的ゲッターを使用しない場合は、Object.defineProperty()
を使用する必要があります。代入を使用することはできません。これは、ゲッターのみを持つキーを持つプロパティがすでに存在するためです。つまり、それは読み取り専用であり、代入することはできません。
たとえば、ここでは MyArray1
の種を Array
に設定しています。
Object
.
defineProperty
(
MyArray1
,
Symbol
.
species
,
{
value
:
Array
});
null
に設定する 種を null
に設定すると、デフォルトのコンストラクターが使用されます (どちらのコンストラクターが使用されるかは、使用される種パターンのバリアントによって異なります。詳細については、前のセクションを参照してください)。
class
MyArray3
extends
Array
{
static
get
[
Symbol
.
species
]()
{
return
null
;
}
}
const
result3
=
new
MyArray3
().
map
(
x
=>
x
);
console
.
log
(
result3
instanceof
Array
);
// true
クラスは JavaScript コミュニティ内で論争の的となっています。一方では、クラスベースの言語から来た人々は、JavaScript の型破りな継承メカニズムに対処する必要がなくなったことを喜んでいます。他方では、多くの JavaScript プログラマーは、JavaScript で複雑なのはプロトタイプ継承ではなく、コンストラクターだと主張しています。
ES6 クラスには、いくつかの明確な利点があります。
ES6 クラスに対するいくつかの一般的な不満を見てみましょう。私はそれらのほとんどに同意しますが、クラスの利点はその欠点をはるかに上回るとも思っています。私はそれらが ES6 に含まれていることをうれしく思っており、それらを使用することをお勧めします。
はい、ES6 クラスは JavaScript 継承の真の性質を曖昧にします。クラスの外観 (その構文) と動作 (そのセマンティクス) の間には、不幸な不一致があります。それはオブジェクトのように見えますが、関数です。私の好みでは、クラスはコンストラクター関数ではなく、コンストラクターオブジェクトであるべきでした。私は、Proto.js
プロジェクト で、小さなライブラリ (このアプローチがどれほど適合しているかを証明するもの) を介して、そのアプローチを探ります。
ただし、下位互換性が重要であるため、クラスがコンストラクター関数であることも理にかなっています。そうすることで、ES6 コードと ES5 はより相互運用可能になります。
構文とセマンティクスの不一致は、ES6 以降でいくらかの摩擦を引き起こすでしょう。しかし、ES6 クラスを額面通りに受け取るだけで、快適な生活を送ることができます。私は、その幻想があなたを苦しめることはないと思います。初心者はより早く始めることができ、後で (言語に慣れてから) 舞台裏で何が起こっているのかを読むことができます。
クラスは単一継承のみを提供するため、オブジェクト指向設計に関して表現の自由を著しく制限します。ただし、それらは常にトレイトなどの多重継承メカニズムの基盤となるように計画されてきました。
すると、クラスはインスタンス化可能なエンティティになり、トレイトを組み立てる場所になります。それが起こるまでは、多重継承が必要な場合は、ライブラリに頼る必要があります。
new
のためにあなたを閉じ込める クラスをインスタンス化する場合は、ES6 で new
を使用する必要があります。つまり、呼び出し側を変更せずにクラスからファクトリ関数に切り替えることはできません。これは確かに制限事項ですが、2 つの緩和要因があります。
constructor
メソッドからオブジェクトを返すことで、new
演算子が返すデフォルトの結果をオーバーライドできます。new
から関数呼び出しへの移行は簡単になります。明らかに、ライブラリの場合のように、コードを呼び出すコードを制御しない場合は、これは役に立ちません。したがって、クラスは構文的には ある程度 あなたを制限しますが、JavaScript にトレイトが導入されると、(オブジェクト指向設計に関して) 概念的 にはあなたを制限しません。
現在、クラスの関数呼び出しは禁止されています。これは、将来、クラスを介して関数呼び出しを処理する方法を追加するためのオプションをオープンにしておくために行われました。
クラスに対する Function.prototype.apply()
の類似物は何ですか? つまり、クラス TheClass
と引数の配列 args
がある場合、どのように TheClass
をインスタンス化しますか?
その 1 つの方法は、スプレッド演算子 (...
) を介することです。
function
instantiate
(
TheClass
,
args
)
{
return
new
TheClass
(...
args
);
}
別のオプションは、Reflect.construct()
を使用することです。
function
instantiate
(
TheClass
,
args
)
{
return
Reflect
.
construct
(
TheClass
,
args
);
}
クラスの設計モットーは「最大限に最小限」でした。いくつかの高度な機能が議論されましたが、最終的には TC39 によって満場一致で受け入れられる設計を得るために破棄されました。
今後の ECMAScript バージョンでは、この最小限の設計を拡張できるようになりました。クラスは、トレイト (またはミックスイン)、値オブジェクト (コンテンツが同じ場合は異なるオブジェクトが等しいとみなされるオブジェクト)、および const クラス (不変のインスタンスを生成するクラス) などの機能の基盤を提供します。
次のドキュメントは、この章の重要な情報源です。