JavaScriptの関数型パターンによるクラス定義を学び信頼性のあるプログラムを書こう
2017/12/04
どうも、こんにちわ。
フロントエンドエンジニアという肩書きを名乗っているのにも関わらず、最近、オブジェクト指向なプログラミングを書けていない管理人です。
というわけで今回は自分のリハビリも兼ねてJavaScriptの関数型継承というものを説明します。
JavaScriptでクラス定義(厳密にはJavaScriptにはクラスという概念は存在しません)を行うにはprototypeを利用するのが一般的ですが、PHPやActionScriptなどから入った人には異質なものに感じたり、プライベートなメンバーが作成できなかったり、どこからでも書き換え可能なためルールを厳格化しないと後々大変なことになったりと不便な点があります。
そこで登場するのが関数型パターンでのクラス定義です。prototypeによるクラス定義のデメリットを見事に解消してくれます。
なお、当記事を読む為には「JavaScriptの基礎知識」、「クロージャー」、「値渡しと参照渡し」、「オブジェクト指向」などの概念を理解している必要があります。
では、早速簡単なサンプルコードを見ていきましょう。
この記事の目次
関数型パターンによるクラス定義はクロージャーそのもの
// Personクラスを定義 function Person(n){ var _name = n; //private変数 function talk(){ console.log("私の名前は" + _name + "です"); } return { talk : talk //publicメソッドとしたいものをオブジェクトの値として渡す }; } // Personクラスのインスタンスを作成 var p = Person("田中"); p.talk();
勘の良い人はお分かりだと思いますがクロージャーそのものなんです。違いといえばこちらのサンプルでは関数をreturnしているのに対し、上記のサンプルではオブジェクトに内包して関数をreturnしています。オブジェクトに内包することで複数の関数をreturnできる為インスタンスのメソッドのように振舞えるわけです。returnされるのはあくまでもオブジェクトに内包された関数であってインスタンスでは無いのでご注意ください。
続いてパブリックメソッドを複数定義した場合です。
function Person(n){ var _name = n; function talk(){ console.log("私の名前は" + _name + "です"); } function walk(){ //追加 console.log("歩きます"); } return { talk : talk, walk : walk //追加 }; } var p = Person("田中"); p.walk();
walkメソッド定義とreturnするオブジェクトにwalkメソッドを追加しただけです。returnするオブジェクトに追加しない場合privateメソッドとして扱われる為、p.walk()の部分でエラーが発生し動作しません。
public変数は定義不可能。代用となるメソッドを定義して対処
ここまでの説明でpublicメソッドとprivateメソッドそしてprivate変数の定義の仕方は理解できたかと思います。残るはpublic変数です。「メソッドと一緒にreturnすれば良いのでは」と思われるかも知れません。一見動作しそうです。しかし、正常に動きません。何故だか分かりますか?サンプルを見てみましょう。
function Person(n){ var name = n; function talk(){ console.log("私の名前は" + name + "です"); } return { talk : talk, name : name //パブリック変数? }; } var p = Person(); p.name = "田中" //コンストラクタ実行後にnameを設定 p.talk(); //「私の名前はundefinedです」 と出力される console.log(p.name); //「田中」と出力される
private変数_nameをpublic変数nameに変更しました。結果は「undefined」ということでpublic変数が正常に働いてないようです。しかし、19行目ではちゃんと出力されています。どういうことでしょうか。
答えは値渡しだからです。
JavaScriptの関数の引数について以下のものは値渡しになります。
- 文字列型
- 数値型
- 論理型
- null
- undefined
オブジェクトに内包したとしても値渡しになります。その為、public変数の参照をreturnしたつもりが変数の値だけをreturnしていたということです。
「ではどうやってpublicな変数を用意すればよいのか」という話になりますが、下記のようにpublic変数nameの代わりにpublicメソッドnameを定義し、private変数_nameを設定・取得するようにします。
function Person(n){ var _name = n; function talk(){ console.log("私の名前は" + _name + "です"); } /* * private変数_nameを設定・取得するためのメソッド */ function name(val){ if(val === undefined){ return _name; }else{ _name = val; } } return { talk : talk, name : name }; } var p = Person(); p.name("田中"); //nameメソッドを通じて、private変数_nameを設定 p.talk(); //「私の名前は田中です」 と出力される console.log(p.name()); //「田中」と出力される
nameメソッドは引数の有無によって処理を分岐させています。こうすることでpublic変数のような振る舞いをさせることが可能になります。
クラスの継承方法もシンプル
さて次はオブジェクト指向の肝である「継承」についてです。早速サンプルコードを見ていきましょう。
function Person(n){ var _name = n; function talk(){ console.log("私の名前は" + _name + "です"); } function name(val){ if(val === undefined){ return _name; }else{ _name = val; } } return { talk : talk, name : name }; } function Tanaka(){ var p = Person("田中"); //Personクラスを継承 p.sleep = sleep; //メソッドを追加 function sleep(){ console.log(p.name() + "は寝た"); } return p; } var t = Tanaka(); t.talk(); //「私の名前は田中です」 と出力される t.sleep(); //「田中は寝た」 と出力される
23行目以降が追加部分です。
Personクラスを継承したサブクラスTanakaクラスです。
Tanakaクラスではコンストラクタで”田中”が設定され、sleepメソッドが新しく追加されています。動作も問題なさそうです。ね、簡単でしょ?
関数型パターンのメリット・デメリット
このように関数型パターンによるクラス定義は、prototypeやthisといったキーワードを一切使用する必要が無いため、クロージャーさえ理解していれば少ないコストで学習可能です。さらにオーソドックスなprototype継承では実装できなかったprivateなメンバーが定義出来るので信頼性の高いコードを記述可能になります。
しかし、便利な関数型パターンにも弱点があります。メモリ効率が悪いということです。インスタンスを作成するたびに新しくメソッドを生成する為、何千何万とインスタンスを作るような場合大きくパフォーマンスを落とします。普段は気にする必要はほとんど無いレベルですが大規模なシステムを作る場合はprototypeと併用するといった対応が必要です。
学習に役立つ書籍の紹介
関数型コンストラクタ継承はオライリーが出版している「JavaScript: The Good Parts ―「良いパーツ」によるベストプラクティス」でも紹介されています。また、本書ではこの他にも様々な良いパーツが紹介されています。
しかし、本書は一通りJavaScriptの勉強を終えている人が改めて正しい知識を得るための書籍ですので万人向けではありません。これからJavaScriptを始めてみたいという方は「よくわかるゼロからはじめるJavaScript」が分かり易くオススメです。
ある程度JavaScriptが書けるようになってきたらJavaScript定番書籍オライリーの「JavaScript(通称サイ本)」を手に取り深い部分を学習しましょう。そこまで学習が済んだらいよいよ「JavaScript: The Good Parts」を学ぶときです。