本記事のポイント

本ブログはIoT-CoreエンジンNEQTOの特徴の一つであるJavaScriptによるNEQTO Engine対応デバイスの開発にフォーカスし、JavaScriptの基礎およびneqto.js特有の拡張機能について連載形式で紹介していきます。

この連載記事では、JavaScriptの基礎となる構文や文法について順番に解説していきます。第5回目の今回は「クラス」について解説します。NEQTOユーザー様がJavaScript開発を円滑に進めるために活用頂くこと、また、NEQTOに興味をお持ちの方がNEQTOを使用したJavaScript開発について理解を深めて頂くことが本ブログの目的です。



1. はじめに

オブジェクト指向言語のJavaScriptで効率的なプログラミングを行う際、ある「型枠」を作り、そこから「実体」を生成する手法が用いられることが多いと思います。通常、この「型枠」を「クラス」、そして「実体」を「インスタンス」と表現します。このブログでは、クラスの構造を説明した後、最終的にはクラスの継承がもっと身近なものになるように解説していきたいと思います。

ところで、学術的には、上記説明は「クラスベースのオブジェクト指向プログラミング言語」に当てはまります。一方、JavaScriptは「プロトタイプベースのオブジェクト指向プログラミング言語」に分類されます。「プロトタイプベース」においては、「型枠(クラス)」を「原型(プロトタイプ)」と呼ぶのが正確かもしれません。しかしながら、「プロトタイプベース」であっても、クラス同様の機能を実現できること、また、本ブログの趣旨は実践的な理解となりますので、タイトルはあえて「クラス」を用いることにします。

もちろん、「プロトタイプベース」である以上、「クラス」の機能を支える仕組みとして「プロトタイプ」は存在しますので、本ブログでも何度も取り上げることになります。ちなみに、ECMAScript 2015以降のJavaScriptでは「クラス構文」自体が存在しますが、これを支えているのは「プロトタイプベース」です。

それでは、実際に「neqto.js」を使用して詳しく解説していきます。「neqto.js」は、NEQTO Engine上で動作するJavaScript環境です。ECMAScript 5.1 Editionに対応しています。

2. クラスの定義

クラスの定義は関数そのものです。これは、クラスの「インスタンス」を形作る「コンストラクタ」として機能します。コンストラクタは、インスタンスを生成し、プロパティの追加、および次の重要な機能があります。

インスタンスにはその原型(プロトタイプ)へのリンクが設定されています。リンク先もまたインスタンスであり、これら一連のリンクがいわば鎖のように繋がっています。この概念が「プロトタイプチェーン」です。そして、コンストラクタにはインスタンスにプロトタイプチェーンを設定する役割があります。

以後、このコンストラクタの関数名が、クラスの名称です。また、クラスの継承について、継承元となるクラスを「スーパークラス」、継承先となるクラスを「サブクラス」として説明します。


コンストラクタ


以下は、クラス(Animal)のコンストラクタです。この中で使用されるthisは後述するnew演算子付きで実行した際、暗黙的に生成されるオブジェクト(インスタンス)を指します。

コンストラクタ内に記述されたthis付きプロパティは、生成されるインスタンスのプロパティとなります。インスタンス毎に存在する固有情報を持つ場合に適しています。一方、コンストラクタに直接追加したプロパティは、「クラスプロパティ」と呼ばれ、インスタンスに依存しない個別情報を付加したい場合に適しています。

print('--- Super Class ---');

var Animal = function(type) {
    if(type)
        this.type = type;
    else
        this.type = Animal.DEF_TYPE;
};

Animal.DEF_TYPE = 'lion';

クラスメソッド(静的メソッド)


クラスプロパティでも触れましたが、Date.now()などのようにインスタンスに依存しないメソッドを必要に応じて追加することができます。

ここでは、コンストラクタの引数を省略したときに設定される値(Animal.DEF_TYPE)を確認するためのクラスメソッドを用意しました。

