Skip to content

Instantly share code, notes, and snippets.

@ukyo
Last active October 6, 2015 11:57
Show Gist options
  • Star 12 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save ukyo/2990054 to your computer and use it in GitHub Desktop.
Save ukyo/2990054 to your computer and use it in GitHub Desktop.
About Javascript prototype

#プロトタイプと、あとクラス、継承、ミックスインと呼ばれている物の説明

この文書はなるべく正確な情報を書きたいのでちょくちょく更新されます。あと、ちょくちょくキャラが変わるのは気にしないでください。修正した部分に関する情報は コメント やリビジョンを参照してください。

プリミティブ型とオブジェクト型

Javascriptの変数にはプリミティブ型とオブジェクト型が存在する。

###プリミティブ型

数字、文字列、真偽値、undefined、nullの5つ。値型。

###オブジェクト型

上で書いた型以外の型。参照型。いわゆるハッシュというデータ構造。

//fooオブジェクトを持つhogeプロパティを作る例
var foo = {
  hoge: 1
};

foo.hoge === foo['hoge']; //同じ

####プロパティの属性

実際のところプロパティは以下のような属性をもっている。

属性名説明
`value` 全ての型プロパティの実際の値
`get` 関数か `undefined` プロパティを読み込んだときに `get` に関数が登録されているのならそれが呼ばれる
`set` 関数か `undefined` プロパティに値を書き込むときに `set` に関数が登録されているのならそれが呼ばれる
`writable` `boolean` 書き換え可能かどうか
`enumerable` `boolean` `for in` で列挙されるかどうか
`configurable` `boolean` プロパティの削除、属性の変更、データプロパティからアクセッサープロパティへ(後述)の変更ができなくなる

さらにプロパティが持つ属性によってデータプロパティ、アクセッサープロパティ(と、隠しプロパティというものもある)に分類できる。

  • データプロパティが持つ属性: value , writable , enumerable , configurable
  • アクセッサープロパティが持つ属性: get , set , enumerable , configurable

ちなみに valueget のような両方にまたがる属性を持つプロパティを作ることはできない。

####プロパティを操作する関数

Object オブジェクトにはプロパティを操作する便利な関数がたくさん登録されている。例えば Object.getOwnPropertyDescriptor でオブジェクト自身のプロパティの属性を調べたり、 Object.defineProperty で属性を設定しながらプロパティの追加ができる。

var foo = {};

foo.bar = 1;
//fooオブジェクトのbarプロパティの属性を調べる
Object.getOwnPropertyDescriptor(foo, 'bar');
/*
{
  value: 1,
  writable: true,
  enumerable: true,
  configurable: true
}
*/

//fooオブジェクトにhogeプロパティを属性を設定しながら追加する
Object.defineProperty(foo, 'hoge', {
  value: 3,
  writable: true,
  enumerable: false,
  configurable: true
});

他のプロパティを操作する方法やその他の詳しい情報については以下を参考にして欲しい。

##プロトタイプ

プロトタイプとはオブジェクト型の変数が持つ隠しプロパティで、通常は読み書きすることができないが、 foo.__proto__Object.getPrototypeOf(foo) で読み書きすることができる。

var foo = {};
var bar = {hoge: 1};

foo.__proto__ = bar;
Object.getPrototypeOf(foo) === bar; //true

また foo = Object.create(bar) でプロトタイプが bar オブジェクトの foo オブジェクトを作ることができる。

var bar = {hoge: 1};
var foo = Object.create(bar);

Object.getPrototypeOf(foo) === bar; //true

###プロトタイプチェーン

上の例では foo.hoge は1を返す。 foo 自身には hoge というプロパティがないにもかかわらず。この例で foo.hoge が1を返すまでどうやって処理を行なっているのかを見てみよう。

  1. foohoge というプロパティがあるかチェックする
  2. なかった! foo のプロトタイプである bar を調べよう
  3. barhoge というプロパティがあるかチェックする
  4. あった! bar のプロパティ hoge を返そう(1が返る)

このようにプロトタイプを辿ることをプロトタイプチェーンと呼ぶ。 プロトタイプチェーンは再帰的に書ける(オブジェクト o からプロパティ名が name というプロパティを探すとして)

  1. o 自身に name という名前のプロパティが存在するか
  • ある : o[name] を返す
  • ない :
    • プロトタイプがオブジェクト型 : o のプロトタイプに対して1.からやり直す
    • null : undefined を返す

