JavaScript 再入門: JavaScript の class 構文はシンタックスシュガー
知ってるようで実は知らない JavaScript 。今回は、そんな JavaScript のオブジェクト指向の仕組みについてまとめてみました。
JavaScript のコンソールでの検証には、基本的に Mac 版 Safari 16.4 を使用していますが、適宜 Chrome や Edge のコンソールでも確認しています。
JavaScript の class 構文はシンタックスシュガー
JavaScript はオブジェクト指向言語です。また、モダン JavaScript では、class 構文が使えます。ただし、 JavaScript の class 構文はいわゆるシンタックスシュガーと呼ばれるものです。
シンタックスシュガーとは英語で syntax sugar もしくは syntactic sugar、日本語訳は糖衣構文もしくは構文糖などと訳されます。
糖衣構文(とういこうぶん、英: syntactic sugar あるいは syntax sugar)は、プログラミング言語において、読み書きのしやすさのために導入される書き方であり、複雑でわかりにくい書き方と全く同じ意味になるものを、よりシンプルでわかりやすい書き方で書くことができるもののことである。
ということは、 JavaScript ではもともと class 構文を使わなくてもオブジェクト指向プログラミングができるのだけれども、それだと複雑で分かりにくい書き方になるので、後からよりシンプルでわかりやすい書き方として class 構文が導入された、ということになります。実際、 class 構文が導入されたのは、ES2015からだそうです。
オブジェクトリテラル
というわけで、もともと JavaScript では、クラスを宣言する必要なしにオブジェクトを使うことができます。オブジェクトは、名前と値のペアの集合(プロパティの集合)で表されます。JavaScript のオブジェクトは、基本的には「ディクショナリ」や「連想配列」などと呼ばれるもの同じものです。
シンプルなオブジェクトリテラルは以下の通りです。
{x:1, y:2, z:3}
{キー名:値, キー名:値, ...}
のように書きます。このように、classがなくてもオブジェクトを生成することができるので、 JavaScript のオブジェクトは、他言語のような特定のクラスのインスタンスというわけではありません。
以下のように、{x:1, y:2, z:3}
の型はオブジェクトです。
typeof({x:1, y:2, z:3})
//"object"
オブジェクトなのでドット記法で要素にアクセスすることができます。
console.log({x:1, y:2, z:3}.x)
//1
ただし、ブラウザのコンソールなどに直接記述する場合は、{}
が間違ってブロックと解釈されないように丸括弧で囲みます。
({x:1, y:2, z:3}).x
//1
当然、変数に代入することもできます。
let obj = {x:1, y:2, z:3}
console.log(obj.x)
//1
オブジェクトリテラルでのメソッド
オブジェクトリテラルでメソッドも記述できます。木村さんのお小遣いは10000円ということにします。
let kimura = {
name: 'Kimura Takumi',
allowance: 10000,
increase: function(amount) {
this.allowance += amount
}
}
このように、increase
プロパティに、無名関数の Function オブジェクトを割り当ててメソッドを実装しています。メソッド内のthis
は、レシーバーオブジェクト=インスタンスのkimura
を指します。
increase
メソッドを呼び出して、お小遣いを100円増額します。
kimura.increase(100)
consple.log(kimura.allowance)
//10100
ちなみに、メソッドのショートカット構文では、下記のようにコロンと function キーワードを省略できます。この書き方は「関数を値とするプロパティ」のシンタックスシュガーです。
let kimura = {
name: 'Kimura Takumi',
allowance: 10000,
increase(amount) {
this.allowance += amount
}
}
Object.create メソッドによるオブジェクトの作成
Object.create メソッドでもオブジェクトを作成できます。
Object.create メソッドを利用すると、
・プロパティの詳細情報(読み取り専用か、列挙可能かなど)
・オブジェクトを生成する際に元となる機能(プロトタイプ)
などの詳細な情報を設定できます。
let kimura = Object.create(Object.prototype, {
name: {
value: 'Kimura Takumi',
writable: true,
configurable: true,
enumerable: true
},
allowance: {
value: 10000,
writable: true,
configurable: true,
enumerable: true
},
increase: {
value: function(amount) {
this.allowance += amount
},
writable: true,
configurable: true,
enumerable: true
}
});
第一引数にObject.prototype
を設定しているのは、Object オブジェクトを継承すると言う意味です。prototype
については後の節で説明します。
第二引数には、プロパティとその詳細な設定を記述することができます。この場合、name
allowance
increase
はプロパティ名で、value
にはプロパティの値を設定します。writable
には書き換え可能か、configurable
には属性の変更やプロパティの削除が可能か、enumerable
にはfor ...inによる列挙が可能かを設定できます。
その他にゲッター/セッター関数を定義することもできます。
コンストラクター関数と new 演算子によるオブジェクトの作成
また、コンストラクター関数と new 演算子を使ってインスタンスオブジェクトを作成することもできます。
まず、コンストラクター関数を記述してオブジェクトの型を定義します。中身のない最もシンプルな定義は、下記のように変数Person
に、空の関数リテラルを代入しているだけです。これがいわゆる「クラス」定義に相当します。
let Person = function(){}
コンストラクター関数は、クラス的な意味合いで?慣習的に1文字目は大文字とします。
Person
は、new演算子でオブジェクトのインスタンスを作成できます。
let person1 = new Person()
typeof(person1)
//"object"
Person
は Function オブジェクトです。
typeof(Person)
//"function"
ということは、一般の関数とコンストラクター関数には実質的な違いがないということですね。
JavaScript では、このように関数にクラスとしての役割を与えています。なのでオブジェクトを生成するのに class 宣言などは必要ありません。
以下のように名前付き関数としてコンストラクター関数を宣言することもできます。new 演算子でオブジェクトを生成しています。
function Person(){}
let person1 = new Person()
typeof(person1)
//"object"
typeof(Person)
//"function"
以下ではオブジェクトリテラルの時と同様に、いくつかのプロパティとメソッドを実装しています。
コンストラクター関数内でのthis
は、生成したインスタンスを指します。thisキーワードに対して変数を指定することで、インスタンスのプロパティを設定できます。
function Person(name, allowance) {
this.name = name
this.allowance = allowance
this.increase = function(amount) {
this.allowance += amount
}
}
let kimura = new Person('Kimura Takumi', 20000)
typeof(kimura)
//"object"
console.log(kimura.name)
//Kimura Takumi
クラスではなくプロトタイプ
JavaScriptnの 全てのオブジェクトは、「クラス」ではなく、その原型となる「プロトタイプ(雛型)」に紐づいています。そして、そのプロトタイプ自体もオブジェクトです。なので、JavaScript のオブジェクト指向は、プロトタイプベースのオブジェクト指向と呼ばれることもあります。「インスタンス」という概念はありますが、いわゆる「クラス」がなく、「プロトタイプ」だけが存在するということです。
クラスの代わりにプロトタイプを利用して新たなオブジェクトを生成します。つまり、あらかじめ用意されたオブジェクト(プロトタイプ)をもとに、新しいオブジェクトを生成します。このように、オブジェクトの複製を作ることをインスタンス化と呼んでいます。
JavaScript では、各オブジェクトは特別な隠しプロパティ [[Prototype]] を持っており、それは別のオブジェクト(もしくは null )を参照します。その別のオブジェクトがプロトタイプです。スクリプトから [[Prototype]] プロパティ自体は直接参照することはできません。Object.getPrototypeOf
メソッドで、 [[Prototype]] プロパティを参照することができます。
また、それとは別にすべての関数はprototype
プロパティを持っています。関数のデフォルトの prototype
はconstructor
というプロパティだけを持つオブジェクトで、それは関数自体を指します。関数のprototype
プロパティは、以下のように直接参照することができます。
function Person(name, allowance) {
this.name = name
this.allowance = allowance
this.increase = function(amount) {
this.allowance += amount
}
}
console.log(Person.prototype)
//Person {}
console.log(Person.prototype.constructor)
//function Person(name, allowance) {
// this.name = name
// this.allowance = allowance
// this.increase = function(amount) {
// this.allowance += amount
// }
//}
コンストラクター関数と new 演算子でインスタンスが作成されたときに、生成されたインスタンスの隠しプロパティ [[Prototype]] に、コンストラクター関数の prototype プロパティのオブジェクトがセットされます。デフォルトのままであれば、コンストラクター関数自体が設定されます。
インスタンスのプロトタイプを取得するには、Object.getPrototypeOf
メソッドを使います。
Person
から生成されたオブジェクトのプロトタイプは、以下のようにPerson
になっています。
let person1 = new Person("Kimura Takumi", 10000)
let person2 = new Person("Nakai Kiichi", 20000)
Object.getPrototypeOf(person1) === Person.prototype
//true
Object.getPrototypeOf(person2) === Person.prototype
//true
Object.getPrototypeOf(person1)
//Person {}
Object.getPrototypeOf(person2)
//Person {}
上記の場合、Person
というコンストラクター関数から、person1
、person2
という2つのインスタンスオブジェクトを生成しています。つまりperson1
、person2
のプロトタイプ(雛型)は両方とも同じPerson.prototype
を参照していて、Person.prototype
の中身はコンストラクラー関数自身であるPerson
ということになります。
継承とプロトタイプチェーン
JavaScript では、継承はオブジェクトのプロトタイプチェーンを使って実現されています。
JavaScript の継承はオブジェクトだけで構成されていて、それぞれのオブジェクトは、そのオブジェクトのプロトタイプと呼ばれる別のオブジェクトへのリンクを持つプライベートプロパティ [[Prototype]] を持っています。 [[Prototype]] は、通常はそのオブジェクトを生成したコンストラクターの prototype を参照していて、それがプロトタイプオブジェクトということになります。
そのプロトタイプオブジェクト自身も [[Prototype]] プロパティに自身のコンストラクターの prototype をプロトタイプとして持ち、 null をプロトタイプとするオブジェクトに到達するまでプロトタイプのチェーンは続きます。
null をプロトタイプとするオブジェクトとは、通常はObject.prototype
のことを指し、基本的にはほぼすべてのオブジェクトのルートとなるのは、Objectオブジェクトです。
つまり、ほぼすべてのオブジェクトは、Object
のインスタンスであり、Object
を継承し、最終的にObject.prototype
を参照しています。
簡単な例を見てみます。
const myDate = new Date();
Object.getPrototypeOf(myDate) === Date.prototype
//true
Object.getPrototypeOf(Date.prototype) === Object.prototype
//true
Object.getPrototypeOf(Object.prototype) === null
//true
上記では、Object.getPrototypeOf
メソッドで Date 型のmyDate
オブジェクトから [[Prototype]] を辿っていくと、 Object.prototype
を経て、最終的にnull
に到達します。この場合プロトタイプチェーンは次のようになります。
myDate ---> Date.prototype ---> Object.prototype ---> null
一般的なコンストラクター関数を、例えば Constructor
とした場合、 Constructor.prototype
プロパティは、コンストラクターから作成されたインスタンスの [[Prototype]] となります。また、Constructor.prototype
自身の [[Prototype]] は、 Object.prototype
になります。ただし Object.prototype
自身の [[Prototype]] は null
です。
したがって、以下のように典型的なコンストラクターは
function Constructor() {}
const obj = new Constructor();
以下のようなプロトタイプチェーンを構築します。
obj ---> Constructor.prototype ---> Object.prototype ---> null
Object.getPrototypeOf(obj) === Constructor.prototype
//true
Object.getPrototypeOf(Constructor.prototype) === Object.prototype
//true
Object.getPrototypeOf(Object.prototype) === null
//true
もっと長いプロトタイプチェーンを構築する場合は、 Constructor.prototype
の [[Prototype]] を Object.setPrototypeOf
メソッドでObject.prototype
以外のオブジェクトに設定することができます。
そのような例として、下記では、Derived.prototype
の [[Prototype]] をBase.prototype
に設定しています。
function Base() {}
function Derived() {}
Object.setPrototypeOf(Derived.prototype, Base.prototype,);
const obj = new Derived();
この場合は、以下のようなプロトタイプチェーンが構築されています。
obj ---> Derived.prototype ---> Base.prototype ---> Object.prototype ---> null
console.log(Object.getPrototypeOf(obj) === Derived.prototype)
//true
console.log(Object.getPrototypeOf(Derived.prototype) === Base.prototype)
//true
console.log(Object.getPrototypeOf(Base.prototype) === Object.prototype)
//true
console.log(Object.getPrototypeOf(Object.prototype) === null)
//true
メソッドの共有
コンストラクター内でメソッドを定義すると、インスタンス間で共通のメソッドを持つことができます。しかし、コンストラクターによるメソッドの定義にはメソッドの数とインスタンスの数に比例してメモリを消費するという問題があります。
function Person(name, allowance) {
this.name = name
this.allowance = allowance
this.increase = function(amount) {
this.allowance += amount
}
}
let person1 = new Person("Kimura Takumi", 10000)
let person2 = new Person("Nakai Kiichi", 20000)
コンストラクターは、インスタンスを生成するたびに、プロパティやメソッドを各インスタンスにコピーします。しかし、メソッドの中身はどのインスタンスも同じなので、全インスタンスに同じメソッドをコピーするのはメモリの無駄になります。
以下の、Object.getOwnPropertyNames
メソッドは、与えられたオブジェクトで発見されたすべての直接のプロパティを含む配列を返します。
Object.getOwnPropertyNames(person1)
//["name", "allowance", "increase"] (3)
Object.getOwnPropertyNames(person2)
//["name", "allowance", "increase"] (3)
increase メソッドを使用してお小遣いを少し増やしてみます。
person1.increase(100)
console.log(person1.allowance)
//10100
person2.increase(200)
console.log(person2.allowance)
//20200
JavaScript では、全インスタンスに同じメソッドをコピーしなくて済むように、コンストラクター関数のprototype
プロパティを利用することができます。これまでみてきたように、各オブジェクトはそのコンストラクター関数のprototype
に対する暗黙的な参照を持っています。そして、prototype
が参照するオブジェクトには、プロパティやメソッドを追加することができます。
function Person(name, allowance) {
this.name = name
this.allowance = allowance
}
//prototypeにincreaseメソッドを追加します
Person.prototype.increase = function(amount) {
this.allowance += amount
}
let person1 = new Person("Kimura Takumi", 10000)
let person2 = new Person("Nakai Kiichi", 20000)
Object.getOwnPropertyNames
メソッドの出力結果には、プロトタイプチェーン上のプロパティ/メソッドは含まれません。なので、今回はインスタンス上の直接のプロパティにはincrease
が含まれておらず、Person.prototype
のプロパティに含まれています。
Object.getOwnPropertyNames(person1)
//["name", "allowance"] (2)
Object.getOwnPropertyNames(person2)
//["name", "allowance"] (2)
Object.getOwnPropertyNames(Person.prototype)
//["constructor", "increase"] (2)
しかし、コンストラクターに直接定義した時と同じように、increase
メソッドを使用できます。
person1.increase(100)
console.log(person1.allowance)
//10100
person2.increase(200)
console.log(person2.allowance)
//20200
prototype
プロパティに追加されたメンバーは、そのコンストラクターを元に生成された全てのインスタンスで利用できます。このようにしてプロトタイプからインスタンスにメソッドが継承されます。
オブジェクトのプロパティ/メソッドにアクセスしたときに、そのオブジェクト自身にプロパティ/メソッドが見つからない場合は、プロトタイプを検索してプロパティ/メソッドを探します。それでもプロパティ/メソッドが見つからない場合は、プロトタイプのプロトタイプが遡って検索され、プロトタイプチェーンのどこかでプロパティ/メソッドが得られるか、プロトタイプチェーンの終わりのnull
に達してundefined
を返すまで繰り返します。JavaScript の継承はこのようにして実現されています。
また、プロトタイプオブジェクトへの変更を、インスタンス側で動的に認識することができます。
function Person(name, allowance) {
this.name = name
this.allowance = allowance
}
let person1 = new Person("Kimura Takumi", 10000)
let person2 = new Person("Nakai Kiichi", 20000)
//インスタンス化の後にメソッドを追加
Person.prototype.increase = function(amount) {
this.allowance += amount
}
インスタンス化の後にメソッドを追加していますが、以下のようにメソッドを認識できています。
person1.increase(100)
console.log(person1.allowance)
//10100
person2.increase(200)
console.log(person2.allowance)
//20200
つまり、すでにインスタンスが生成されていても、共有するプロパティ/メソッドを事後的に追加できるといことです。
プロトタイプは動的に変更可能
プロトタイプチェーンのどのメンバーも、実行時に変更することが可能です。
コンストラクター関数のprototype
プロパティは書き換え可能なので、インスタンス作成後にコンストラクター関数のprototype
プロパティが別のオブジェクトに変更された場合、その後 new 演算子によって生成された新しいインスタンスオブジェクトは [[Prototype]] として別のオブジェクトを持ちます。しかし、変更前に生成されたオブジェクトは [[Prototype]] として古い変更前のオブジェクトを保持したままになります。
let Human = function(){}
let Animal = function(){}
let person1 = new Human()
//Human.prototypeをAnimal.prototypeに変更してインスタンスを作成します。
Human.prototype = Animal.prototype
let person2 = new Human()
console.log(Object.getPrototypeOf(person1))
//Human {}
console.log(Object.getPrototypeOf(person2))
//Animal {}
Human.prototype
変更前にインスタンス化したものは、変わらず Human 型のオブジェクトのままです。JavaScript のプロトタイプチェーンは、インスタンスが生成された時点で固定され、その後の変更に関わらず保存されるということです。
また、Object.setPrototypeOf
メソッドを使えばオブジェクトの [[Prototype]] を直接変更することもできます。
let Human = function(){}
let Animal = function(){}
let person1 = new Human()
console.log(Object.getPrototypeOf(person1))
//Human {}
//person1の[[Prototype]]をAnimal.prototypeに変更します。
Object.setPrototypeOf(person1, Animal.prototype)
console.log(Object.getPrototypeOf(person1))
//Animal {}
Java や C# における継承とはあくまでも静的なものですが、JavaScript での継承はこのように動的なものです。同一のオブジェクトが、ある時点ではオブジェクト X を継承しており、別の時点ではオブジェクト Y を継承しているということも可能です。
しかし、Human
コンストラクタで生成したオブジェクトのプロトタイプが実はAnimal
であるというのは非常にわかりづらいので、そのような実装をした場合は大きな混乱を招きかねません。
また、プロトタイプチェーンの途中でコンストラクターのprototype
の変更をすると、その影響は広範囲に及ぶ可能性があり、思わぬ副作用が生じる恐れがあります。
このようにコンストラクターのprototype
やオブジェクトの [[Prototype]] を上書きすると、オブジェクトのコンストラクターが入れ替わってしまうので、あまりよい方法とは思えません。
正しいconstructor
を維持するためにはprototype
や [[Prototype]] の全体を上書きするのではなくて、上記の「継承とメソッドの共有」でみたように、デフォルトのprototype
に対してプロパティ/メソッドの追加/削除を行うようにしたほうがよいでしょう。
古典的な継承の構文
以下は JavaScript の古典的な継承の方法です。
let Person = function(personName) {
this.personName = personName;
this.speakable = true;
};
Person.prototype.speak = function() {
if (this.speakable) {
console.log('Hi, I am ' + this.personName);
}
};
let Passenger = function(personName, nationality) {
Person.call(this, personName);
this.nationality = nationality;
};
Object.setPrototypeOf(Passenger.prototype, Person.prototype);
//もしくは、setPrototypeOfメソッドを使う代わりに以下のようにも書ける
//Passenger.prototype = Object.create(Person.prototype);
//Passenger.prototype.constructor = Passenger;
Passenger.prototype.speak = function() {
if (this.speakable) {
console.log('Hi, I am ' + this.personName + ' from ' + this.nationality);
}
};
let passenger1 = new Passenger('Takumi', 'Japan');
passenger1.speak()
//Hi, I am Takumi from Japan
Passenger
はPerson
を継承しています。Passenger
コンストラクター内では、まずスーパークラスのコンストラクターを呼び出します。Person.call(this, name)
でPerson
コンストラクターを実行時のthis
で呼び出します。引数name
も同時に渡します。
そして、Object.setPrototypeOf(Passenger.prototype, Person.prototype);
で、Passenger.prototype
の [[Prototype]] にPerson.prototype
をセットします。
プロトタイプをセットするもう一つの方法は、(コメントアウトしている部分ですが、)
Object.create(Person.prototype)
で、Person.prototype
をプロトタイプに持つオブジェクトを生成して、Passenger.prototype
にセットする方法です。この方法では、Passenger.prototype.constructor
が、親のPerson.prototype.constructor
(つまりPerson
)になってしまいますので、それを避けるために Passenger.prototype.constructor
に Passenger
(子のコンストラクター) を再設定します。
プロトタイプチェーンを確認してみます。
Object.getPrototypeOf(passenger1) === Passenger.prototype
//true
Object.getPrototypeOf(Passenger.prototype) === Person.prototype
//true
Object.getPrototypeOf(Person.prototype) === Object.prototype
//true
Object.getPrototypeOf(Object.prototype) === null
//true
この場合は、以下のようなプロトタイプチェーンが構築されています。
passenger1 ---> Passenger.prototype ---> Person.prototype ---> Object.prototype ---> null
プロパティを確認してみます。
Object.getOwnPropertyNames(passenger1)
//["personName", "speakable", "nationality"] (3)
Object.getOwnPropertyNames(Passenger.prototype)
//["constructor", "speak"] (2)
Object.getOwnPropertyNames(Person.prototype)
//["constructor", "speak"] (2)
Object.getOwnPropertyNames(Passenger)
//["length", "prototype", "name"] (3)
Object.getOwnPropertyNames(Person)
//["length", "prototype", "name"] (3)
passenger1
インスタンスオブジェクトには、["personName", "speakable", "nationality"] の3つのプロパティがあります。
passenger1
の [[Prototype]] のPassenger.prototype
には、["constructor", "speak"] の2つのプロパティがあり、speak
メソッドはオーバーライドしたメソッドです。
Passenger.prototype
の [[Prototype]] のPerson.prototype
にも["constructor", "speak"] の2つのプロパティがあり、speak
メソッドはオリジナルのメソッドです。
Passenger
コンストラクターと Person
コンストラクターには、Function オブジェクトに特有のプロパティがあるのが確認できます。
クラス構文
JavaScript の class 構文は、(Wikipedia的に言うと)これまでみてきたように複雑で分かりにくい書き方であるコンストラクタ関数やプロトタイプによる継承など、JavaScript 特有のオブジェクト指向の構文と全く同じ意味になるものを、よりシンプルでわかりやすい書き方で書くことができるようにしたもの、と言うことになります。
上記「古典的な継承の構文」場合と同じクラスを定義してみます。
class Person {
constructor (personName) {
this.personName = personName;
this.speakable = true;
}
speak() {
if (this.speakable) {
console.log('Hi, I am ' + this.personName);
}
}
};
class Passenger extends Person {
constructor (personName, nationality) {
super(personName);
this.nationality = nationality;
}
speak() {
if (this.speakable) {
console.log('Hi, I am ' + this.personName + ' from ' + this.nationality);
}
}
};
var passenger1 = new Passenger('Takumi', 'Japan');
passenger1.speak()
//Hi, I am Takumi from Japan
他の言語の class 構文と同様の書き方になっているので、他言語の習得者には馴染みやすい構文となっていますね。それぞれの class 内に必要なものが収まっているので、わかりやすいと言えます。
この class 構文でも、これまでみてきたことと全く同じことが実行されています。クラス宣言は、実際にはコンストラクター関数Person
, Passenger
を宣言します。constractor
キーワードはそれぞれPerson
, Passenger
コンストラクターの関数本体を宣言します。そして、各speak
メソッドはそれぞれPerson.prototype
, Passenger.prototype
に追加され、Passenger
側でオーバーライドされます。
オブジェクトを生成するには、new 演算子でコンストラクター関数を呼び出します。
プロトタイプチェーンを確認してみます。
Object.getPrototypeOf(passenger1) === Passenger.prototype
//true
Object.getPrototypeOf(Passenger.prototype) === Person.prototype
//true
Object.getPrototypeOf(Person.prototype) === Object.prototype
//true
Object.getPrototypeOf(Object.prototype) === null
//true
この場合も、古典的な継承の時と同じように以下のようなプロトタイプチェーンが構築されています。
passenger1 ---> Passenger.prototype ---> Person.prototype ---> Object.prototype ---> null
PersonクラスとPassengerクラスの型を確認します。
typeof(Person)
//"function"
typeof(Passenger)
//"function"
このように、 class 構文で定義していても、Person
とPassenger
はFunctionオブジェクトです。
また、 class 構文でも、「古典的な継承の構文」と同様のプロパティを確認することができます。
Object.getOwnPropertyNames(passenger1)
//["personName", "speakable", "nationality"] (3)
Object.getOwnPropertyNames(Passenger.prototype)
//["constructor", "speak"] (2)
Object.getOwnPropertyNames(Person.prototype)
//["constructor", "speak"] (2)
Object.getOwnPropertyNames(Passenger)
//["length", "prototype", "name"] (3)
Object.getOwnPropertyNames(Person)
//["length", "prototype", "name"] (3)
このように、確かに class 構文は従来と比較してシンプルで分かり易く書きやすい構文で、しかも裏側で必要な処理を自動的に実行してくれます。ミスも減ると思われるので、今後は class 構文を使用すべきだと思います。
ただし、JavaScript のオブジェクト指向の挙動がなぜそうなるかを理解して、間違いのない実装をするには、実際には古典的な構文で書いているような仕組みで動いていることは、理解しておくことが大切だと思います。
プロトタイプ関連の表記方法等の整理
最後に、プロトタイプ関連の表記方法を整理しておきます。
1.オブジェクトのプロトタイプの隠しプロパティ obj.[[Prototype]]
JavaScript では、オブジェクトは特別な隠しプロパティ [[Prototype]] を持っており、それは別のオブジェクト(もしくは null )を参照します。その別のオブジェクトが当該オブジェクトのプロトタイプなのですが、デフォルトではコンストラクタ関数のprototype
プロパティを参照しています。
JavaScript での「継承」は、この [[Prototype]] プロパティを利用して実現されています。
ただし、スクリプト内でobj.[[Prototype]]
としてもエラーになり参照したり変更したりすることはできません。
Chrome や Edge の開発者コンソールでは、コンソールのログでオブジェクトのプロパティの一部として、 [[Prototype]] プロパティが他のプロパティと同様に表示されます。
obj.__proto__
2.オブジェクトプロトタイプのアクセサ(非推奨)obj.__proto__
は、オブジェクトの隠しプロパティ [[Prototype]] を読み書きできるアクセサです。現在多くのブラウザでサポートされてはいますが、非推奨になっていますので、いつサポートされなくなってもおかしくありません。コンソールで確認したりするのに使うのには便利ですが、プログラム内での使用は控えた方がよいでしょう。
Object.getPrototypeOf
メソッドとObject.setPrototypeOf
メソッド
3.オブジェクトのプロトタイプを取得するのがObject.getPrototypeOf
メソッドで、設定するのがObject.setPrototypeOf
メソッドです。今後は、非推奨となったobj.__proto__
アクセサーの代わりにこれらのメソッドを使用します。以下のように使います。
let obj = {a: 1}
let someObj = {b: 2}
Object.getPrototypeOf(obj) //objのプロトタイプを取得
//{}
Object.setPrototypeOf(obj, someObj) //objのプロトタイプをsomeObjに変更
Object.getPrototypeOf(obj)
//{b: 2}
{ __proto__: ... }
4.オブジェクト初期化子 オブジェクトのリテラル表記の際に、初期化子としてプロトタイプ [[Prototype]] を指定する際に __proto__:
を使用します。(上記 obj.__proto__
アクセサーとは違い、非推奨ではありません。)
const obj = {
a: 1,
b: 2,
//この場合プロトタイプをリテラルで指定しています。
__proto__: {
b: 3,
c: 4,
},
};
obj.a
//1
//objのプロトタイプチェーンを確認します。
//まず、objのプロトタイプを取得します。
Object.getPrototypeOf(obj)
//{b: 3, c: 4}
//直接{b: 3, c: 4}を操作できないので、間接的に取得します。
Object.getPrototypeOf(Object.getPrototypeOf(obj)) === Object.prototype
//true
Object.getPrototypeOf(Object.prototype) === null
//true
上記の場合、いわゆる obj.[[Prototype]] は {b: 3, c: 4}
です。obj.[[Prototype]].[[Prototype]] は Object.prototype
です。そして、 obj.[[Prototype]].[[Prototype]].[[Prototype]] は null
で、プロトタイプチェーンは終了です。したがって、この場合プロトタイプチェーンは次のようになります。
{ a: 1, b: 2 } ---> { b: 3, c: 4 } ---> Object.prototype ---> null
この場合に、2番目のプロトタイプ{b: 3, c: 4}
はコンストラクター関数ではないオブジェクトですので、prototype
プロパティはありません。
以下のような継承のパターンも書けます。
let animal = {
eats: true,
walk() {
console.log("Animal can walk")
}
}
let human = {
speak: true,
__proto__: animal
}
let japanese = {
lunguage: 'Japanese',
__proto__: human
}
japanese.walk()
//Animal can walk
console.log(japanese.speak)
//true
walk
やspeak
は プロトタイプチェーンから取得されました。
ただし、aminal
human
japanese
はコンストラクター関数ではないので、prototype
プロパティは持ちません。
Object.getPrototypeOf(japanese) === human
//true
Object.getPrototypeOf(human) === animal
//true
Object.getPrototypeOf(animal) === Object.prototype
//true
Object.getPrototypeOf(Object.prototype) === null
//true
この場合プロトタイプチェーンは次のようになります。
japanese ---> human ---> animal ---> Object.prototype ---> null
Function.prototype
5.関数オブジェクトのプロトタイププロパティ すべての関数は、prototype
プロパティを持っていますが、これはオブジェクトの [[Prototype]] ではありません。関数のデフォルトのprototype
はconstructor
というプロパティだけを持つオブジェクトで、それは関数自体を指します。関数のprototype
プロパティは、直接参照することができます。
コンストラクタ関数と new 演算子でインスタンスが作成されたときに、生成されたインスタンスの隠しプロパティ [[prototype]] に、コンストラクタ関数のprototype
プロパティのオブジェクトの参照がセットされます。デフォルトのままであれば、コンストラクタ関数自体が設定されます。
Object
もコンストラクタ関数なので、Object.prototype.constructor
はObject
自身です。
参考文献
Web sites:
Books:
・改訂新版JavaScript本格入門 〜モダンスタイルによる基礎から現場での応用まで 山田祥寛 技術評論社
・改訂3版JavaScript本格入門 〜モダンスタイルによる基礎から現場での応用まで 山田祥寛 技術評論社
・JavaScriptモダンプログラミング完全ガイド 堅牢なコードを効率的に開発できる! Cay S Horstman インプレス
・JavaScript 第7版 David Flanagan オライリージャパン
Discussion