オブジェクト、クラス、プロトタイプの継承の操作
学習の目的
この単元を完了すると、次のことができるようになります。
- オブジェクトリテラル表記とコンストラクターを使用してオブジェクトを作成する。
- オブジェクトにプロパティと関数を代入する。
- JavaScript のオブジェクト継承におけるプロトタイプの役割を特定する。
- JavaScript のクラス構文について説明する。
- Lightning Web コンポーネントにおける継承とオブジェクトリテラル表記の役割を説明する。
JavaScript という言語を説明する方法はたくさんあります。どのような定義を選ぶにしろ、JavaScript でオブジェクトの概念が重要であることには誰もが同意することでしょう。JavaScript のオブジェクトとそのしくみに対する理解が深まるほど、効率的な JavaScript を記述できるようになります。
始める前に、オブジェクトについていくつかの注意事項があります。
- オブジェクトには、Apex、Java、C# の開発者が考えるようなクラスがありません。
- どのオブジェクトも別のオブジェクトから継承します。
- オブジェクトは変更可能です。
- オブジェクトは作成時に独自の可変コンテキストを取得します。
オブジェクトの作成
構文の点から言えば、JavaScript ではオブジェクトを数通りの方法で作成できます。けれども、オブジェクトをどのような方法で作成するにしろ、実際には Object.create()
という基盤となる API を抽象化することになります。
場合によっては Object.create()
を直接使用することが妥当なこともありますが、ここでは説明しません。その代わり、オブジェクトを作成するより一般的な方法を見ていきます。
オブジェクトリテラル表記
1 つ目のオブジェクト作成構文は、オブジェクトリテラル表記というものです。これは、オブジェクトを一度にまとめて宣言して代入する、シンプルな宣言型の方法です。オブジェクトはその後、同じステートメントの一部としてただちに代入されます。
const bike = { gears: 10, currentGear: 3, changeGear: function(direction, changeBy) { if (direction === 'up') { this.currentGear += changeBy; } else { this.currentGear -= changeBy; } } } console.log(bike.gears); // 10 console.log(bike.currentGear); //3 bike.changeGear('up', 1); console.log(bike.currentGear); //4
オブジェクトリテラル表記は本質的に宣言型です。この例の bike
オブジェクトには、gears
プロパティと currentGear
プロパティと changeGear
関数という 3 つのメンバーがあります。オブジェクトが作成された後でこれらのメンバーを参照する場合は、ドット表記を使用します。
リテラルオブジェクトは単発のオブジェクトに適しています。他方、同じ種別のオブジェクトを 2 つ以上作成する場合は、実用的ではありません。この場合は、新しいオブジェクトを作成する、繰り返し可能なロジックが必要です。
コンストラクターを使用した新しいオブジェクト
オブジェクトを作成するもう 1 つの方法はコンストラクターを使用することです。コンストラクターとは、オブジェクトを作成して代入するときにそのプロパティを設定するための説明を含む関数です。この方法では、同じプロパティのオブジェクトのインスタンスを多数作成できるという点で、オブジェクトリテラルよりも便利です。
function Bike(gears, startGear) { this.gears = gears; this.currentGear = startGear; } Bike.prototype.changeGear = function(direction,changeBy){ if(direction === 'up') { this.currentGear += changeBy; } else { this.currentGear -= changeBy; } } const bike = new Bike(10, 3); console.log(bike.gears); // 10 console.log(bike.currentGear); //3 bike.changeGear('up', 1); console.log(bike.currentGear); //4
この例で Bike
は、オブジェクトを定義する通常の JavaScript 関数です。ここでは JavaScript のルールに従って、この関数がコンストラクターであることを知らせるために最初の文字を大文字にしています。この new
キーワードは極めて重要です。new
がなければ、this
ポインターが意図するオブジェクトをポイントせず、予期しない動作が生じます。this
ついては、後の単元のコンテキストの説明でもう一度取り上げます。
changeGear
関数の代入は、prototype
というものを使用して行われます。この方法では、関数が一度定義されると、このコンストラクターで作成されたすべてのインスタンスに確実に共有されます。プロトタイプの使用と継承については、この単元の後半で説明します。
構文の点では、オブジェクトリテラル表記とコンストラクターはかなり異なりますが、どちらの場合も、新しいオブジェクトがメモリに作成され、bike
変数がそのオブジェクトへのポインターになります。コンストラクターを使用すると、同じプロパティと関数が設定された Bike
オブジェクトをたくさん作成できます。
オブジェクトへのプロパティと関数の代入
上記の bike
の例から、オブジェクトにはプロパティと関数という 2 種類のメンバーがあるのではないかと推測した方、正解です。
プロパティには次の 3 つの基本的な形態があります。
- プリミティブ
- オブジェクト
- 配列
このモジュールを作成した時点で、JavaScript には、文字列、数値、Boolean、null
、undefined
、記号、BIGINT の 7 つのプリミティブ型があります。プリミティブ型は変更できません。変数がプリミティブ型の場合は、代入時に値によって変数が渡されます。つまり、プリミティブが代入されるたびに、値のコピーが作成され、新しい変数に代入されます。
JavaScript でプリミティブに該当しないものはほぼすべてオブジェクトです。オブジェクトリテラル表記では、オブジェクトのプロパティが中括弧で囲まれます。
配列自体も JavaScript ではオブジェクトとして実装されます。配列は、Array()
コンストラクター関数、または角括弧で囲まれたリテラル表記を使用して作成できます。
関数については、このモジュールに独自の単元があるため、ここでは説明しませんが、上記を基にもう一度オブジェクトリテラル表記を使用してさらに複雑な bike
オブジェクトを定義してみましょう。
const bike = { frontGearIndex: 0, rearGearIndex: 0, transmission: { frontGearTeeth: [30,45], rearGearTeeth: [11,13,15,17,19,21,24,28,32,36] }, calculateGearRatio: function() { let front = this.transmission.frontGearTeeth[this.frontGearIndex], rear = this.transmission.rearGearTeeth[this.rearGearIndex]; return (front / rear); }, changeGear: function(frontOrRear, newValue) { if (frontOrRear === 'front') { this.frontGearIndex = newValue; } else { this.rearGearIndex = newValue; } } };
括弧構文によるプロパティの参照
オブジェクトメンバーの参照は、通常ドット表記を使用して行います。たとえば、前の例では、オブジェクトのプロパティと関数を次のように参照します。
bike.frontGearIndex bike.transmission.frontGearTeeth bike.calculateGearRatio()
ドット表記では、プロパティの名前に厳密なルールがあります。けれども JavaScript では、括弧表記という別の構文も使用できます。括弧表記では上記のメンバーが次のように参照されます。
bike["frontGearIndex"] bike["transmission"]["frontGearTeeth"] bike["calculateGearRatio"]()
括弧表記のほうが入力する文字数が多いものの、2 つの利点があります。プロパティや関数に任意の名前を付けることができます。そして、文字列であるため、変数を介してプロパティや関数の名前を渡してコールできます。
では、changeGear
関数を再考し、この方法ではどのように機能するのか見てみましょう。ここでは、フロントギアとリアギアの上下のシフトを定義する 4 つの関数を使用します。changeGear
関数で、文字列パラメーターに基づいてコールする関数の名前を構築し、その名前をコールします。
changeGear: function(frontOrRear, upOrDown) { let shiftFunction = frontOrRear + upOrDown; this[shiftFunction](); }, frontUp: function(){ this.frontGearIndex += 1; }, frontDown: function(){ this.frontGearIndex -= 1; }, rearUp: function(){ this.rearGearIndex += 1; }, rearDown: function(){ this.rearGearIndex -= 1; }
これらの名前を bike オブジェクトに追加すると、どのように機能するのかを確認できます。
console.log(bike.calculateGearRatio()); // 2.727272727 //Calls the frontUp() function bike.changeGear("front", "Up"); console.log(bike.calculateGearRatio()); // 4.090909091 //calls the rearUp() function bike.changeGear("rear", "Up"); console.log(bike.calculateGearRatio()); // 3.461538461
オブジェクトの変更可能性
オブジェクトを定義する各種の構文のほか、JavaScript のオブジェクトにはもう 1 つ重要な原則があります。変更可能性です。
JavaScript のオブジェクトは変更可能で、オブジェクトの形態を変えたければ変更できることを意味します。
作成した bike
オブジェクトを見てみましょう。たとえば、新しいプロパティや関数を追加することができます。
bike.isTandem = true; bike.popAWheelie = function() { … };
オブジェクトが最初に定義されたコードにアクセスできなくても、オブジェクトがメモリに入れられた後でその形態を変更できます。ここで重要な点は、オブジェクトの 1 つのインスタンスのみが変更されることです。では、もう一度 Bike
コンストラクターを見てみましょう。
const bike1 = new Bike(); const bike2 = new Bike(); bike1.isTandem = true; console.log(bike1.isTandem); // true console.log(bike2.isTandem); // undefined
いくつかのオブジェクトでプロパティまたはメソッドを共有したい場合は、継承モデルを使用します。詳しく見てみましょう。
オブジェクトと継承
JavaScript には従来の言語で定義されていたようなクラスはありませんが、プロトタイプ継承という継承モデルがあります。
プロトタイプは、実際のところもう 1 つのオブジェクトです。これはメモリ内に存在し、他のオブジェクトが同じプロトタイプを共有する場合に継承するプロパティや関数を定義します。
JavaScript では従来、オブジェクトが同じコンストラクター関数を共有するというやり方で、同じプロトタイプを共有します。Bike
コンストラクターを思い出してください。changeGear
関数を prototype
というものに代入します。
function Bike(gears, startGear) { this.gears = gears; this.currentGear = startGear; } Bike.prototype.changeGear = function(direction, changeBy) { if (direction === 'up') { this.currentGear += changeBy; } else { this.currentGear -= changeBy; } }
こうすれば、Bike
から作成される各オブジェクトが changeGear
関数を継承します。
プロトタイプには、複数レベルの継承を実装でき、プロトタイプチェーンとして参照されます。コンストラクター関数を使用したプロトタイプチェーンの実装は複雑で、相当量の定型コードが必要です。この点も当モジュールの範疇を超えています。ここで知っておくべきことは、プロトタイプチェーンの複雑さに対処するために、ECMA が、継承を実装するより端的な構文、つまり class
構文に関する標準を導入しているということです。
クラスと JavaScript
「クラス」という単語を読んで安堵感を覚え、真のクラスベースの継承を作成するものを見ていくのかなと思った方は、落胆することになるでしょう。JavaScript の class
というキーワードは、コンストラクター関数を使用してプロトタイプ継承の複雑性に対処する糖衣構文のようなものです。上辺の糖衣の下では、この場合もエンジンが Object.create
を使用し、(オブジェクト指向という意味での) クラスはありません。メモリ内にプロトタイプオブジェクトがあるだけで、実際はこれが継承元になります。
幸いにも、JavaScript 固有の事項をいくつか考慮する必要はあるものの、見た目は Java や C# のコードによく似ています。
ここでは JavaScript クラスについて詳述しませんが、後学のために class 構文を使用して bike オブジェクトを実装したバージョンを確認しておくとよいでしょう。
class Bike { constructor(gears, startGear){ this.gears = gears; this.currentGear = startGear; } changeGear(direction, changeBy) { if (direction === 'up') { this.currentGear += changeBy; } else { this.currentGear -= changeBy; } } } const bike = new Bike(10, 5); console.log(bike.currentGear); // 5 bike.changeGear('up', 2); console.log(bike.currentGear); // 7
ご覧のとおり、この構文は Java や Apex のクラスとよく似ています。明確な違いは、コンストラクター関数に常に constructor
という名前が付けられていることです。重要な特徴は、Object.prototype を直接参照しなくても、関数や属性が自動的にプロトタイプチェーンに属することです。このため、複数レベルのプロトタイプ継承を簡単に作成できます。
Lightning Web コンポーネントとオブジェクト
これまでに説明した数種の構文やプロトタイプチェーンなど、この単元の内容のいくつかは Lightning Web コンポーネントの開発に関連しています。
クラスと Lightning Web コンポーネント
Lightning Web コンポーネントは、JavaScript の多数の最新機能を活用しており、その代表例が class 構文の使用です。コンポーネントは通常、LightningElement という別のクラスを拡張する JavaScript クラスによって定義されます。次のようになります。
import { LightningElement } from lwc; export default class MyComponent extends LightningElement { myProperty; myFunction() { console.log(this.myProperty); } }
Lightning Web コンポーネントの機能は、JavaScript クラスで定義されます。この例では、モジュール (import
と export
) に関してまだ説明していない一部の構文も使用しています。
オブジェクトリテラル
このモジュールのいくつかの例では、オブジェクトがどのように機能するかを学習する目的で、オブジェクトリテラル内で関数を宣言しています。このやり方は、最新の JavaScript で推奨される実践法ではありません。オブジェクトリテラルは、JavaScript プログラムの機能部分間でデータを渡すアドホックデータ構造を作成する優れた方法ですが、オブジェクトリテラルで関数を定義することは回避します。
リソース
- JavaScript の Object
- オブジェクトの利用
- JavaScript のオブジェクトモデルの詳細
- Trailhead プロジェクト: Lightning Web コンポーネントを使用した熊追跡アプリケーションの作成
- Lightning Web Components Developer Guide (Lightning Web コンポーネント開発者ガイド): Define a Component (コンポーネントの定義)