コードにすると以下のようになる。

function getProp(o, name) {
  //oにnameが存在するか
  if(o.hasOwnProperty(name)) { //ある
    return o[name];
  } else { //ない
    var proto = Object.getPrototypeOf(o);
    var type = typeof proto;
    if(proto !== null && type === 'function' || type === 'object') { //プロトタイプがオブジェクト型
      //`o` のプロトタイプに対して1.からやり直す
      return getProp(proto, name);
    } else { //それ以外
      return undefined;
    }
  }
}

###プロトタイプチェーン上のthis

あるオブジェクト o のプロトタイプチェーン上のプロパティに関数 f が存在したときに o.f()f を読み込んだ時のfの中で使っている thiso

var oo = {
  f: function() {
    return this;
  }
};

var o = Object.create(oo);

o === o.f(); //true

##関数オブジェクト

Function型のオブジェクト(以下、関数オブジェクト)は以下のようにして定義することができる。

//function文を用いた定義
function foo() {
  //何か
}

//無名関数を用いた定義
var bar = function () {
  //何か
};

//無名関数には名前をつけることができる
var baz = function baz() {
  //何か
};

//Functionをnewする
var hoge = new Function();

###prototypeプロパティ

関数オブジェクトは prototype という特別なプロパティを持つ。この prototype というプロパティは上で説明したプロトタイプとは違うものだということに注意してほしい。ちなみに実際の関数オブジェクトのプロトタイプは Function.prototype を指す。

Object.getPrototypeOf(foo) === foo.prototype; //false
Object.getPrototypeOf(foo) === Function.prototype: //true

さらに、この prototypeconstructor というプロパティを持ち、それは関数オブジェクト自体を指す。

foo.prototype.constructor === foo; //true

###Function.prototypeの便利な関数

関数オブジェクトのプロトタイプは Function.prototype なので、そこに登録されている便利な関数を使うことができる。ここでは call , apply , bind について紹介する。

####call

call は理解しにくのでとりあえず具体的な例を見て欲しい。

var o = {
  hoge: 1,
  fuga: 2
};

//プロトタイプチェーン上にhogeやfugaプロパティは無いはず
var foo = function(x, y) {
  return this.hoge + this.fuga + x + y;
};

//第一引数に渡されたオブジェクトoがfooの中のthisとして、
//第二引数以降の引数がfooの引数として使われている
foo.call(o, 3, 4) === 10 //true

処理の流れは以下のようになる。

  1. foo がプロトタイプチェーン上に存在する call 関数を呼び出す
  2. プロトタイプチェーン上の thisfoo を指すので call 関数は foo を呼び出す関数に決定する
  3. 第一引数 o を呼び出す関数の this を指すことにする
  4. 第二引数以降を呼び出す関数の引数とする
  5. 実際に呼び出す

####apply

call は第二引数以降を呼び出す関数の引数に対して、 apply は第二引数に配列を入れてそれを引数とする。

//同じ
foo.call(o, 1, 2);
foo.apply(o, [1, 2]);

####bind

TODO

##名状しがたきクラスのようなもの

Javascriptでは new と関数オブジェクトを使っていわゆるクラス的なものからインスタンス的なものを作るようなことができる。このとき関数オブジェクトはコンストラクタ、 prototype が持つプロパティはメソッド的な役割を果たす。

//コンストラクタ
var Animal = function(name) {
  this.name = name;
};

//メソッド
Animal.prototype.walk = function() {
  console.log(this.name + ' is walking.');
};

//newを使ってAnimalのインスタンスを作る
var pochi = new Animal('pochi');

pochi.walk(); //pochi is walkingと表示される

new で生成された pochi というオブジェクトはオブジェクト自体に name 、プロトタイプに walkconstructor というプロパティを持つ。ちなみにオブジェクト自身が持つプロパティは Object.getOwnPropertyNames 関数で調べることができる。

Object.getOwnPropertyNames(pochi); //["name"]
Object.getOwnPropertyNames(Object.getPrototypeOf(pochi)); //["walk", "constructor"]

似たものとしては Object.keys という関数があり、こちらはオブジェクト自身が持つプロパティの中で、enumerable 属性が true のプロパティ名だけを返す。

