なめこ備忘録

プログラミングに関する備忘録や経験したこと,考えたことなど好き勝手書きます.

[tex: ]

JavaScriptでプロパティを実装してみる

こんにちは,なめこです.

2,3年ほど前にC#のプロパティを乱用し,それをJavaScriptでも同様に実装しようとしていた時期がありました.(発想が既におかしいですけど先人がいました)
今回はそんなJavaScriptオブジェクト指向をする時に必要になるであろうプロパティの実装までを備忘録として書き留めておきます.

オブジェクト指向とは

そもそものお話から書いていこうと思います.
簡単に言うとオブジェクト単位で機能を実装して,それらの相互作用でシステムを作るような考え方です.
はい,まだちょっとわかりにくいですね.

例としてデスクトップパソコンを考えます.
キーボード,マウス,ディスプレイ,パソコン本体(もうちょっと分けられそう)の4つくらいのモノに分けて考えます.
これらのモノがオブジェクトです.マウスとかディスプレイっていうオブジェクトです.
どのオブジェクトもお互いが一体どういう原理でどう動いているのかなんて知りません.
でもデータ通信の規格が決まっていて,どんな機能を持っているのかは分かっているので,何の問題もなく動作するわけです.
それぞれが固有の機能(キーボードならキーを押すとか)を持っていて,その結果のデータのやり取りは何かしらの通信によって行われていますよね.
どんな機能でどんなデータが飛んできてってのが分かっていれば中身を知らなくていいわけです.
自分の使っているものの全原理を完璧に理解した上で使っている人なんていません.
ここを押したらこうなるっていう機能とその結果だけ分かっていればいいんです.

オブジェクト指向では各オブジェクトの機能(とデータのやり取りの形式)を定めて中身はブラックボックスとして考えます.

小さくて簡単なプログラムを作る時には特に必要ないと思いますが,これが大規模で複雑なものになるとすごくありがたいですよね.
君はキーボード作って,君はマウスねって委託できるんですから.
ついでに何かエラーが生じてもオブジェクトごとにチェックできるんですから原因の発見もすんなりいきそうです.

カプセル化

オブジェクト指向って実装の隠蔽(カプセル化)って考えがあります.
これも簡単に言うと,外から使える機能だけ公開して,他は見えないようにしましょうってことです.
キーボードのキーを押すのはいいけど,解体して中の回路とかをいじられると困るわけです.なのでそれをできないようにしましょうねってことです.

そのために,オブジェクト内の各変数やメソッド(関数)にアクセスレベル(アクセスできる範囲の制限)を設定します.
よくみかけるのはprivateとpublicですね.
privateはそのオブジェクト自身からしか操作(参照も)できません.逆にpublicはどこからでも操作できます.
キーボードの中身(主に変数)は外枠で隠して(private),外側のキーは使える機能(メソッド)として公開(public)する感じです.

プロパティとは

ようやく今回のメイン,プロパティです.
プロパティはJavaC++にはない機能で外側からは変数,内側からはメソッドのように振舞います.

順を追って説明しましょう.
オブジェクト指向では基本的に変数は全部隠して,外側から操作するときは公開してあるメソッドから行います.
こんな感じです.

private int data; // 外からは操作できない
public int GetInt(){return data;} // dataを取得
public void SetInt(int d){this.data = d;} // dataに値をセット

どうせゲットもセットもできるようにするなら変数を公開すればいいのにって思うかもしれませんが,メソッドからの操作が基本ですから一部だけ変数だと使いづらくなるんです.
この実装方法は,例えば何かの処理中の内部データを保存している変数があったとして,途中経過を確認したいからデータを観れるようにしたいけど,操作できてしまうのは困るってときに重宝します.

でもこれだと変数の数だけGet〜やSet〜のメソッドが出てきかねないんです.
コードが無駄に長くなりますし,読みづらいです.
また,メソッドからの操作が基本と言いましたが,使用者視点だと,Get,Setなんかより普通に変数に代入したいわけです.

そこで,外からは変数,内からはメソッドのように使えるプロパティが登場します.
このプロパティ,SetとGetでアクセスレベルを変えられるというのがとても便利です.

C#での実装

C#での実装法をざっくり書いておきます.

private int d;
public int data{
    get{ return d;}
    private set{ d = value;}
}

こんな記述で,外からは見ることしかできないdata変数のようなものができます.
詳細は省きますが,書き方はほぼ関数なので途中に他の処理を挟んでも問題なく機能します.