Animal.getDefaultType = function() {
    return Animal.DEF_TYPE;
};

print(Animal.getDefaultType()); //lion

メソッド(インスタンスメソッド)

インスタンス化した後に使用可能となるメソッドを追加します。単に「メソッド」と呼ばれる場合は、通常こちらを指します。

ここでは、単純にプロパティ(type)を読み出すメソッドを用意しました。

Animal.prototype.getType = function() {
    return this.type;
};

以上で、クラスの完成です。

3. クラスの継承

当然ですが、同じものは再利用した方が効率が良く、保守性も優れています。クラスの継承はまさに再利用です。

ここからは、クラスの継承について実践的に確認していきます。以下二つのアプローチで、微妙なニュアンスの違いにあえてこだわって、「継承」を少しだけ深掘りしながら説明していきます。


Animalクラス(スーパークラス)を継承したDogクラス(サブクラス)の作成


サブクラスのコンストラクタを用意します。
コンストラクタの中には、必要に応じてDogクラス独自の新規プロパティを追加しておきます。

Animalクラスをnew演算子で生成したインスタンスをDog.prototypeプロパティ上に配置することで、Animalクラスのtypeプロパティは 「dog」固定 になります。

{Function}.prototypeとは、インスタンスにプロトタイプチェーンを設定するためのプロパティです。

あえてコンストラクタに含めなかったtypeプロパティは、プロトタイプチェーン上(Dog.prototype.type)からアクセスできることがわかります。

print('--- Sub Class 1 ---');

var Dog = function(name, breed) { 
    this.name = name; 
    this.breed = breed; 
};

Dog.prototype = new Animal('dog');
print(Dog.prototype instanceof Animal); //true
print(Dog.prototype.constructor === Animal); //true

print(Dog.prototype.type); //dog

Dog.prototype.constructor = Dog;
print(Dog.prototype.constructor === Dog); //true

Animalクラス(スーパークラス)を継承したPetクラス(サブクラス)の作成


サブクラスのコンストラクタを用意します。
ここでは、{Function}.callメソッドを使用して、スーパークラスのコンストラクタを再利用するようにします。
コンストラクタの中には、必要に応じてPetクラス独自の新規プロパティを追加しておきます。

Object.createAnimal.prototypeを引数として生成したインスタンスをPet.prototypeプロパティ上に配置することで、Animalクラスのtypeプロパティは 可変になります。指定がない場合は「dog」を使用することにします。

Object.createnew演算子と異なり、インスタンス生成時にコンストラクタを実行しないという特徴があります。

①との重要な違いは、Pet.prototype.typeからアクセスできないことです。typeプロパティは、プロトタイプチェーン上(Dog.prototype.type)には存在せず、後述するインスタンス化を経てアクセス可能です。

print('--- Sub Class 2 ---');

var Pet = function(name, type) {
    if(type)
        Animal.call(this, type);
    else
        Animal.call(this, 'dog');

    this.name = name; 
}

Pet.prototype = Object.create(Animal.prototype);
print(Pet.prototype instanceof Animal); //true
print(Pet.prototype.constructor === Animal); //true

print(Pet.prototype.type); //undefined

Pet.prototype.constructor = Pet;
print(Pet.prototype.constructor === Pet); //true

オーバーライド


「オーバーライド」とは継承元の動作を継承先で変更することです。主にメソッドで使用されますが、プロパティでも同じことが言えます。

ここでは、プロトタイプチェーン上でのオーバーライドを確認するため、第三のクラスとしてCatクラスを作成してみます。Dogクラスと同様に、Animalクラスを継承することも可能ですが、あえてDogクラスを継承してみます。

print('--- Sub Class 3 ---');

var Cat = function(name, breed) {
    Dog.call(this, name, breed);
};

Cat.prototype = Object.create(Dog.prototype);
Cat.prototype.constructor = Cat;

typeプロパティが「dog」のままです。プロトタイプチェーンをつたって「dog」にたどり着いていることがわかります。