Object.keys(Object.getPrototypeOf(pochi)); //["walk"]

###実際にnewがしていること

ある関数オブジェクト F があったとしたとき new F は次のような処理を行なう。

  1. プロトタイプが F.prototype であるオブジェクト o をつくる
  2. o のコンテキスト(F の中で使っている thiso ということにすること)で F を呼ぶ
  3. o を返す

コードにすると

function new_(F) {
  return function() {
    var o = Object.create(F.prototype);
    F.apply(o, arguments);
    return o;
  };
}

var Foo = function(a, b, c) {
  this.a = a;
  this.b = b;
  this.c = c;
};

Foo.prototype.sum = function() {
  return this.a + this.b + this.c;
};

//同じ
var foo1 = new Foo(1, 2, 3);
var foo2 = new_(Foo)(1, 2, 3);

##名状しがたき継承のようなもの

上で説明した知識を使うとクラスベースのオブジェクト指向言語的な継承を行うことができる。

###最も簡単な例

最も簡単に継承を実現するには子にしたい関数オブジェクトの prototype に親にしたい関数オブジェクトを new したものを代入すればいい。

var A = function() {
  this.hoge = 'a';
};

A.prototype.walk = function() {
  return this.hoge + ' is walking';
};

var B = function() {
  this.hoge = 'b';
};
//子にしたい関数オブジェクトのprototypeに親にしたい関数オブジェクトをnewしたものを代入する
B.prototype = new A();

var a = new A();
var b = new B();
a.walk(); //'a is walking'
b.walk(); //'b is walking'

//プロトタイプチェーンを確認してみよう
Object.getPrototypeOf(b) === B.prototype;
Object.getPrototypeOf(B.prototype) === A.prototype;

b のプロトタイプは B.prototype で、 B.prototype のプロトタイプは A.prototype なので確かに b.walk() は問題なく呼べることが確認できる。 ただし、この方法だと以下のような問題が生じる。

  1. 引数をチェックしてエラーを出すような関数オブジェクトを親にすることができない
  2. 子の関数オブジェクトの prototypeconstructor プロパティが消える

###改良版

上の問題を解決する inherits 関数を作ってみよう。

function inherits(child, parent) {
  var F = function() {};
  F.prototype = parent.prototype;
  child.prototype = new F();
  Object.defineProperty(child.prototype, 'constructor', {
    value: child,
    configurable: true,
    enumerable: false,
    writable: true
  });
}

var A = function(hoge) {
  if(typeof hoge === 'undefined') throw new Error('hoge is undefiend!');
  this.hoge = hoge;
};

A.prototype.walk = function() {
  return this.hoge + ' is walking';
};

var B = function() {
  this.hoge = 'b';
};

inherits(B, A);

var a = new A();
var b = new B();

a.walk(); //'a is walking'
b.walk(); //'b is walking'

この方法では一度ダミーの関数オブジェクトを作り、親の関数オブジェクトの prototype をコピーすることにより1.の問題を解決する。 さらに child.prototype = new F() したあとに 子の prototypeconstructor プロパティを追加することで2.の問題を解決する。

###inherits関数の改良版1

そういえば Object.createparent.prototype をプロトタイプにしたオブジェクトを作れたので、 inherits 関数は以下のように書き換えられる。

function inherits(child, parent) {
  child.prototype = Object.create(parent.prototype);
  Object.defineProperty(child.prototype, 'constructor', {
    value: child,
    configurable: true,
    enumerable: false,
    writable: true
  });
}

###親コンストラクタを呼ぶ

例えば new するときに引数を指定することがよくある。今までの知識で継承を実現しようとするとこのようなる。

var A = function(a, b, c) {
  this.a = a;
  this.b = b;
  this.c = c;
};

var B = function(a, b, c) {
  this.a = a;
  this.b = b;
  this.c = c;
};

inherits(B, A);

親と子のどちらにも同じことが書いてあって無駄だし、親の中身を変更したら子の方も変更しなければならない。例えばJavaだとsuperで親コンストラクタを呼べるがJavascriptには今のところそういう構文が無いのでできない。しかし callapply を使えば擬似的に親コンストラクタを呼ぶような動作を実現することができる。