JavaScriptオブジェクト指向をしてみる

それでは,まずはオブジェクト指向プログラミングをJavaScriptでやってみましょう.
もともとJavaScriptオブジェクト指向スクリプト言語らしいので,普通にオブジェクト指向の書き方ができます.
ただしC#Javaとはちょっと違った記法になります.

オブジェクトの生成

まずはオブジェクトの生成方法です.
この時点で複数あります...というか,実はJavaScript,ほとんどがオブジェクトです.
配列だと思っていたもの,連想配列だと思っていたもの,関数だと思っていたもの,純粋な変数以外のもの全てがオブジェクトです.
ですので,配列として宣言したものに唐突に新しい変数名とデータを入れてあげても当たり前のように動きます.

var a = [1, 2, 3];
a.data = 'data'; // aというオブジェクトに新しくdataという変数を作った上で'data'を代入

console.log(a.data); // data
console.log(a['data']); // data
console.log(a); // [ 1, 2, 3, data: 'data' ] 実際の中身はこんな感じ

a.push(10); // 普通の配列としての操作もできる
console.log(a[3]); // 10

というわけで,だいたい何でもオブジェクト扱いですが,よく使われるオブジェクトの宣言方法を示します.
thisだとかコンストラクタだとかの細かい説明は省きます.

空のオブジェクトの宣言方法

var obj = new Object();
obj.data = 10;
obj.func = function(){return this.data;};
console.log(obj.data); // 10
console.log(obj.func()); // 10

連想配列のような宣言方法

var obj = {
    data : 'd',
    i : 5,
    func : function(n){
        return this.i*n;
    }
};
console.log(obj.data); // d
console.log(obj.i); // 5
console.log(obj.func(4)); // 20

関数のような宣言方法

コンストラクタ関数を定義することでオブジェクトを生成することができます.

function Obj(data, i, func){
    this.data = data;
    this.i = i;
    this.func = func;
}
var obj = new Obj('data', 10, function(){return this.data;});
console.log(obj.func()); // data

プロパティの実装

さてようやくJavaScriptでのプロパティの実装です.setやgetを設定できるのは連想配列の時のみのようです.
以下のようにするとsetやgetを設定できますが,JavaScriptではアクセスレベルの指定はできません.

var obj = {
    _data : 'd',
    _i : 5,
    get data(){return _data;},
    get i(){return _i;},
    set i(x){_i = i;}
};
console.log(obj.data); // d
obj.i = 20;
console.log(obj.i); // 20

getやsetの後に関数のように定義することでプロパティが実装できます.
しかし,この実装方法だと

obj._i = 5;
obj._data = 'null';

とすることでアクセスできてしまいます.
これではアクセサの意味があまりありません.

そこで,JavaScriptの特殊な仕様を利用して次のようにしてオブジェクトを生成します.

var obj = (function(){
    var data = 'obj_data';
    var i = 0;
    return {
        get data(){return data;},
        get i(){return i;},
        set i(x){i = x;}
    };
})();

はい,一気にわけのわからなさが増しましたね.
でもこうすることで,以下のようにsetを設定していないdataにはデータのセットができないようになります.

obj.data = 'null'; // 代入されないがエラーも出ない
console.log(obj.data); // obj_data(内部で入れられたデータが入っている)
obj.i = 10;
console.log(obj.i); // 10

JavaScriptではfunctionのオブジェクトを変数に入れたとき,関数内のローカル変数は最初に宣言されたあと保持されるようです.
なのでobjに代入する際の無名関数の内部で宣言した data や i が消えずに残ります.

また,関数内で新たに宣言したローカル変数に関しては外側からはアクセスできないので実質的にprivateで宣言したものと同じになります.
正確にはobjに実際に入れられているのは,即時関数の返り値である連想配列なので,ローカル変数へのアクセスの手段がそもそもないですね.
なぜこれでローカル変数の値が保持され続けるのか不思議ですが,細かい話をするとわけがわからなくなるのでやめておきましょう.

まとめ

というわけで,JavaScriptにちゃんと機能として実装されているという感じではないですが,プロパティの実装ができました. JavaScriptオブジェクト指向プログラミングをする際にプロパティをどうしても使いたい!となったときは,

  • 隠蔽したいものはローカル変数
  • 公開するものは返り値の連想配列の中

とすれば実装できます.
ただし可読性はどんどん下がっていくので気をつけましょう.