print(Cat.prototype.type); //dog
print(Object.getPrototypeOf(Cat.prototype).type); //dog
print(Cat.prototype.type === Object.getPrototypeOf(Cat.prototype).type); //true

typeプロパティを「cat」に変更する必要があるので、オーバーライドを使用します。

Cat.prototype.type = 'cat';

typeプロパティは、Dogクラスとは異なるものとなりました。また、この時点でDogクラスのtypeプロパティは「dog」のまま影響を受けないことがわかります。

print(Cat.prototype.type); //cat
print(Object.getPrototypeOf(Cat.prototype).type); //dog
print(Cat.prototype.type === Object.getPrototypeOf(Cat.prototype).type); //false

ここでは、オーバーライドをプロパティで試しましたが、メソッドでも同様です。

4. クラスのインスタンス化

new演算子


関数をコンストラクタとして実行します。つまり、新しいインスタンスを生成します。
このnewを付け忘れると想定外の動作を引き起こす可能性がありますので注意が必要です。

まず、スーパークラスであるAnimalクラスのインスタンス化を試してみます。

var obj = new Animal();

print(obj.getType()); //lion
print(obj.type);      //lion

次は、二つのサブクラスになります。


Dogクラス

var dog = new Dog('hachi', 'akita');

print(dog.getType()); //dog
print(dog.type);      //dog
print(dog.name + ' ' + dog.breed); //hachi akita

Petクラス

var pet = new Pet('miko', 'cat');

print(pet.getType()); //cat
print(pet.type);      //cat
print(pet.name);      //miko

内部プロパティ[[Prototype]]


インスタンスには、内部プロパティとして[[Prototype]]が存在します。コンストラクタのprototypeプロパティと同じ参照がインスタンス生成時に設定されます。これら二つの「プロトタイプ」の違いを明確に理解することは「継承」を理解する上で重要です。

Object.getPrototypeOf({instance})は、プロトタイプチェーンを構成する内部プロパティ[[Prototype]]を取得するメソッドです。つまり、インスタンスからその内部プロパティである[[Prototype]]へのアクセスは特別であることを意味します。

  • Notes:
  • 同様の意味を持ち簡易にアクセス可能な非推奨プロパティである{instance}.__proto__はneqto.jsでは非サポートです。
  • [[Prototype]]は、ECMAScript仕様書の表記に従っています。

それでは、DogクラスおよびPetクラスを使用して確認してみます。


Dogクラス


[[Prototype]]に直接触れて見ると、typeプロパティがプロトタイプチェーン上に存在していることが改めてわかります。また、nameプロパティはインスタンス自身のプロパティであることも確認できます。

print(Object.getPrototypeOf(dog).type); //dog
print(Object.getPrototypeOf(dog).name); //undefined
print(Object.getPrototypeOf(dog) === Dog.prototype); //true

上記において、{Function}.prototypeおよび、このインスタンスの[[Prototype]]が同じであることを確認しました。
次に、インスタンス生成後でも{Function}.prototypeの変更を行えば、全てのインスタンスが変更されることを確認してみます。

print(dog.color); //undefined
Dog.prototype.color = 'white';
print(dog.color); //white

上記は、{Function}.prototype配下のプロパティを追加/変更した場合ですが、下記のように{Function}.prototypeそのものを再設定することは混乱の元となりますので避けるべきです。

Dog.prototype = ...;

Petクラス


①とは異なり、typeプロパティがnameプロパティと同じであり、プロトタイプチェーン上に存在していないことがわかります。

print(Object.getPrototypeOf(pet).type); //undefined
print(Object.getPrototypeOf(pet).name); //undefined
print(Object.getPrototypeOf(pet) === Pet.prototype); //true

インスタンス生成後の{Function}.prototype配下のプロパティ変更については、Dogクラスと同様の挙動です。

print(pet.color); //undefined
Pet.prototype.color = 'white';
print(pet.color); //white

5. サンプルコード

