LoginSignup
76
72

More than 3 years have passed since last update.

javascript で学ぶオブジェクト指向

Posted at

はじめに

プログラミングの勉強を進めていくとオブジェクト指向の話が出てきます。
なんとなく分かったつもりでいても、オブジェクト指向言語を使えばいいというものではなく、その考え方をちゃんと理解しながら使っていかなければ身につきません。

ここでは例として生活の中で分かりやすい対象としてテレビと新聞を見る動作をjavascriptで表現してみました。またそのソースコードに少しずつオブジェクト指向を取り込んで発展していくように書いています。
皆さんもなにか身の回りのものをスクリプト化して挑戦してみてください。

オブジェクト指向とは

ソフトウェアの設計や開発において、操作手順よりも操作対象に重点を置く考え方をオブジェクト指向といいます。すでに存在するオブジェクト(データと手続き)については、利用に際してその内部構造や動作原理の詳細を知る必要はなく、外部から指示を送れば機能するため、特に大規模なソフトウェア開発において有効な考え方であるとされています。

例えば、テレビを操作する際には、テレビ内部でどのような回路が働いているかを理解する必要はありません。ただテレビの操作方法だけを知っていれば、それでテレビを使うことができます。すなわち、テレビという「オブジェクト」は電源をいれれば電波を拾って映像が表示されるものだということを知っており、それを利用するためには(例えばリモコンで)適切な指示を与えるだけでよいわけです。


ネームスペース (名前空間)

手続き型の問題

プログラムを構築する際に手続きに沿って処理を書いた方が初心者のうちは理解しやすく書きやすいでしょう。しかし、例えばテレビを閲覧する処理をプログラムで書き出した場合、ザッピングで次々にチャンネルを変える処理を書こうとすると同じ処理を何度も書くことになります。

コピペでいくつも複製していけばいいと思う人もいるかもしれませんが、行数が増えるとだんだんソースコードが読みにくくなり、間違いも起きやすくなります。

手続き型(ベタ打ち)の例

// デフォルト設定
const newspaper_name = "毎朝新聞";
let newspaper_page = "表紙";
const television_name = "リビングのテレビ";
let television_power = "off";
let television_channel = 1;

// 実行
newspaper_page = "テレビ欄";
console.log( newspaper_name + "" + newspaper_page + "ページを開きました。" ); // 毎朝新聞のテレビ欄ページを開きました。
console.log( newspaper_name + "" + newspaper_page + "ページを読みます。" );   // 毎朝新聞のテレビ欄ページを読みます。
if( television_power != "on" )
{
    television_power = "on";
    console.log( television_name + "を点けました。" );  // リビングのテレビを点けました。
}
television_channel = 10;
console.log( television_name + "のチャンネルを" + television_channel + "に変えました。" );  // リビングのテレビのチャンネルを10に変えました。
console.log( television_name + "" + television_channel + "チャンネルを観ます。" );        // リビングのテレビで10チャンネルを観ます。
television_channel = 4;
console.log( television_name + "のチャンネルを" + television_channel + "に変えました。" );  // リビングのテレビのチャンネルを4に変えました。
console.log( television_name + "" + television_channel + "チャンネルを観ます。" );        // リビングのテレビで4チャンネルを観ます。
television_channel = 1;
console.log( television_name + "のチャンネルを" + television_channel + "に変えました。" );  // リビングのテレビのチャンネルを1に変えました。
console.log( television_name + "" + television_channel + "チャンネルを観ます。" );        // リビングのテレビで1チャンネルを観ます。

そこで、処理を関数化すると実行部分が1行につき1処理になって処理がわかりやすくなります。

手続き型(処理を関数化したもの)

// デフォルト設定
const newspaper_name = "毎朝新聞";
let newspaper_page = "表紙";
const television_name = "リビングのテレビ";
let television_power = "off";
let television_channel = 1;

// 関数
function changeNewspaperPage(new_page)
{
    newspaper_page = new_page;
    console.log( newspaper_name + "" + newspaper_page + "ページを開きました。" );
}
function readNewspaperPage()
{
    console.log( newspaper_name + "" + newspaper_page + "ページを読みます。" );
}
function onTelevisionPower()
{
    if( television_power == "on" ) return;
    television_power = "on";
    console.log( television_name + "を点けました。" );
}
function changeTelevisionChannel(new_channel)
{
    television_channel = new_channel;
    console.log( television_name + "のチャンネルを" + television_channel + "に変えました。" );
}
function watchTelevisionChannel()
{
    console.log( television_name + "" + television_channel + "チャンネルを観ます。" );
}