var A = function(a, b, c) {
  this.a = a;
  this.b = b;
  this.c = c;
};

var B = function() {
  A.apply(this, arguments);
};

/*
callの例

var B = function(c) {
  A.call(this, 1, 2);
  this.c = c;
};
*/

inherits(B, A);

###親のメソッドを呼ぶ

同じようにJavaで言うところの親のメソッドを呼ぶようなこともできる。

var A = function(hoge) {
  this.hoge = hoge;
};

A.prototype.walk = function() {
  console.log(this.hoge + ' is walking');
};

var B = function() {
  A.call(this, 'b');
};

inherits(B, A);

B.prototype.walk = function() {
  A.prototype.walk.call(this);
  console.log(this.hoge + 'は歩いています');
};

var a = new A('a');
var b = new B();

a.walk(); //'a is walking'
b.walk(); //'b is walking'と'bは歩いています'

実現はできたが、親自体を変えたときのコストが高そうだ。

###inherits関数の改良版2

inherits 関数を実行したときに子の関数オブジェクトが親の関数オブジェクトを参照できるようにすればこの問題を解決できる。

function inherits(child, parent) {
  child.prototype = Object.create(parent.prototype);
  Object.defineProperty(child.prototype, 'constructor', {
    value: child,
    configurable: true,
    enumerable: false,
    writable: true
  });
  child.__super__ = parent.prototype; //__super__というプロパティから親のprototypeプロパティを読み込める
}

var A = function(hoge) {
  this.hoge = hoge;
};

A.prototype.walk = function() {
  console.log(this.hoge + ' is walking');
};

var B = function() {
  B.__super__.constructor.call(this, 'b');
  //関数オブジェクトのprototypeプロパティのconstructorプロパティは
  //その関数オブジェクト自身という性質を利用する
};

inherits(B, A);

B.prototype.walk = function() {
  B.__super__.walk.call(this);
  console.log(this.hoge + 'は歩いています');
};

var a = new A('a');
var b = new B();

a.walk(); //'a is walking'
b.walk(); //'b is walking'と'bは歩いています'

ちなみにcoffeescriptではこれと同じような方法を用いている。

###親の関数オブジェクトから直接子の関数オブジェクトを生成する

親の関数オブジェクトから直接子の関数オブジェクトを生成する extend 関数を作ってみよう。

function extend(parent, props) {
  parent = parent || Object;
  props = props || {};
  var child = props.__init__ || function(){ parent.apply(this, arguments) }; //省略したら親コンストラクタを呼ぶ関数を登録
  inherits(child, parent);
  Object.keys(props).forEach(function(key) {
    if(key !== '__init__') child.prototype[key] = props[key];
  });
  return child;
}

var A = extend(null, {
  __init__: function(a, b, c) {
    this.a = a;
    this.b = b;
    this.c = c;
  },

  sum: function() {
    return this.a + this.b + this.c;
  }
});

var B = extend(A, {
  mul: function() {
    return this.a * this.b * this.c;
  }
});

var a = new A(1, 2, 3);
var b = new B(2, 3, 4);

上の例では第一引数を親、第二引数に渡すオブジェクトの __init__ というプロパティを子のコンストラクタ、それ以外を子の prototype プロパティのプロパティにしている。

enchant.jsとかがこれと同じだった気がする。

##名状しがたきミックスインのようなもの

今までのいわゆる継承的なもので親のメソッドを呼ぼうとするとプロトタイプを2回辿るので処理が遅くなるという問題がある。もしもメソッドだけ(いわゆるミックスイン的なもの)が欲しいのなら関数オブジェクトの prototype プロパティに直接追加してしまえばよい。

###オブジェクトに関数を入れておいてそれを使いまわす

オブジェクトに関数を入れておいて専用の mixin 関数を使ってミックスインを実現してみよう。

function mixin(proto, methods) {
  Object.getOwnPropertyNames(methods).forEach(function(key) {
    if(key === 'constructor') return; //constructorプロパティは変えないようにする。
    proto[key] = methods[key]; //methodsオブジェクトの内容全てをprotoオブジェクトに追加する。
  });
}

var A = extend(null, {
  __init__: function(a, b, c) {
    this.a = a;
    this.b = b;
    this.c = c;
  }
});

var B = extend(A);

