jsのオブジェクト指向プログラミングことはじめ
社内勉強会での資料を備忘録的に掲載します。
- 対象
- 言語仕様
- Object
- Objectを調べる関数群
- Function
- new
- プロパティチェーン
- 内部プロパティPrototypeとObject.prototype
- toStringはだれのプロパティか?
- 話さなかったが、残っているトピック
対象
- オブジェクト指向プログラミング言語に触れたことがある。
- オブジェクト指向プログラミングの持つ要素を理解している。オブジェクト指向プログラミングとは以下を想定している。
- JavaScriptよくわからんっていうサーバサイドエンジニア
JavaScriptの言語仕様の中でどのように実現できるかを説明します。 「JavaScriptのプロトタイプベースのoopを実現している」ということで、言語仕様およびプロトタイプについて厚めに説明し、継承を重点的に解説します。
言語仕様
まず、JavaScriptの言語仕様のうち、以下の4つのトピックを抑えている必要があります。 ※正確にはJavaScriptの仕様ではなく、ECMAScript標準の仕様。JavaScriptは実装系による互換性が低いため、ECMAScript標準が定められている
- Object (データ型についての理解)
- Function (関数型について理解)
- new (オブジェクト生成について理解)
- Prototype (プロトタイプチェーンの言語仕様の理解)
Object
JavaScriptのデータ型は、プリミティブ型、参照型の2種類あります。
プリミティブ型
単純なデータとしてメモリに格納される
- 真偽値
- 数値
- 文字列
- null
undefined
なぜnullとundefinedは別々に用意されている?
- ごめんなさいmm 納得のいく説明ができませんmm 直感的には、存在しないことを表現するのがnull。定義されてないことを表現するのがundefinedという理解でしょうか。
※ JavaScriptではプリミティブ型もメソッド呼び出しができる。それを実現しているのが、プリミティブラッパー型の存在だがここでは割愛。
参照型
実体の場所を指すポインタとして格納されます。 JavaScriptには言語仕様としてのクラス(オブジェクト(実体)を抽象化して記述する仕組み)はないです。 参照型のインスタンスを生成する仕組みは存在します。 new演算子によって提供されています。 JavaScriptでは参照型に属している値のことを一般的にオブジェクトとして扱います。
ジェネリックなオブジェクトは、Object型の値(インスタンス)として生成できます。
var hoge = new Object(); // インスタンスの生成 var hoge.a = 1; // プロパティ定義 // リテラル var fuga = {a: 1};
JavaScriptのオブジェクトはkey-valueで表されたプロパティを格納したな順序のないデータ構造です。 Perlと似てますね。
ビルトインの参照型として、
- Object
- Array
- Date
- Error
- Function
- RegExp
が提供されています。
Objectを調べる関数群
ここ以降、頻繁に使うので、まとめます。
// instanceof // オブジェクトがその型に属しているかを確認 var f = function(a) { console.log(a) }; f instanceof Function // => true f instanceof Object // => true // obj.constructor // オブジェクトのコンストラクタを返す console.log(obj.constructor); // Object.getPrototypeOf(obj) // オブジェクトのプロトタイプを返す // obj.__proto__ と一緒 // obj.hasOwnProperty(プロパティ名) var a = { hoge : 1 }; a.hasOwnProperty('hoge'); // => true a.hasownproperty('toString'); // => false
Function
JavaScriptの関数はFunction型のオブジェクトです。つまりインスタンスとして生成できますが、その他にいくつかのリテラルが提供されています。
// 関数宣言(function declaration)の書式のリテラル function hoge() { console.log("hello"); } //上と同義 var hoge = new Function("console.log(\"hello\");"); // さらに関数式(function expression)と呼ばれるリテラルもある var hoge = function () { console.log("hello"); }; hoge instanceof Function // => true hoge instanceof Object // => true
Perlと同じく、変数に代入できたり、関数コール時の引数として扱えます。(first-class object) 関数はオブジェクトであり、内部プロパティCallを持っていて、オブジェクトが実行可能であることを示します。
※内部プロパティとは、ECMAScript標準で定められている、オブジェクトが内部で保持しているプロパティ。2重ブラケットで表す慣習。
先ほど、JavaScriptのオブジェクトは、Perlとすごく似ていると言いましたが、 オブジェクトが持つ手続き、つまりメソッドもプロパティとしてほかのプロパティと同様に定義されている点が異なります。
new
一言でいうと関数をコンストラクタとしてインスタンスを生成する演算子です。
Constructorとしての関数
先ほどnew Object()
という形式でオブジェクトを生成しました。
Objectはジェネリックなオブジェクト型であり、その実体は関数です。
Object instanceof Function # => true
UpperCamelCaseなのは単なる命名規則に過ぎず、言語処理系で保証しているものではないです。
new 演算子を使って関数を呼び出すことでその関数をコンストラクタに持つオブジェクトを生成できます。
new F()
で呼び出すと内部的に
var instance = new Object();
で得られるような空オブジェクトを作成- instance の内部プロパティPrototypeにF.prototype を設定
- thisをinstanceにした上で、F()を呼び出す
- instanceを返す
という挙動をします。
function Person(name) { this.name = name; } var person1 = new Person("hoge"); var person2 = new Person("fuga"); console.log(person1.constructor); // => [Function: Person] console.log(person2.constructor); // => [Function: Person] //単なる関数呼び出し(new演算子がないとただの関数実行となる。危険) var person3 = Person("piyo"); console.log(person3); // => undefined console.log(name); // => this == Globalになっているため、Globalにnameが設定されてしまっている。グローバル汚染 console.log(person3.constructor); // => error
プロパティチェーン
本発表の肝です。
内部プロパティPrototypeとObject.prototype
全てのオブジェクトは内部プロパティとしてPrototypeを持っています。
上記で説明したnew 演算子の挙動に当てはめると、
var instance = new Object();
で生成したinstanceの内部プロパティPrototypeには
Object.prototypeが参照されています。
ECMAScript5標準では、Object.getPrototypeOf(object)でアクセスできます。 JavaScriptの実装系によっては、protoというアクセサでアクセスもできます。(ECMAScript6で標準化の予定)
toStringはだれのプロパティか?
var instance = new Object();
で生成されたオブジェクトはデフォルトでいくつかの関数を持っています。
console.log(instance.toString()); // => [object Object]
for (v in instance) { // 自分のプロパティを列挙 console.log(v + ": " + instance[v]); } // 何もない
オブジェクトはプロパティ呼び出しを受けると、
- 自身のプロパティを走査
- 自身のプロパティでヒットしなかった場合は、内部プロパティPrototypeに設定されているオブジェクトを走査
- 内部プロパティPrototypeに設定されているオブジェクトにない場合は、「内部プロパティPrototypeに設定されているオブジェクト」の内部プロパティPrototypeに設定されているオブジェクトを走査
- 以下見つかるまで繰り返し...
という挙動をします。 これをプロトタイプチェーンと呼びます。 プロトタイプチェーンは、内部オブジェクトはPrototypeがnullになるまで、続きます。
- 全てのオブジェクトはObject.prototypeを継承するのか?
- No.
var nakedObject = Object.create(null); // Object.create()は第1引数にもらったオブジェクトを[[Prototype]]にもつオブジェクトを生成する var NakedObject = function () {}; NakedObject.prototype = null; var obj = new Nakedobject(); obj.prototype // => Object(); // newの挙動が、コンストラクタのprototypeプロパティが設定されていなかったら、Object.prototypeを生成する[[Prototype]]に使う
toStringが誰のプロパティか確認すると、
console.log(instance.hasOwnProperty("toString")); // => false console.log(instance.__proto__.hasOwnProperty("toString")); // => true
↑のように、toStringは自身のプロパティではないですが、Prototypeのプロパティなので、
console.log(instance.toString()); // => [object Object]
というようにプロパティかのように呼べます。
JavaScriptではプロトタイプチェーンを使って継承を実現します。
オブジェクト自身がtoString
を持っている場合は、それがコールされます。
instance.toString = function () { return "original"; }; //toStringプロパティを追加 console.log(instance.toString()); // => original console.log(instance.hasOwnProperty("toString")); // => true console.log(instance.__proto__.hasOwnProperty("toString")); // => true
混乱しやすいですが、とても重要なことは、
instance.prototype
とinstance内部プロパティ[[Prototype]]
は別物
を抑えておくことです。
継承
ここまでくれば、JavaScriptにおけるプロトタイプベースの継承を理解することができます。
function Rectangle(length, height) { this.length = length; this.height = height; } Rectangle.prototype.getArea = function() { return this.length * this.height; } var rect = new Rectangle(2,4); rect.getArea(); console.log(rect.getArea()); // => 8 function Square(size) { this.length = size; this.height = size; } Square.prototype = new Rectangle(); Square.prototype.constructor = Square; var square = new Square(4); console.log(square.getArea()); # => 16
cf.) thisオブジェクト
Function型のオブジェクトは、call()
と apply()
という関数を持っています。
call()
と apply()
は関数型オブジェクトを実行するときのthis
を明示的に指定できます。
var a = { hoge : 1 }; var b = { hoge : 2 }; var func = function(key) { console.log(this[key]) }; // 第1引数にthis, 第2引数に引数 func.call(a, "hoge"); func.call(b, "hoge"); func.apply(a, ["hoge"]); // callとapplyの違いは引数の渡し方
逆説的ですが、obj.func()
はobj.func.apply(obj)
のシンタックスシュガーと考えるのが、
一番理解しやすかったです。
つまり、thisオブジェクトとは、ある関数が呼び出された際にその関数を呼び出した(メッセージを受け取った)オブジェクトを指します。
var hoge = { value: 1, getValue: function() { console.log(this.value); // # => 1 thisはhoge function getValue() { //getValueはどのオブジェクトにも格納されていない console.log(this.value); // # => undefined thisはGlobal (ブラウザだとwindow) } getValue(); } }; hoge.getValue();
※JavaScriptにブロックスコープはないです。
var hoge = 1; { var hoge = 2; } console.log(hoge); // => 2
※関数スコープによって、隠蔽できます。
ECMAScript6のトピック
https://teppeis.github.io/run-through-es6/#1 に記載。
話さなかったが、残っているトピック
- カプセル化
- どうしても隠蔽したいときのハック