// 実行
changeNewspaperPage( "テレビ欄" );  // 毎朝新聞のテレビ欄ページを開きました。
readNewspaperPage();                // 毎朝新聞のテレビ欄ページを読みます。
onTelevisionPower();                // リビングのテレビを点けました。
changeTelevisionChannel( 10 );      // リビングのテレビのチャンネルを10に変えました。
watchTelevisionChannel();           // リビングのテレビで10チャンネルを観ます。
changeTelevisionChannel( 4 );       // リビングのテレビのチャンネルを4に変えました。
watchTelevisionChannel();           // リビングのテレビで4チャンネルを観ます。
changeTelevisionChannel( 1 );       // リビングのテレビのチャンネルを1に変えました。
watchTelevisionChannel();           // リビングのテレビで1チャンネルを観ます。

「実行」以下がスッキリしました。

しかし、今度は関数が多くなってしまってその処理を探すのに苦労します。変数名や関数名が重複しないように気を配る必要があるので変数名や関数名は長くなりがちになり、関数内の処理の1行1行が横に長くなってしまいます。

名前空間

そこで、この長い変数名や関数名をオブジェクト(もの)別にグルーピングします。オブジェクト内の変数や関数は this. で表せられるため、メソッド内の処理がシンプルになります。
このように変数名や関数名の名前の衝突を防ぐためにグループ化することを名前空間を使うといいます。

プロパティとメソッド

変数はオブジェクト内では属性(プロパティ)、関数はオブジェクト内ではメソッドといいます。

名前空間を使用した例

// 新聞
const Newspaper =
{
    // プロパティ
    name : "毎朝新聞",
    page : "表紙",
    // メソッド
    changePage : function(new_page)
    {
        this.page = new_page;
        console.log( this.name + "" + this.page + "ページを開きました。" );
    },
    readPage : function()
    {
        console.log( this.name + "" + this.page + "ページを読みます。" );
    }
}
// テレビ
const Television =
{
    // プロパティ
    name : "リビングのテレビ",
    power : "off",
    channel : 1,
    // メソッド
    onPower : function()
    {
        if( this.power == "on" ) return;
        this.power = "on";
        console.log( this.name + "を点けました。" );
    },
    changeChannel : function(new_channel)
    {
        this.channel = new_channel;
        console.log( this.name + "のチャンネルを" + this.channel + "に変えました。" );
    },
    watchChannel : function()
    {
        console.log( this.name + "" + this.channel + "チャンネルを観ます。" );
    }
}

// 実行
Newspaper.changePage( "テレビ欄" ); // 毎朝新聞のテレビ欄ページを開きました。
Newspaper.readPage();               // 毎朝新聞のテレビ欄ページを読みます。
Television.onPower();               // リビングのテレビを点けました。
Television.changeChannel( 10 );     // リビングのテレビのチャンネルを10に変えました。
Television.watchChannel();          // リビングのテレビの10チャンネルを観ます。
Television.changeChannel( 4 );      // リビングのテレビのチャンネルを4に変えました。
Television.watchChannel();          // リビングのテレビの4チャンネルを観ます。
Television.changeChannel( 1 );      // リビングのテレビのチャンネルを1に変えました。
Television.watchChannel();          // リビングのテレビの1チャンネルを観ます。

名前空間のメリット

名前空間を使用しないと以下の問題が起きます。

  • 全てがグローバル変数になってしまう。グローバルスコープが汚染される。
  • 変数や関数が衝突しやすい。
  • 衝突を避けるために変数や関数名が長くなりがち。

名前空間を使うとスッキリ整理でき、以下の利点が生まれます。

  • グローバルスコープが汚染されにくくなる。
  • 名前空間内では衝突を気にしなくていい。
  • ものや意味などでまとめられ、作りやすくなる。
  • ソースコードが読みやすくなる。
  • 開発を分担しやすくなる。

カプセル化

カプセル化とは

