Javascriptでオブジェクト指向するときに覚えておくべきこと

  • 672
    いいね
  • 1
    コメント
この記事は最終更新日から1年以上が経過しています。

Javascriptはブラウザのクライアントサイドで動く唯一の言語と言ってもいいので、普段書かなくてもちょいちょい書くことになる。そんな時用に、他の言語使っていると忘れてしまうJavascriptの重要な法則をまとめておく。

基本的にリファレンスにしているのはMozilla Developer Network (MDN)のドキュメントの以下のページ。MDNはJavascript関連では一番ちゃんとしたドキュメントだと信じている。

プロトタイプベース言語

Javascriptはプロトタイプベースのオブジェクト指向言語で、クラスベースのオブジェクト指向言語(例: C++, Java)とは異なる部分が多々ある。
例えば、クラスベース言語はクラスとインスタンスという概念があるのに対し、プロトタイプベース言語はPrototypical Objectというオブジェクトがあるだけ。

表8.1 クラスベース (Java) のオブジェクトシステムとプロトタイプベース (JavaScript) のオブジェクトシステムとの比較

クラスベース (Java) プロトタイプベース (JavaScript)
クラスとインスタンスは異なる実体です 。 すべてのオブジェクトはインスタンスです。
クラス定義を用いてクラスを定義します。また、コンストラクタメソッドを用いてクラスをインスタンス化します。 コンストラクタ関数を用いてオブジェクトのセットを定義および作成します。
new 演算子を用いて単一のオブジェクトを作成します。 同じです。
既存のクラスのサブクラスを定義するクラス定義を用いて、オブジェクト階層を構築します。 コンストラクタ関数に結びつけられたプロトタイプとしてオブジェクトを割り当てることで、オブジェクト階層を構築します。
クラスチェーンに従ってプロパティを継承します。 プロトタイプチェーンに従ってプロパティを継承します。
クラス定義が、クラスの全インスタンスの全プロパティを定義します。実行時に動的にプロパティを追加することはできません。 コンストラクタ関数またはプロトタイプがプロパティの初期セットを指定します。個々のオブジェクトやオブジェクトのセット全体へ動的にプロパティを追加したり、それらからプロパティを削除したりできます。

クラスベース言語とプロトタイプベース言語 - オブジェクトモデルの詳細 - MDN

全てはオブジェクト

Javascriptで全てはオブジェクトで、クラスベース言語でいうところのインスタンスのみだと思えば良い。

JavaScript では、ほぼあらゆるものがオブジェクトです。
null と undefined 以外のすべてのプリミティブ型はオブジェクトとして扱われます。
すべてはオブジェクト - Working with Objects - MDN

オブジェクトはプロパティを持つ

オブジェクトは、変数や関数をプロパティとして持つことができる。

JavaScript のオブジェクトは、自身に関連付けられたプロパティを持ちます。オブジェクトのプロパティは、オブジェクトに関連付けられている変数と捉える事ができます。オブジェクトのプロパティは基本的に、オブジェクトに属するものという点を除いて通常の JavaScript 変数と同じようなものです。
オブジェクトとそのプロパティ - Working with Objects - MDN

オブジェクトの実装は、結局、HashMap(連想配列)

オブジェクトはHashMapなので、簡単な例としては、プロパティを配列の要素アクセスのように呼び出せる。

> "hoge".length
4
> "hoge"['length']
4

JavaScript オブジェクトのプロパティは、ブラケット表記法でもアクセスや設定ができます。オブジェクトは連想配列と呼ばれることがあります。
オブジェクトとそのプロパティ - Working with Objects - MDN

例えば、オブジェクトの宣言方法は、オブジェクト初期化子などと言われているが、結局よくあるHashMapの宣言と同じ。

var myHonda = {color: "red", wheels: 4, engine: {cylinders: 4, size: 2.2}};

オブジェクト初期化子の利用 - Working with Objects - MDN

メソッドと関数

オブジェクトのプロパティになっている関数がメソッド。

メソッドはオブジェクトに関連付けられた関数です。簡単に言えば、オブジェクトのプロパティのうち関数であるものがメソッドです。
メソッドの定義 - Working with Objects - MDN

ただし、メソッドはオブジェクトから参照されているだけで束縛されているわけではない。
よって、オブジェクトのプロパティとして宣言された関数(つまりメソッド)を、グローバルな環境で使えば通常の関数として振る舞う。

this は関数に「渡される」のであって、関数に固定されている訳ではありません。言い換えると、メソッドはそれをメソッドとして持つオブジェクトに束縛されているのではなく、オブジェクトによって参照されているだけです。

function Car(brand) {
   this.brand = brand;
}
Car.prototype.getBrand = function() {
   return this.brand;
}
var foo = new Car("toyota");
println(foo.getBrand());  // "toyota"
var brand = "not a car";
var bar = foo.getBrand;
println(bar());  // "not a car"

メソッドの束縛 - this - MDN

コンストラクタと関数

どの関数でもコンストラクタとして使用でき、new演算子とともに使用することでオブジェクトを作成する。

どの JavaScript 関数もコンストラクタとして使用できます。new 演算子をコンストラクタ関数とともに使用することで、新しいオブジェクトを作成します。
クラスの定義 - オブジェクトモデルの詳細 - MDN

なお、慣習として、コンストラクタとして使う関数は、通常の関数と区別するため、大文字で始める。

継承とプロトタイプチェーン

クラスの継承はプロトタイプチェーンという概念によって実現される。

// Object.prototype のプロトタイプは null です。
// o ---> Object.prototype ---> null
var o = {a: 1};

// 配列は Array.prototype(indexOf、forEachなどのようなメソッドを持っている)から継承します。 
// a ---> Array.prototype ---> Object.prototype ---> null
var a = ["yo", "whadup", "?"];

// 関数は Function.prototype(call、bindなどのようなメソッドを持つ)から継承します。
// f ---> Function.prototype ---> Object.prototype ---> null
function f() {
  return 2;
}

継承とプロトタイプチェーン - MDN

this について

プロトタイプベースだということで、問題になりやすいのがthisが何を指すか。
Qiita上でお二方が、thisは4種類あるよと書いてくれている。それぞれ比較するとわかりやすかった。

完全におんなじことを書いているわけではないが、だいたい同じことを書いている。せっかくなので対応表を書いておく。

Haru39流 vvakame流
メソッド呼び出しパターン 何かに所属している時のthis
関数呼び出しパターン トップレベルのthis
コンストラクタ呼び出しパターン コンストラクタ内のthis
apply,call呼び出しパターン function#apply とか function#call とかで無理矢理変えられた時のthis

このあと、MDNのthisの説明も読むといいかも。

  • apply, callの時(4つ目)は明示的にObject、
  • メソッドの時(1つ目)は暗黙的にObjectを指して、
  • それ以外(2つ目)はグローバルオブジェクトになる。
  • newを使ったコンストラクタのthis(3つ目)については触れられてない (thisではなくnewの説明に書いてある)。