これまで紹介してきたサンプルコードをまとめて掲載します。
これらのコードはすべてを通して一連動作するようになっています。

print('--- Super Class ---');

//constructor
var Animal = function(type) {
    if(type)
        this.type = type;
    else
        this.type = Animal.DEF_TYPE;
};

//class property/method
Animal.DEF_TYPE = 'lion';
Animal.getDefaultType = function() {
    return Animal.DEF_TYPE;
};

print(Animal.getDefaultType()); //lion

//instance method
Animal.prototype.getType = function() {
    return this.type;
};
print('--- Sub Class 1 ---');

//constructor
var Dog = function(name, breed) {
    this.name = name;
    this.breed = breed;
};

//prototype property
Dog.prototype = new Animal('dog');
print(Dog.prototype instanceof Animal); //true
print(Dog.prototype.constructor === Animal); //true

print(Dog.prototype.type); //dog

Dog.prototype.constructor = Dog;
print(Dog.prototype.constructor === Dog); //true
print('--- Sub Class 2 ---');

//constructor
var Pet = function(name, type) {
    if(type)
        Animal.call(this, type);
    else
        Animal.call(this, 'dog');

    this.name = name; 
}

//prototype property
Pet.prototype = Object.create(Animal.prototype);
print(Pet.prototype instanceof Animal); //true
print(Pet.prototype.constructor === Animal); //true

print(Pet.prototype.type); //undefined

Pet.prototype.constructor = Pet;
print(Pet.prototype.constructor === Pet); //true
print('--- Sub Class 3 ---');

var Cat = function(name, breed) {
    Dog.call(this, name, breed);
};

Cat.prototype = Object.create(Dog.prototype);
Cat.prototype.constructor = Cat;

print(Cat.prototype.type); //dog
print(Object.getPrototypeOf(Cat.prototype).type); //dog
print(Cat.prototype.type === Object.getPrototypeOf(Cat.prototype).type); //true

//override
Cat.prototype.type = 'cat';

print(Cat.prototype.type); //cat
print(Object.getPrototypeOf(Cat.prototype).type); //dog
print(Cat.prototype.type === Object.getPrototypeOf(Cat.prototype).type); //false
print('--- Instantiation ---');

//super class instance
var obj = new Animal();

print(obj.getType()); //lion
print(obj.type);      //lion

//(1) instance
var dog = new Dog('hachi', 'akita');

print(dog.getType()); //dog
print(dog.type);      //dog
print(dog.name + ' ' + dog.breed); //hachi akita

//(2) instance
var pet = new Pet('miko', 'cat');

print(pet.getType()); //cat
print(pet.type);      //cat
print(pet.name);      //miko
print('--- Internal Property ---');

//(1) prototype chain
print(Object.getPrototypeOf(dog).type); //dog
print(Object.getPrototypeOf(dog).name); //undefined
print(Object.getPrototypeOf(dog) === Dog.prototype); //true

//(1) add to prototype property
print(dog.color); //undefined
Dog.prototype.color = 'white';
print(dog.color); //white

//(2) prototype chain
print(Object.getPrototypeOf(pet).type); //undefined
print(Object.getPrototypeOf(pet).name); //undefined
print(Object.getPrototypeOf(pet) === Pet.prototype); //true

//(2) add to prototype property
print(pet.color); //undefined
Pet.prototype.color = 'white';
print(pet.color); //white

6. まとめ

いかがでしたか。「クラス」とりわけ「継承」を身近なものにすることが本ブログの目標でした。neqto.jsは柔軟であり、様々なコーディング手法を用いることができます。今回紹介した「クラス」を使いこなし、「継承」を効果的に使えるようになれば、より効率的でかつ保守性の高いプログラムの構築に役立ちます。ぜひご参考にしてください。


リンク


Standard ECMA-262 5.1 Edition ECMAScript® Language Specification - 15 Standard Built-in ECMAScript Objects

NEQTOとは

NEQTOハードウェア

NEQTO技術ドキュメント