テレビの電源を入れたりチャンネルを変えたりするとき、普通はリモコンで操作します。その時、テレビの中で何が起きているか、どういう構造で動いているかは知らなくても、テレビを操れます。たとえテレビの構造を知っているエンジニアだったとしてもリモコンを使います。わざわざテレビを分解して電源を入れる必要はなく、そんなことをしたら逆に誤作動を起こしたり、テレビが壊れたりするでしょう。

プログラムも同じように、中の複雑な処理は隠蔽し、インターフェースだけを提供することでシンプルで分かりやすく便利になります。これをカプセル化といいます。

オブジェクト指向はこれをオブジェクト単位で行います。

カプセル化されていないと

カプセル化をしないと以下の問題が起きます。

  • プロパティやメソッドを外部から好きなように変更できてしまう。想定外の変更を許してしまう。
  • バグが見つかった場合に、想定外の処理をしている箇所がオブジェクトの外側にある可能性があるため、改修に時間がかかってしまう。
  • オブジェクトの中身を考慮してプログラムを組む必要があり、開発に時間がかかる。

下記は名前空間でまとめただけのものですが、プロパティもメソッドもグローバルスコープで全てアクセスできてしまうため間違った使い方をしてしまう可能性があります。

※Television は const 宣言していますが、そのプロパティは可変になります。

// テレビ
const Television =
{
    // プロパティ
    name : "リビングのテレビ",
    power : "off",
    channel : 1,
    // メソッド
    onPower : function()
    {
        if( this.power == "on" ) return;
        this.power = "on";
        console.log( this.name + "を点けました。" );
    },
    changeChannel : function(new_channel)
    {
        this.channel = new_channel;
        console.log( this.name + "のチャンネルを" + this.channel + "に変えました。" );
    },
    watchChannel : function()
    {
        console.log( this.name + "" + this.channel + "チャンネルを観ます。" );
    }
}

// 実行
Television.power = "on";    // 簡単に書き換えられてしまう。出力はされない。
Television.channel = -10;   // -10という想定していないチャンネルを設定できてしまう。
Television.watchChannel();  // 出力: リビングのテレビの-10チャンネルを観ます。

スコープ

変数が利用できる範囲のことをスコープといいます。

javascriptは関数内で宣言された変数は関数の内側でしか使えません。関数の中にさらに関数がある場合、内側の関数は外側の関数内にある変数は利用できますが、外側からは内側で宣言される変数は利用できません。

クロージャ

通常は関数の中に書かれたプロパティは引数で値を渡して中で書き換えでもしない限り変更されません。引数で変更したとしても関数内のプロパティは関数の処理が終了したら開放され、初期化されてしまいます。

しかし、関数を変数として格納してしまえば、その関数が使えるスコープ内の変数も一緒に保持するので、オブジェクトの状態(プロパティ)を保持することができます。この関数とその関数の環境そのものをセットで記憶し、アクセスできる関数のことをクロージャといいます。

このjavascriptのスコープとクロージャの性質を利用してプライベートなプロパティ/メソッドを定義することができます。

以下はクロージャを使ってカプセル化した例です。

クロージャを使ってカプセル化した例

// テレビ
function Television()
{
    // プロパティ
    const name = "リビングのテレビ";
    let power = "off";
    let channel = 1;
    // メソッド
    this.onPower = function()
    {
        if( power == "on" ) return;
        power = "on";
        console.log( name + "を点けました。" );
    }
    this.changeChannel = function(new_channel)
    {
        // 新しいチャンネルを代入する前に値が正しいかチェック
        new_channel = parseInt(new_channel);
        if ( isNaN(new_channel) ) return;               // チャンネルは数字意外は無視。
        if( new_channel < 1 || new_channel > 100 ) return;  // チャンネルは最低1、最高100。それ以外は無視。
        channel = new_channel;
        console.log( name + "のチャンネルを" + channel + "に変えました。" );
    }
    this.watchChannel = function()
    {
        console.log( name + "" + channel + "チャンネルを観ます。" );
    }
}

// インスタンス生成
const LivingTelevision = new Television();