var FooMixin = {
  sum: function() {
    return this.a + this.b + this.c;
  },

  mul: function() {
    return this.a * this.b * this.c;
  }
}

mixin(A.prototype, FooMixin);
mixin(B.prototype, FooMixin);

var a = new A(1, 2, 3);
var b = new B(1, 3, 4);

a.sum(); //6
b.sum(); //8
a.mul(); //6
b.mul(); //12

###ミックスインするためだけの関数を作る

call を駆使ことによってミックスインするためだけの関数を作ることもできる。

function FooMixin() {
  function sum() {
    return this.a + this.b + this.c;
  }

  function mul() {
    return this.a * this.b * this.c;
  }

  this.sum = sum;
  this.mul = mul;
}

FooMixin.call(A.prototype);
FooMixin.call(B.prototype);

//ただしA.prototype.sumとB.prototype.sum (mulも)は違うもの
A.prototype.sum === B.prototype.sum //false

###ミックスインするためだけの関数(キャッシュ版)

クロージャを使ってキャッシュしてみよう。

var FooMixin = (function() {
  function sum() {
    return this.a + this.b + this.c;
  }

  function mul() {
    return this.a * this.b * this.c;
  }

  return function() {
    this.sum = sum;
    this.mul = mul;
  };
})();

FooMixin.call(A.prototype);
FooMixin.call(B.prototype);

A.prototype.sum === B.prototype.sum //true

キャッシュできるということは直接プロパティに追加してもメモリ負荷が少なくなり、さらにプロトタイプを辿る必要がなくなるので動作も速くなる(頻繁に new するものだと逆に遅くなるかもしれない)。パフォーマンスの章を参照。

var A = function(a, b, c) {
  this.a = a;
  this.b = b;
  this.c = c;
  FooMixin.call(this);
};

var a = new A(1, 2, 3);
Object.keys(a); //["a", "b", "c", "sum", "mul"] 順番はこうならないかもしれない

##パフォーマンス

###考察

  • 頻繁に new するものはクラス的な継承一択
  • あまり new しないものについては、ブラウザによってキャッシュされたミックスインを使ったほうが良い場合がある
  • プロトタイプチェーンが意外に速い
  • Object.create は今のところ微妙
  • というよりか今まで new でやっていたことをわざわざ Object.create でやる必要はない

##おわり

疲れたー。間違ってたらコメントちょーだい。

##参考文献

それぞれの話題のより詳しい情報については以下を参照。

@syoichi
Copy link

syoichi commented Jun 27, 2012

「プロパティを操作する関数」と「参考文献」のES仕様のリンクは、今のところES5.1の公式HTML版の方が良いのではないでしょうか。
また、「オブジェクトに関数を入れておいてそれを使いまわす」のコードの、

  Object.keys(methods).forEach(key) {
    proto[key] = methods[key]; //methodsオブジェクトの内容全てをprotoオブジェクトに追加する。
  };

は、

  Object.keys(methods).forEach(function(key) {
    proto[key] = methods[key]; //methodsオブジェクトの内容全てをprotoオブジェクトに追加する。
  });

ではないでしょうか。

@ukyo
Copy link
Author

ukyo commented Jun 27, 2012

ご指摘ありがとうございます。ES仕様へのリンクを修正しました(正直、あんまり読み込んでないのでいろいろ不備があったら教えて欲しいなぁと思います)。

それとミックスインに関しては Object.getOwnPropertyNames のほうがオブジェクトのプロパティを全てをコピーするという趣旨に適しているかて思ってこれを使うようにしました(ただし constructor を上書きしないように)。

例: Object.keys を使うと以下のようなことができない。

mixin(NodeList.prototype, Array.prototype);

@syoichi
Copy link

syoichi commented Jun 27, 2012

修正されたmixin関数について、continuereturnではないでしょうか。このままですとSyntaxErrorになってしまいます。

@ukyo
Copy link
Author

ukyo commented Jun 27, 2012

あー、間違えてました。修正しました。

@hokaccha
Copy link

名状しがたき継承のようなもの > 改良版 のコードにnewするコードがないようです。

inherits(B, A);

a.walk(); //'a is walking'
b.walk(); //'b is walking'

の部分です。

@ukyo
Copy link
Author

ukyo commented Jul 24, 2012

ありがとうございます。修正しました。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment