ここではC#の以下のようなプログラムをJavaScript 5/TypeScript/ECMAScript 2015に書き換えてみよう。
using System;
namespace cs
{
public class Foo {
private string msg; // インスタンス変数の宣言
public Foo(string msg = "foo") { // デフォルト引数
this.msg = msg; // インスタンス変数の初期化
}
public void Hello() { // インスタンスメソッド
Console.WriteLine("hello " + msg);
}
}
public class Program
{
public static void Main(string[] args)
{
var f = new Foo("world");
f.Hello();
}
}
}
ここでは、Programクラスとは別にFooクラスを定義し、MainメソッドではFooクラスのインスタンスを利用するようにした。注目してほしいのはあくまでもFooクラスの中身だ。Fooクラスではプライベートなインスタンス変数msgと、パブリックなインスタンスメソッドHelloを宣言している。コンストラクターにはデフォルト引数が指定してあり、引数なしでコンストラクターを呼び出すとインスタンス変数msgの値は"foo"に設定される。
Program.Mainメソッドでは、Fooクラスのインスタンスを生成して、そのインスタンスに対してHelloメソッド呼び出しを行っているだけだ。
JavaScript 5では、どんなコードになるだろうか。
var Foo = (function() {
function Foo(msg) {
this.msg = msg === void 0 ? "foo" : msg; // インスタンスプロパティの初期化
}
Foo.prototype.Hello = function() { // インスタンスメソッドの定義
console.log("hello " + this.msg);
}
return Foo;
})();
var f = new Foo("world");
f.Hello();
最後の2行は、C#版のMainメソッドで行っていた処理を外出しにしたものだ。特に説明の必要はないだろう。では、Fooクラスの定義の内容を見ていこう。
インスタンスプロパティとデフォルト引数
まず注目したいのは「this.msg = msg === void 0 ? "foo" : msg;」という部分だ。
C#版のコードではインスタンス変数msgを「string msg;」として宣言して、その後、コンストラクター内部で「this.msg = msg;」として初期値を代入していたが、JavaScript 5ではこのような形でインスタンスプロパティ(インスタンス変数)を宣言することはない。コンストラクター(Foo関数)の中で直接「this.msg = 〜」のようにして、個々のインスタンスが持つプロパティを初期化していけばよい。
この「this」は、new演算子とともに呼び出されたコンストラクター内部では新規に作成されたオブジェクトを、他の関数では呼び出しに使用されたオブジェクトを参照する。そのため、ここでは「new Foo(...)」として呼び出され、新規に作成されたオブジェクトに「msg」という名前のプロパティを追加することになる。なお、JavaScriptではオブジェクト自身が持つメンバーを参照する際には「this」が必須である。
以上で「this.msg =」までの説明は終わりだが、まだ「msg === void 0 ? "foo" : msg」という部分が残っている、C#版のコードでは「Foo(string msg = "foo") 」のようにしてデフォルト引数が指定されていた。JavaScript 5にはデフォルト引数がないので、これをエミュレートしているのがこの部分だ(TypeScript/ECMAScript 2015にはデフォルト引数が追加されている)。
「new Foo()」とFooコンストラクターを無引数で呼び出すとmsgパラメーターの値は「undefined」となる。そこで、このコードでは「void 0が常にundefinedとなる」性質と三項演算子を組み合わせて、デフォルト引数のエミュレートを行っている*2。
*2 JavaScriptコードに記述する「undefined」はグローバルな変数であり、その値は「プリミティブ値のundefined」である。同時に「undefined」は予約語ではないので、コード中で識別子として利用できる。そのため、「var undefined = ...」などとした場合には、もともとのundefinedの値が隠ぺいされてしまう(詳細は「undefined」ページなどを参照されたい)。一方、「void 0」は常に「undefined」となる(void演算子は引数を一つ取り、常にundefinedを返す)ので、この値をmsgパラメーターと比較し、その値が「undefined」かどうかを安全に判定している。そして、引数なしでコンストラクターが呼び出されたら、デフォルト引数の値である"foo"をthis.msgに代入しているのである。
インスタンスメソッドとプロトタイプ
「Foo.prototype.Hello = function() {...}」は個々のインスタンスに対して呼び出しを行うインスタンスメソッドを定義している。先ほど、Mainメソッドを定義したときには「Program.Main = function() {...}」のようにしたのに対して、こちらでは「prototype」というキーワードが増えている。
先ほども述べたように、JavaScriptでは全てのオブジェクトは何らかのプロトタイプを基に生み出される。そして、上のコードに出てきたprototypeこそが、JavaScriptのオブジェクト生成の源であるプロトタイプを参照するものであり、以降で説明するクラスの継承などでも重要な役割を果たす。
JavaScriptでは、全てのオブジェクトはそのプロトタイプを持つ(__proto__プロパティ)。特に何も指定しなければObject型のオブジェクトが、そのプロトタイプとなる。そして、プロトタイプを同じくするオブジェクトは全て、そのプロトタイプが持つ属性を共有する(「よって、通常のオブジェクトは『toString』メソッドなどのObject型のインスタンスメソッドを共通に呼び出せる」と書けば、何となく.NETの世界との類似性が感じられるのではなかろうか)。
一方、コンストラクターはprototypeプロパティを持つ。そして、そのコンストラクターを利用して作成された全てのオブジェクトは、コンストラクターのprototypeプロパティが参照するプロトタイプを共有するように作成されるのである。
よって、上のコードの「Foo.prototype.Hello = function() {...}」は「FooコンストラクターのプロトタイプのHelloプロパティに関数を設定する」ことになる(その値が関数となっているプロパティのことを「メソッド」と呼ぶ)。そして、Fooコンストラクターを利用して作成されたオブジェクトを利用して、そのHelloメソッドを呼び出せるのだ。
と文字で説明しても分かりにくいので、上のコードで行っている二つの処理を図にしてみよう。
とまあ、JavaScript 5のコードはやはりC#プログラマーには難解なところがあるかもしれない。では、TypeScript/ECMAScript 2015ではどうなるだろう。
TypeScriptのコードは以下のようになる。
class Foo {
private msg:string; // プライベートなプロパティの宣言
constructor(msg:string = "foo") { // デフォルト引数
this.msg = msg; // プロパティの初期化
}
public Hello():void { // インスタンスメソッド
console.log("hello " + this.msg);
}
}
var f = new Foo("world");
f.Hello();
C#プログラマーには非常に分かりやすい形で記述できる。
まず「constructor(msg:string = "foo")」とデフォルト引数を指定できることが分かる。もう一つ注意しておきたいのは「private msg:string;」行だ。TypeScriptではプライベートなプロパティを定義できるのだ。ただし、これはあくまでも仕様上の話である。TypeScriptコードは最終的にJavaScriptコードに変換されるが、前述した通り、JavaScriptではプライベートなプロパティは存在しない(クロージャを使うことで、疑似的に隠ぺいすることは可能だ)。上記のコードをJavaScript 5にコンパイルしたものを見てみよう。
var Foo = (function () {
function Foo(msg) {
if (msg === void 0) { msg = "foo"; }
this.msg = msg; // 生成されたコードでは自由にmsgプロパティを使用可能
}
Foo.prototype.Hello = function () {
console.log("hello " + this.msg);
};
return Foo;
})();
var f = new Foo("world");
f.Hello();
先ほど見たJavaScript 5版のコードとほぼ同じものが出来上がっている(違いは、三項演算子ではなくif文を使ってデフォルト引数をエミュレートしているところくらいだ)。そして、TypeScriptのコードで指定した「private」という型注釈(に関連した情報)は一切ない。型注釈はあくまでもTypeScriptでコードを記述する際の支援だと考えるのがよい。JavaScriptへのコンパイル時にデータ型に関連したエラーが発生するのであれば、それは静的なコード解析で問題があることを教えてくれている。よって、そこを修正することで、JavaScriptに変換された後でもデータ型の取り扱いに関して整合性が保証されるということだ。これはHelloメソッドに付加した「public」についても同様だ。
次にECMAScript 2015のコードを見てみよう。TypeScriptのコードとほぼ同様なことが期待される(型注釈を除く)。
class Foo {
constructor(msg = "foo") {
this.msg = msg; // プロパティの初期化
}
Hello() { // インスタンスメソッド
console.log("hello " + this.msg);
}
}
var f = new Foo("world");
f.Hello();
TypeScriptのコードとほぼ同様だが、型注釈以外に異なる点が一つある。それは、TypeScriptにはあった「private msg:string;」行に相当する行がないことだ。実はECMAScript 2015では、このようなプロパティは定義できない。JavaScript 5のコードと同様に、コンストラクターの中でプロパティを適宜設定していくだけだ。
なお、TypeScriptとECMAScript 2015でのコーディングの違いとして、TypeScriptではコンストラクターのパラメーター指定で直接プロパティに値を代入できる点がある。これを「パラメータープロパティ宣言」と呼ぶ。以下に例を示す。
class Foo {
// private msg:string; ← プロパティの宣言を削除
constructor(private msg:string = "foo") { // コンストラクターの引数でプロパティを宣言
// this.msg = msg; ← コンストラクター内での初期化は不要
}
…… 省略 ……
}
上のコードのようにコンストラクターのパラメーターにアクセス修飾子を付加することで、クラス定義内でのプロパティの定義や、コンストラクター内部でのプロパティへのパラメーター値の代入などを記述することなく、プロパティの宣言と初期化が可能だ。このコードではデフォルト値を指定しているので初期化は必ず行われるが、デフォルト値を指定していない場合に引数を指定せずにコンストラクターを呼び出すと、そのプロパティの値は「undefined」となる(コンパイル時にエラーも発生する)。デフォルト引数と組み合わせて使うのが良策だと思われる。プロパティがたくさんある場合には、パラメータープロパティ宣言を使うことでコーディング量を大幅に削減できるだろう。
では次に、クラスの継承について見てみよう。
Copyright© Digital Advantage Corp. All Rights Reserved.