【JavaScript】他言語からJavaScriptへ移行した時のオブジェクト指向での勘所 1回目
どうも、先日まで共同研究の進捗報告でてんやわんやしてました。
さてはて、こちらは雪も落ち着いてきたかなと思ったら降ってきたりと相変わらず安定しない天気です。
冬になるとどうしても家に籠もりガチになるのは良くない傾向ですね。
来月にはひとつイベントもあるし、4月には後輩への勉強会に向けてそろそろ準備をしなければと思ってます。
ではでは、今回はJavaScriptのオブジェクト指向で躓いた所の要点を全2~3回に分けてやっていきたいと思います。
去年の夏頃にFlashの衰退のせいか、Canvas要素にActionScript3.0と同じ感覚で書ける描画オブジェクトを扱えるCreateJSというライブラリを触ろうと思い、Javascriptを触り始めました。
触り始めた時はAS3との差異をあまり感じませんでしたが、オブジェクト指向な書き方をしようとすると途端に詰まりました。
様々なサイトを参考にして、クラスの作成、クラスの継承、カプセル化などのやり方をJavascriptでどのように実現すればいいのかを調べ、何となく理解しながら使っていましたが、完全に腹落ちはしておらず仕組みが分からないまま使っている感があり、正直気持ち悪いと思っていたので色々と調べてまとめてみました。
特に自分のようにJava、C++などのC言語派生(正確にはB言語)のオブジェクト指向から来た人はかなり痛い目を見る場合が多いので、ここで腹落ちしてもらえると個人的に嬉しいです。
私のOOPの経験
JSのOOPの解説を行う前に自分が触れるオブジェクト指向な言語を以下に紹介します。
- C
ポインタでどのようにメモリ確保されるのか、関数スタックやプリコンパイルでの多次元配列のメモリ空間上での並び方を知っている程度の実力 - C++
STLとかOperatorとかClassとかテンプレートを使っている程度の実力 - Java
主にAndroidで使用。Javaらしいコードは書けない程度の実力 - ActionScript3.0
適当なエフェクト、ゲームが作れる程度の実力 - C#
ネットで適当にググりながら研究用で使うGUIを作れる程度の実力
こんな所でしょうか。
見て頂けると分かると思いますが、AS3を除いた言語では主にC/C++をリスペクトしたOOPです。
AS3もECMASCRIPTに準拠していますが、書き方はJavaに近い書き方になっており、同じECMASCRIPT準拠のJSでもクラス定義の方法はまったく異なります。
JSに触れるまで、一つの言語をしっかりと理解すれば大体の言語は特有の書き方さえ除けば同じ要領で使えるので、移行には特に労力を必要としないというスタンスだったのですが、現在はその考えを多少改める必要が出てきました。
おそらく、JSはLispをリスペクトした言語であるのもその要因の一つだからでしょうね。
ちなみに、私は関数型言語は食わず嫌いで触っておりません。
JavaScriptのOOP
さて、早速ですが今回は私が詰まっていた部分を見事に全て解説してくれている資料がありました。
そのスライドは次回、説明する時に掲載したいと思います。
文章の省略の為、以降からJavaScriptのオブジェクト指向プログラミングを「JSOOP」と略して呼称したいと思います。
JSOOPでは他言語からのOOPでいう以下の機能の実現はまりやすいと個人的に感じています。
- クラスの作成方法
- カプセル化
これらをJSOOPで実現すると以下の仕組みを利用する必要があります。
- オブジェクトのプロトタイプ
- プロトタイプチェーン
- 即時関数
- クロージャ
ここではトップダウンに解説を行っていきましょう。
JSでのクラスの作成方法
とりあえず、同じクラスをJavaとJSで作成してみる。
class Human { private int age; private string name; public int height; Human(int age, string name, int height){this.age = age; this.name = name; this.height = height;}; public void setAge(int age){this.age = age}; public void setName(string name){this.name = name}; public string sayHello(){return "Hello, I'm " + name + ", and my age is " + aeg + ".";} public string sayHeight(){return "My height is " + height + ".";} }
var Human = (function() { function Human(cAge, cName, cHeight){ var age = cAge; var name = cName; this.height = cHeight; this.setAge = function(v){age = v;}; this.setName = function(v){name = v;}; this.sayHello = function(){return "Hello, I'm " + name + ", and my age is " + age + ".";}; } Human.prototype.sayHeight = function(){return "My height is " + this.height ".";}; return Human; })();
こんな感じ書くことで上のJavaと同様の処理が期待出来る。
しかし、Javaとは動作が一緒だが、裏側で行っている動作が圧倒的に違う部分がある。
JSでのクラス利用上の注意
上のJavaコードではprivateを使って変数をカプセル化して外部のオブジェクトから"age", "name"変数を見えなくしている。
もちろん、その動作はJS側のコードでも保証されている。
では、以下のようにJavaコードを拡張したとする。
class Human { private int age; private string name; public int height; Human(int age, string name, int height){this.age = age; this.name = name; this.height = height;}; public void setAge(int age){this.age = age}; public void setName(string name){this.name = name}; public string sayHello(){return "Hello, I'm " + name + ", and my age is " + aeg + ".";} public string sayHeight(){return "My name is " + name + ", and my height is " + height + ".";} }
Javaもあんな感じで"name"変数と文字列を連結して拡張してるし、こっちも関数に同じようにやってみよう!
var Human = (function() { function Human(cAge, cName, cHeight){ var age = cAge; var name = cName; this.height = cHeight; this.setAge = function(v){age = v;}; this.setName = function(v){name = v;}; this.sayHello = function(){return "Hello, I'm " + name + ", and my age is " + age + ".";}; } Human.prototype.sayHeight = function(){return "My name is " + name + ", and my height is " + this.height + ".";}; return Human; })();
よし、JSの拡張も出来たしこのsayHeight()関数を呼び出そう!
var h1 = new Human(23, "hoge", 170); console.log(h1.sayHeight()); // 期待している結果 // My name is hoge, and my height is 170. // 出力された結果 // My name is , and my height is 170.
あれ、nameの部分が何も表示されない?"hoge"って名前を設定してるのに何故?
試しに関数呼び出し前に以下のような一文を付け加えてみると…
var h1 = new Human(23, "hoge", 170); window.name = "Huga"; console.log(h1.sayHeight()); // My name is Huga, and my height is 170.
「window.nameに代入するとその結果が表示されてる!?
ってことは、このnameの指している先ってグローバル変数ってこと!!?」
そう、これがJSで悪名高き「グローバル汚染」を引き起こす要因にもなりそうですね。
この関数を期待した結果にする場合は以下のようにコードを変更する必要がある。
var Human = (function() { function Human(cAge, cName, cHeight){ var age = cAge; var name = cName; this.height = cHeight; this.setAge = function(v){age = v;}; this.setName = function(v){name = v;}; this.sayHello = function(){return "Hello, I'm " + name + ", and my age is " + age + ".";}; this.sayHeight = function(){return "My name is " + name + ", and my height is " + this.height + ".";}; } return Human; })();
カプセル化の問題点
「上の例を見るとJavaScriptのpublicな関数っていうのは、publicな変数しか扱えないってこと?
それじゃ、このHumanっていうコンストラクタみたいな関数の中にどんどん"this.なんちゃら"って付け加えればいいじゃん。
わざわざprototypeなんて面倒な物通さなくてもいいんじゃないの?」
と思った人も居ると思うので、以下の様なクラスを用意した。
var Hoge = (function() { function Hoge(){ this.sayHello = function(){return "Hello, Hoge"}; } return Hoge; })(); var Huga = (function() { function Huga(){} Huga.prototype.sayHello = function(){return "Hello, Huga"}; return Huga; })();
更にこのクラスを使用して以下のコードを実行してみる。
var hoge1 = new Hoge(); var hoge2 = new Hoge(); console.log(hoge1.sayHello()); // Hoge console.log(hoge2.sayHello()); // Hoge console.log(hoge1.sayHello == hoge2.sayHello); // false var huga1 = new Huga(); var huga2 = new Huga(); console.log(huga1.sayHello()); // Huga console.log(huga2.sayHello()); // Huga console.log(huga1.sayHello == huga2.sayHello); // true
上の結果で一番注目して欲しいのは結果を同値比較してみるとprototypeを使用していないHogeはfalse、使用しているHugaはtrueが出ている。
この結果の差異は何を示しているのだろか?
それはつまりクラス内に宣言された関数がインスタンス毎に別のオブジェクトとして生成されているか、どうかということ。
更に噛み砕いて言ってしまうとHogeクラスは無駄に関数のオブジェクトを生成し、メモリ空間を余分に喰っているということ。
falseの結果が出ているHogeは"this.なんちゃら"で関数を宣言しているが、これが主な原因。
ここでお気づきの方も居ると思うが、privateな変数にアクセスするにはHogeクラスと同様の方法で関数を宣言した。
つまり、カプセル化を行うことでデータの秘匿性は高まるが代わりにメモリを余分に消費してしまう、という問題点がある。
※もしかしたら、カプセル化をしつつメモリの無駄使いを回避する方法があるのかもしれないが私は知らない。知っていたら教えて頂けると助かります!
次回
冒頭でも触れたように
- オブジェクトのプロトタイプ
- プロトタイプチェーン
という用語を出したいが、これはこの回でも触れたprototypeに深く関係している。
- 即時関数
- クロージャ
以上のワードを直接は出していないが、今回のコードにたっぷりとこれらを使っている。
次回はこれの用語について深く説明したいと思う。
1回で4つを全て説明するのが大変だったら2つに分けます!
では、次回へ続く!