JavaScriptには、オブジェクト指向プログラミング(OOP)のための機能が備わっています。 OOPでは、「テンプレート」(クラス)からオブジェクト(インスタンス)を作成し、特定のデータと機能を持たせます。 データのプロパティはOOPの文脈では_フィールド_と呼ばれ、関数のプロパティは_メソッド_と呼ばれます。
JavaScriptには、2015年にクラスが言語仕様に追加されるまでクラスはまったくありませんでしたが、プロトタイプベースの継承を使ったオブジェクト指向プログラミングは可能でした。
そして、今日ではclassキーワードが利用できますが、JavaScriptは今でも_プロトタイプベース_の言語です。
プロトタイプベースの言語であることの意味と、JavaScriptが実際にどのように動作するのかを理解するために、クラスが存在しなかった時代にさかのぼってみましょう。
JavaScriptでは、テンプレート(クラス)は通常の関数によって実現されます。
関数をそのようなテンプレートとして使う場合、その関数は_コンストラクター関数_と呼ばれ、慣例として関数名を大文字で始めます。
インスタンス(オブジェクト)は、コンストラクター関数を呼び出すときにnewキーワードを使ってテンプレートから生成されます。
function Car() {
// ...
}
const myCar = new Car();
const yourCar = new Car();
重要なのは、JavaScriptでは、インスタンスが作成されたあとも、インスタンスとコンストラクター関数のあいだに関係が保たれるという点です。
すべてのインスタンスオブジェクトには、言語仕様で[[prototype]]と呼ばれる、隠された内部プロパティが含まれます。
これは、コンストラクター関数のprototypeキーの値への参照を保持します。
そうです、そのとおりです。JavaScriptの関数は、舞台裏ではオブジェクトでもあるため、キーと値のペアを持てます。
まとめると:
prototypeプロパティは、インスタンスの_プロトタイプ_になります。多くの場合、派生したすべてのオブジェクト(インスタンス)にいくつかのフィールドを含め、オブジェクトの構築時にそれらの初期値を渡したいことがあります。
これは、thisキーワードによって実現できます。
コンストラクター関数の内部で、thisはnewで作成される新しいオブジェクトを表します。
newを付けて呼び出されたとき、thisはコンストラクター関数から自動的に返されます。
つまり、コンストラクター関数内でthisに追加することで、新しいインスタンスにフィールドを追加できます。
function Car(color, weight) {
this.color = color;
this.weight = weight;
this.engineRunning = false;
}
const myCar = new Car('red', '2mt');
myCar.color;
// => 'red'
myCar.engineRunning;
// => false
メソッドは、コンストラクター関数のprototypeプロパティを通じて追加します。
メソッドの内部では、thisを通じてインスタンスのフィールドにアクセスできます。
これは、次の一般的なルールによって機能します。
関数がオブジェクトのメソッドとして呼び出されると、その
thisは、そのメソッドが呼び出されたオブジェクトに設定されます。1
function Car() {
this.engineRunning = false;
// ...
}
Car.prototype.startEngine = function () {
this.engineRunning = true;
};
Car.prototype.addGas = function (litre) {
// ...
};
const myCar = new Car();
myCar.startEngine();
myCar.engineRunning;
// => true
上の例のmyCarは通常のJavaScriptオブジェクトであり、これを調べても(たとえばブラウザーのコンソールで)、myCarオブジェクトの直接の内部に、値が関数であるstartEngineプロパティは見つかりません。
では、上のコードはそもそもどのように動作するのでしょうか?
その秘密は、_プロトタイプチェーン_と呼ばれる仕組みにあります。
オブジェクトの任意のプロパティ(フィールドまたはメソッド)にアクセスしようとすると、JavaScriptはまず、そのキーがオブジェクト自体の直接の内部に見つかるかどうかを確認します。
見つからない場合、元のオブジェクトの[[prototype]]プロパティが参照するオブジェクト内で、そのキーを探し続けます。
前述したように、この例では[[prototype]]はコンストラクター関数のprototypeプロパティを指します。
そこにstartEngine関数を追加したため、JavaScriptはそこでこの関数を見つけます。
function Car() {
// ...
}
Car.prototype.startEngine = function () {
// ...
};
そして、チェーンはそこで終わりません。
Car.prototypeの[[prototype]]プロパティ(myCar.[[prototype]].[[prototype]])は、Object.prototype(Objectコンストラクター関数のprototypeプロパティ)を参照します。
そこには、すべてのJavaScriptオブジェクトで利用できる一般的なメソッド(たとえばtoString())が含まれます。
結論として、myCar.toString()を呼び出すことができ、JavaScriptがプロトタイプチェーン全体を通じてこのメソッドを探すため、そのメソッドは存在します。
プロトタイプチェーンをたどるのは、値を取得するときだけです。 プロパティを直接設定したり、インスタンスオブジェクトのプロパティを削除したりしても、その特定のインスタンスだけが対象になります。 クラスベースの継承を持つ言語に慣れていると、これは予想外かもしれません。
今日では、JavaScriptはclassキーワードを使ってクラスを定義できます。
これは2015年に言語仕様に追加されました。
一方で、これはクラスを読み書きしやすくする糖衣構文を提供します。
新しい構文は、C++やJavaなどの言語でクラスを書く方法により似ています。
それらの言語から移行してきたエンジニアは、適応しやすくなります。
その一方で、クラスの構文は、プロトタイプの構文では利用できない新しい言語機能への道を開きます。
新しい構文では、クラスはclassキーワードで定義し、その後にクラス名と波括弧({})で囲んだクラス本体を書きます。
本体にはコンストラクター関数の定義、つまりconstructorという名前の特別なメソッドが含まれます。
この関数は、プロトタイプの構文におけるコンストラクター関数とまったく同じように動作します。
クラス本体には、すべてのメソッド定義も含まれます。
メソッドの構文は、オブジェクト内に関数を値として追加するときに見た省略記法に似ています。オブジェクトの概念を参照してください。
class Car {
constructor(color, weight) {
this.color = color;
this.weight = weight;
this.engineRunning = false;
}
startEngine() {
this.engineRunning = true;
}
addGas(litre) {
// ...
}
}
const myCar = new Car();
myCar.startEngine();
myCar.engineRunning;
// => true
舞台裏では、JavaScriptは今でもプロトタイプベースの言語であることを覚えておいてください。 上の「プロトタイプの構文」のセクションで学んだすべての仕組みは、今でも当てはまります。
デフォルトでは、JavaScriptではすべてのインスタンスフィールドがパブリックです。 これらは直接アクセスして代入できます。
ただし、アンダースコアで始まるフィールドとメソッドはプライベートとして扱うという確立された慣例があります。 これらはクラスの外部から直接アクセスするべきではありません。
プライベートフィールドには、ゲッターとセッターが伴うことがあります。
getとsetキーワードを使うと、関数と同じ名前のプロパティがアクセスまたは代入されたときに実行される関数を定義できます。
class Car {
constructor() {
this._mileage = 0;
}
get mileage() {
return this._mileage;
}
set mileage(value) {
throw new Error(`Mileage cannot be manipulated, ${value} is ignored.`);
// Just an example, usually you would not provide a setter in this case.
}
}
const myCar = new Car();
myCar.mileage;
// => 0
myCar.mileage = 100;
// => Error: Mileage cannot be manipulated, 100 is ignored.
thisの例:オブジェクトのメソッドとして、MDN。https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/this#as_an_object_method(2021年12月3日参照) ↩
この演習では、ウィンドウシステムベースのコンピューターシステムをシミュレートします。 移動したりサイズを変更したりできるウィンドウを作成します。 次の図は、このあと扱う値を表したものです。
<--------------------- screenSize.width --------------------->
^ ┌────────────────────────────────────────────────────────────┐
| │ │
| │ position.x, _ │
| │ position.y \ │
| │ \<----- size.width -----> │
| │ ^ *──────────────────────┐ │
| │ | │ title │ │
| │ | ├──────────────────────┤ │
screenSize.height │ | │ │ │
| │ size.height │ │ │
| │ | │ contents │ │
| │ | │ │ │
| │ | │ │ │
| │ v └──────────────────────┘ │
| │ │
| │ │
v └────────────────────────────────────────────────────────────┘
📣 JavaScriptの幅広いスキルを練習するために、タスク1と2はプロトタイプ構文で、残りのタスクはクラス構文で解いてみましょう。
Sizeという名前のクラス(コンストラクター関数)を定義します。
ウィンドウの現在の寸法を保存するwidthとheightという2つのフィールドを持たせます。
コンストラクター関数は、これらのフィールドの初期値を受け取ります。
幅は1つ目の入力として、高さは2つ目の入力として渡されます。
幅と高さのデフォルト値は、それぞれ80と60です。
さらに、新しい幅と高さを入力として受け取り、新しいサイズを反映するようにフィールドを変更するresize(newWidth, newHeight)メソッドを定義します。
const size = new Size(1080, 764);
size.width;
// => 1080
size.height;
// => 764
size.resize(1920, 1080);
size.width;
// => 1920
size.height;
// => 1080
Positionという名前のクラス(コンストラクター関数)を、ウィンドウの左上隅の現在の水平位置と垂直位置をそれぞれ保存するxとyという2つのフィールドとともに定義します。
コンストラクター関数は、これらのフィールドの初期値を受け取ります。
xの値は1つ目の入力として、yの値は2つ目の入力として渡されます。
どちらのフィールドもデフォルト値は0です。
位置(0, 0)は画面の左上隅で、右に移動するほどxの値が大きくなり、下に移動するほどyの値が大きくなります。
また、新しいxとyを入力として受け取り、新しい位置を反映するようにプロパティを変更するmove(newX, newY)メソッドを定義します。
const point = new Position();
point.x;
// => 0
point.y;
// => 0
point.move(100, 200);
point.x;
// => 100
point.y;
// => 200
次のフィールドを持つProgramWindowクラスを定義します。
screenSize:widthが800、heightが600のSize型の固定値を保持しますsize:Size型の値を保持します。初期値はSizeインスタンスのデフォルト値ですposition:Position型の値を保持します。初期値はPositionインスタンスのデフォルト値ですウィンドウを開いたとき(作成したとき)は、最初は必ずデフォルトのサイズと位置になります。
const programWindow = new ProgramWindow();
programWindow.screenSize.width;
// => 800
// Similar for the other fields.
補足:ProgramWindowという名前を使っているのは、ブラウザー環境にある組み込みのWindowクラスとこのクラスを区別するためです。
ProgramWindowクラスには、resizeメソッドを含めます。
Size型の入力を1つ受け取り、ウィンドウを指定されたサイズにリサイズしようとします。
ただし、新しいサイズはある範囲を超えることはできません。
x = 400、y = 300で、height = 400、width = 300へのリサイズが要求された場合、y方向に画面が足りず要求をすべて満たせないため、ウィンドウはheight = 300、width = 300にリサイズされます。const programWindow = new ProgramWindow();
const newSize = new Size(600, 400);
programWindow.resize(newSize);
programWindow.size.width;
// => 600
programWindow.size.height;
// => 400
リサイズ機能に加えて、ProgramWindowクラスにはmoveメソッドも含めます。
Position型の入力を1つ受け取ります。
moveメソッドはresizeと似ていますが、サイズではなく、ウィンドウの_位置_を要求された値に調整します。
resizeと同様に、新しい位置も一定の範囲を超えることはできません。
xとyのどちらも、位置の最小値は0です。x = 250、y = 100で、x = 600、y = 200への移動が要求された場合、x方向に画面が足りず要求をすべて満たせないため、ウィンドウはx = 550、y = 200に移動されます。const programWindow = new ProgramWindow();
const newPosition = new Position(50, 100);
programWindow.move(newPosition);
programWindow.position.x;
// => 50
programWindow.position.y;
// => 100
ProgramWindowインスタンスを入力として受け取り、ウィンドウを指定されたサイズと位置に変更するchangeWindow関数を実装します。
この関数は、変更を適用したあとに、渡されたProgramWindowインスタンスを返します。
ウィンドウは、幅400、高さ300、位置がx = 100、y = 150になるようにします。
const programWindow = new ProgramWindow();
changeWindow(programWindow);
programWindow.size.width;
// => 400
// Similar for the other fields.
Exercismに登録すれば、37個のコンセプト159個の演習、そして本物の人間によるメンタリングとともに、JavaScriptを学んでマスターできます。すべて無料です。