// 実行
LivingTelevision.onPower();                 // 出力: リビングのテレビを点けました。
LivingTelevision.channel = -10;             // プライベートなプロパティとは別のプロパティにアクセスしている。
LivingTelevision.changeChannel( -10 );      // 誤ったチャンネル指定は無視される。
LivingTelevision.watchChannel();            // 出力: リビングのテレビの1チャンネルを観ます。
console.log(LivingTelevision.channel);      // -10 と出力される。

オブジェクト形式ではなく関数形式にすることで、プロパティを const 宣言できます。関数内部で変数を宣言しているので、関数の外からは変数を操作できません。プロパティをメソッド経由でしか設定できないようにすれば、値を代入する前に毎回チェックすることができ、想定していない値を設定されることを防げます。

以下は即時関数を使ってカプセル化した例です。即時関数の場合はnewしなくても実行できます。上記の例との違いはオブジェクトをreturnで返しています。Television には即時関数全体ではなくこのオブジェクトが代入されますが、javascriptは内側からは外側の変数にアクセスできるため、このオブジェクトから即時関数内の変数にもアクセス可能です。

ただ、この場合は逆にコンストラクタがないためnewでインスタンスを生成できません。複数のインスタンスを生成したい場合は上記の例を使ってください。

即時関数を使ってカプセル化した例

// テレビ
const Television = (function ()
{
    // プロパティ
    const name = "リビングのテレビ";
    let power = "off";
    let channel = 1;
    // メソッド
    return {
        onPower : function()
        {
            if( power == "on" ) return;
            power = "on";
            console.log( name + "を点けました。" );
        },
        changeChannel : function(new_channel)
        {
            // 新しいチャンネルを代入する前に値が正しいかチェック
            new_channel = parseInt(new_channel);
            if ( isNaN(new_channel) ) return;               // チャンネルは数字意外は無視。
            if( new_channel < 1 || new_channel > 100 ) return;  // チャンネルは最低1、最高100。それ以外は無視。
            channel = new_channel;
            console.log( name + "のチャンネルを" + channel + "に変えました。" );
        },
        watchChannel : function()
        {
            console.log( name + "" + channel + "チャンネルを観ます。" );
        }
    }
}());

// 実行
Television.onPower();               // 出力: リビングのテレビを点けました。
Television.changeChannel( -10 );    // 誤ったチャンネル指定は無視される。
Television.watchChannel();          // 出力: リビングのテレビの1チャンネルを観ます。

カプセル化のメリット

カプセル化をすると以下の利点が生まれます。

  • オブジェクトのデータの処理はオブジェクトに集約され、バグの発生を防ぐ。
  • バグが見つかってもオブジェクトの内側だけ確認すればよくなる場合が多い。
  • 使う側はシンプルでわかりやすくなる。
  • 使う側はデータの処理が減るので組み立てがシンプルになる。
  • 開発を分担しやすくなる。

抽象化

新聞は新聞社によって特徴があり同じネタでも違いがありますが、「新聞」ということでは共通しており朝刊、夕刊といった共通の属性(プロパティ)があります。
テレビもブラウン管テレビ、液晶テレビ、4Kテレビなど形や大きさ、構造に違いがありますが、「テレビ」ということでは共通しており、電源を入れる、チャンネルを変えるという共通の機能や行動(メソッド)があります。
また、テレビと新聞は情報を得るというメソッドは同じであり、情報を得る「媒体(メディア)」というグルーピングができます。

このように、具体的な対象から具体性を排除し、共通の特徴によってグルーピングし、共通の性質を抽出することで、より汎用的な概念に構成していくことを抽象化といいます。

この抽象化を経ることで継承やポリモーフィズム(多様性)が成り立つので抽象化はとても重要です。

継承

抽象化されたオブジェクトのプロパティやメソッドを引き継いで、より具体化された別のオブジェクトとして定義することを継承といいます。

以下はその抽象化、継承を取り入れた例です。

// メディア
function Media(media_name)
{
    // プロパティ
    let name = media_name;
    // メソッド
    this.getName = function()
    {
        return name;
    }
    this.getInformation = function()    // 各媒体ごとに異なる動作をするメソッドになる
    {
        console.log( name + "で情報を得る。" );
    }
}

// テレビ
function Television(media_name)
{
    Media.call(this, media_name);  // 親クラスのコンストラクタを call で呼び出して継承する。
    // テレビとしての追加プロパティ
    let power = "off";
    let channel = 1;
    // メソッド
    this.onPower = function()
    {
        if( power == "on" ) return;
        power = "on";
        console.log( this.getName() + "を点けました。" );
    }
    this.changeChannel = function(new_channel)
    {
        // 新しいチャンネルを代入する前に値が正しいかチェック
        new_channel = parseInt(new_channel);
        if ( isNaN(new_channel) ) return;               // チャンネルは数字意外は無視。
        if( new_channel < 1 || new_channel > 100 ) return;  // チャンネルは最低1、最高100。それ以外は無視。
        channel = new_channel;
        console.log( this.getName() + "のチャンネルを" + channel + "に変えました。" );
    }
    this.watchChannel = function()
    {
        console.log( this.getName() + "" + channel + "チャンネルを観ます。" );
    }
    this.getInformation = function()    // 親と同じメソッド名を定義することでオーバーライドする
    {
        this.onPower();     // 情報を得るためにはまずは電源を入れる
        this.watchChannel();
    }
}

// 新聞
const Newspaper = function(media_name)
{
    Media.call(this, media_name);  // 親クラスのコンストラクタの呼び出しには call を使用
    // 新聞としての追加プロパティ
    let page = "表紙";
    // メソッド
    this.changePage = function(new_page)
    {
        page = new_page;
        console.log( this.getName() + "" + page + "ページを開きました。" );
    }
    this.readPage = function()
    {
        console.log( this.getName() + "" + page + "ページを読みます。" );
    }
}

// インスタンス生成
const MaiasaNewspaper = new Newspaper("毎朝新聞");
const LivingTelevision = new Television("リビングのテレビ");

// 実行
MaiasaNewspaper.getInformation();   // 毎朝新聞で情報を得る。
LivingTelevision.getInformation();  // リビングのテレビを点けました。リビングのテレビの1チャンネルを観ます。

ポリモーフィズム(多様性)

テレビで情報を得る場合は電源を入れて目的のチャンネルに合わせ、映像と音で情報を得ます。新聞で情報を得るには目的のページを開いて、活字情報から情報を得ます。異なるオブジェクトですが似たような処理があることが分かります。
この共通する「情報を得る」という処理を予め決めたメソッド名で作るようにしておき、そのメソッドの中身は各オブジェクトに合わせて最適化しておくことで、メインの処理を作る側はいちいちオブジェクトによって処理を分岐する必要がなくなります。

このように、同じ呼び出し方なのに異なる動作(多様な動作)をするという特性をポリモーフィズムといいます。

抽象化ですでに getInformation という統一されたメソッドを用意できていますが、ページやチャンネルを変えるという動作を同じメソッド changeView で実現してみます。ついでにオブジェクトを連想配列に格納してループさせてみます。

// メディア
function Media(media_name)
{
    // プロパティ
    let name = media_name;
    // メソッド
    this.getName = function()
    {
        return name;
    }
    this.getInformation = function()    // 各媒体ごとに異なる動作をするメソッドになる
    {
        console.log( name + "で情報を得る。" );
    }
    this.changeView = function(view)
    {
        // 各オブジェクト側で実装してね
    }
}

// テレビ
function Television(media_name)
{
    Media.call(this, media_name);  // 親クラスのコンストラクタを call で呼び出して継承する。
    // テレビとしての追加プロパティ
    let power = "off";
    let channel = 1;
    // メソッド
    this.onPower = function()
    {
        if( power == "on" ) return;
        power = "on";
        console.log( this.getName() + "を点けました。" );
    }
    this.changeChannel = function(new_channel)
    {
        // 新しいチャンネルを代入する前に値が正しいかチェック
        new_channel = parseInt(new_channel);
        if ( isNaN(new_channel) ) return;               // チャンネルは数字意外は無視。
        if( new_channel < 1 || new_channel > 100 ) return;  // チャンネルは最低1、最高100。それ以外は無視。
        channel = new_channel;
        console.log( this.getName() + "のチャンネルを" + channel + "に変えました。" );
    }
    this.watchChannel = function()
    {
        console.log( this.getName() + "" + channel + "チャンネルを観ます。" );
    }
    this.getInformation = function()    // 親と同じメソッド名を定義することでオーバーライドする
    {
        this.onPower();     // 情報を得るためにはまずは電源を入れる
        this.watchChannel();
    }
    this.changeView = function(view)
    {
        this.changeChannel(view);
        this.watchChannel();
    }
}

// 新聞
const Newspaper = function(media_name)
{
    Media.call(this, media_name);  // 親クラスのコンストラクタの呼び出しには call を使用
    // 新聞としての追加プロパティ
    let page = "表紙";
    // メソッド
    this.changePage = function(new_page)
    {
        page = new_page;
        console.log( this.getName() + "" + page + "ページを開きました。" );
    }
    this.readPage = function()
    {
        console.log( this.getName() + "" + page + "ページを読みます。" );
    }
    this.changeView = function(view)
    {
        this.changePage(view);
        this.readPage();
    }
}

// インスタンス生成
let medias =
{
    MaiasaNewspaper : new Newspaper("毎朝新聞"),
    LivingTelevision : new Television("リビングのテレビ"),
    TouzaiNewspaper : new Newspaper("東西新聞"),
}

// 連続実行
Object.keys(medias).forEach(function (key)
{
    medias[key].getInformation();
    var random = Math.floor( Math.random() * 20 ) + 1;  // ページやチャンネルを乱数で指定
    medias[key].changeView( random );
});

// 出力は以下のようになる。ページやチャンネルは乱数。

// 毎朝新聞で情報を得る。
// 毎朝新聞の2ページを開きました。
// 毎朝新聞の2ページを読みます。
// リビングのテレビを点けました。
// リビングのテレビの1チャンネルを観ます。
// リビングのテレビのチャンネルを5に変えました。
// リビングのテレビの5チャンネルを観ます。
// 東西新聞で情報を得る。
// 東西新聞の1ページを開きました。
// 東西新聞の1ページを読みます。

クラス

ここまでのソースコードはクラスを使えば(特に他の言語で慣れ親しんだ方であれば)もっと分かりやすくなります。

ただし、クラス構文はECMAScript 6(ES6)以降になるのでIEでは使えません。クラスのパブリック記述やプライベート記述となるとさらにSafariも未対応になるのでiPhoneでも使えなくなります。

最初の名前空間で作成したコードをクラスで書き直すと以下のようになります。

// 新聞
class Newspaper
{
    // プロパティ
    name = "毎朝新聞";
    page = "表紙";
    // メソッド
    changePage(new_page)
    {
        this.page = new_page;
        console.log( this.name + "" + this.page + "ページを開きました。" );
    }
    readPage()
    {
        console.log( this.name + "" + this.page + "ページを読みます。" );
    }
}
// テレビ
class Television
{
    // プロパティ
    name = "リビングのテレビ";
    power = "off";
    channel = 1;
    // メソッド
    onPower()
    {
        if( this.power == "on" ) return;
        this.power = "on";
        console.log( this.name + "を点けました。" );
    }
    changeChannel(new_channel)
    {
        this.channel = new_channel;
        console.log( this.name + "のチャンネルを" + this.channel + "に変えました。" );
    }
    watchChannel()
    {
        console.log( this.name + "" + this.channel + "チャンネルを観ます。" );
    }
}

// インスタンス生成
const MaiasaNewspaper = new Newspaper();
const LivingTelevision = new Television();

// 実行
MaiasaNewspaper.changePage( "テレビ欄" ); // 毎朝新聞のテレビ欄ページを開きました。
MaiasaNewspaper.readPage();               // 毎朝新聞のテレビ欄ページを読みます。
LivingTelevision.onPower();               // リビングのテレビを点けました。
LivingTelevision.changeChannel( 10 );     // リビングのテレビのチャンネルを10に変えました。
LivingTelevision.watchChannel();          // リビングのテレビの10チャンネルを観ます。
LivingTelevision.changeChannel( 4 );      // リビングのテレビのチャンネルを4に変えました。
LivingTelevision.watchChannel();          // リビングのテレビの4チャンネルを観ます。
LivingTelevision.changeChannel( 1 );      // リビングのテレビのチャンネルを1に変えました。
LivingTelevision.watchChannel();          // リビングのテレビの1チャンネルを観ます。

クラスはオブジェクトの設計書みたいなものなので、そのままは使えません。new でインスタンス化する必要があります。クラスのメソッドには function 宣言が不要になり、プロパティの宣言は変数同様に = で値を入れます。

クラスでのカプセル化

クラスのプライベートフィールド宣言はちょっと特殊で頭に # を付けます。
カプセル化のところで作成したコードをクラスで書き直すと以下のようになります。

// テレビ
class Television
{
    // プロパティ
    #name = "リビングのテレビ";
    #power = "off";
    #channel = 1;
    // メソッド
    onPower()
    {
        if( this.#power == "on" ) return;
        this.#power = "on";
        console.log( this.#name + "を点けました。" );
    }
    changeChannel(new_channel)
    {
        // 新しいチャンネルを代入する前に値が正しいかチェック
        new_channel = parseInt(new_channel);
        if ( isNaN(new_channel) ) return;               // チャンネルは数字意外は無視。
        if( new_channel < 1 || new_channel > 100 ) return;  // チャンネルは最低1、最高100。それ以外は無視。
        this.#channel = new_channel;
        console.log( this.#name + "のチャンネルを" + this.#channel + "に変えました。" );
    }
    watchChannel()
    {
        console.log( this.#name + "" + this.#channel + "チャンネルを観ます。" );
    }
}

// インスタンス生成
const LivingTelevision = new Television();

// 実行
LivingTelevision.onPower();                 // 出力: リビングのテレビを点けました。
LivingTelevision.channel = -10;             // プライベートなプロパティとは別のプロパティにアクセスしている。
// LivingTelevision.#channel = -10;         // これはプライベートにはアクセスできないよと怒られる。
LivingTelevision.changeChannel( -10 );      // 誤ったチャンネル指定は無視される。
LivingTelevision.watchChannel();            // 出力: リビングのテレビの1チャンネルを観ます。
console.log(LivingTelevision.channel);      // -10 と出力される。

クラスでの継承

クラスの継承は他の言語でもおなじみの extends を使います。
継承のところで作成したコードをクラスで書き直すと以下のようになります。

// メディア
class Media
{
    // プロパティ
    #name;
    // コンストラクタ
    constructor(media_name)
    {
        this.#name = media_name;
    }
    // ゲッター
    get name()
    {
        return this.#name;
    }
    // メソッド
    getInformation()    // 各媒体ごとに異なる動作をするメソッドになる
    {
        console.log( this.name + "で情報を得る。" );
    }
}

// テレビ
class Television extends Media
{
    // テレビとしての追加プロパティ
    #power = "off";
    #channel = 1;
    // コンストラクタ
    constructor(media_name)
    {
        super(media_name); // 親コンストラクターを呼び出す
    }
    // メソッド
    onPower()
    {
        if( this.#power == "on" ) return;
        this.#power = "on";
        console.log( this.name + "を点けました。" );
    }
    changeChannel(new_channel)
    {
        // 新しいチャンネルを代入する前に値が正しいかチェック
        new_channel = parseInt(new_channel);
        if ( isNaN(new_channel) ) return;               // チャンネルは数字意外は無視。
        if( new_channel < 1 || new_channel > 100 ) return;  // チャンネルは最低1、最高100。それ以外は無視。
        this.#channel = new_channel;
        console.log( this.name + "のチャンネルを" + this.#channel + "に変えました。" );
    }
    watchChannel()
    {
        console.log( this.name + "" + this.#channel + "チャンネルを観ます。" );
    }
    getInformation()    // 親と同じメソッド名を定義することでオーバーライドする
    {
        this.onPower();     // 情報を得るためにはまずは電源を入れる
        this.watchChannel();
    }
}

// 新聞
class Newspaper extends Media
{
    // 新聞としての追加プロパティ
    #page = "表紙";
    // コンストラクタ
    constructor(media_name)
    {
        super(media_name); // 親コンストラクターを呼び出す
    }
    // メソッド
    changePage(new_page)
    {
        this.#page = new_page;
        console.log( this.name + "" + this.#page + "ページを開きました。" );
    }
    readPage()
    {
        console.log( this.name + "" + this.#page + "ページを読みます。" );
    }
}

// インスタンス生成
const MaiasaNewspaper = new Newspaper("毎朝新聞");
const LivingTelevision = new Television("リビングのテレビ");

// 実行
MaiasaNewspaper.getInformation();   // 毎朝新聞で情報を得る。
LivingTelevision.getInformation();  // リビングのテレビを点けました。リビングのテレビの1チャンネルを観ます。
console.log(MaiasaNewspaper.name);  // 毎朝新聞
console.log(LivingTelevision.name); // リビングのテレビ

関数で記述していた方では new したときに引数を渡し、その引数をそのままプロパティに代入していましたが、クラスでは new したときに必ず実行されるコンストラクタを用意します。

継承先で親のコンストラクタを呼び出したい場合はスーパークラス super を呼び出します。

親クラスにあるゲッターは、外部から MaiasaNewspaper.name というようにプロパティのように参照でき、内部ではプロパティではなくメソッドを実行できるので、よりカプセル化しやすくなります。

最後にポリモーフィズムのところで作成したコードをクラスで書き直すと以下のようになります。

// メディア
class Media
{
    // プロパティ
    #name;
    // コンストラクタ
    constructor(media_name)
    {
        this.#name = media_name;
    }
    // ゲッター
    get name()
    {
        return this.#name;
    }
    // メソッド
    getInformation()    // 各媒体ごとに異なる動作をするメソッドになる
    {
        console.log( this.name + "で情報を得る。" );
    }
    changeView(view)
    {
        // 各オブジェクト側で実装してね
    }
}

// テレビ
class Television extends Media
{
    // テレビとしての追加プロパティ
    #power = "off";
    #channel = 1;
    // コンストラクタ
    constructor(media_name)
    {
        super(media_name); // 親コンストラクターを呼び出す
    }
    // メソッド
    onPower()
    {
        if( this.#power == "on" ) return;
        this.#power = "on";
        console.log( this.name + "を点けました。" );
    }
    changeChannel(new_channel)
    {
        // 新しいチャンネルを代入する前に値が正しいかチェック
        new_channel = parseInt(new_channel);
        if ( isNaN(new_channel) ) return;               // チャンネルは数字意外は無視。
        if( new_channel < 1 || new_channel > 100 ) return;  // チャンネルは最低1、最高100。それ以外は無視。
        this.#channel = new_channel;
        console.log( this.name + "のチャンネルを" + this.#channel + "に変えました。" );
    }
    watchChannel()
    {
        console.log( this.name + "" + this.#channel + "チャンネルを観ます。" );
    }
    getInformation()    // 親と同じメソッド名を定義することでオーバーライドする
    {
        this.onPower();     // 情報を得るためにはまずは電源を入れる
        this.watchChannel();
    }
    changeView(view)
    {
        this.changeChannel(view);
        this.watchChannel();
    }
}

// 新聞
class Newspaper extends Media
{
    // 新聞としての追加プロパティ
    #page = "表紙";
    // コンストラクタ
    constructor(media_name)
    {
        super(media_name); // 親コンストラクターを呼び出す
    }
    // メソッド
    changePage(new_page)
    {
        this.#page = new_page;
        console.log( this.name + "" + this.#page + "ページを開きました。" );
    }
    readPage()
    {
        console.log( this.name + "" + this.#page + "ページを読みます。" );
    }
    changeView(view)
    {
        this.changePage(view);
        this.readPage();
    }
}

// インスタンス生成
let medias =
{
    MaiasaNewspaper : new Newspaper("毎朝新聞"),
    LivingTelevision : new Television("リビングのテレビ"),
    TouzaiNewspaper : new Newspaper("東西新聞"),
}

// 連続実行
Object.keys(medias).forEach(function (key)
{
    medias[key].getInformation();
    var random = Math.floor( Math.random() * 20 ) + 1;  // ページやチャンネルを乱数で指定
    medias[key].changeView( random );
});

// 出力は以下のようになる。ページやチャンネルは乱数。

// 毎朝新聞で情報を得る。
// 毎朝新聞の2ページを開きました。
// 毎朝新聞の2ページを読みます。
// リビングのテレビを点けました。
// リビングのテレビの1チャンネルを観ます。
// リビングのテレビのチャンネルを5に変えました。
// リビングのテレビの5チャンネルを観ます。
// 東西新聞で情報を得る。
// 東西新聞の1ページを開きました。
// 東西新聞の1ページを読みます。
76
72
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
76
72