【JavaScript 上級講座】プロトタイプチェーンとオーバーライド
本講座では、プロトタイプチェーンをメソッドの観点から解説する。 前回の講座の冒頭で、次のようなプログラムを掲載した。
002//コンストラクタ関数
003var Item = function () { ;
004 this.move = function () {
005 alert ( "item move" ) ;
006 }
007}
008//prototypeに定義
009Item.prototype.run = function () {
010 alert ( "item run" ) ;
011}
012</script>
Itemコンストラクタ関数を定義して、move()関数とrun()関数を定義する。 move()関数はItemコンストラクタ関数内に定義する。 run()関数は、Itemコンストラクタ関数のprototypeオブジェクトに追加する。 一般的に、run()関数の定義が推奨される。
なぜ?
その理由を、本講座で学習する。
2つのコンストラクタ関数に、プロトタイプチェーンを構築する
全講座で学習した通り、 コンストラクタ関数とObjectコンストラクタ関数は、 __proto__とprototypeで繋がっている。 いま、独自に2つのコンストラクタ関数を定義する。 2つのコンストラクタ関数は、それぞれObjectコンストラクタ関数と繋がっている。 しかし、2つのコンストラクタ関数同士は繋がっていない。 今回、2つのコンストラクタ関数にプロトタイプチェーンを構築する (__proto__とprototypeを繋げる)処理を見る。
002//コンストラクタ関数
003var Item = function () { };
004
005//コンストラクタ関数
006var Iori = function () { } ;
007
008//プロトタイプチェーンを構築
009Object.setPrototypeOf(
010 Iori.prototype ,
011 Item.prototype
012) ;
013//[true]
014alert (
015 Iori.prototype.__proto__
016 ===
017 Item.prototype
018) ;
019</script>
プログラムを実行すると「true」と表示する。
3行目でItemコンストラクタ関数を定義する。 6行目でIoriコンストラクタ関数を定義する。 Itemコンストラクタ関数とIoriコンストラクタ関数は繋がっていない。 全く別のプログラムである。 いま、2つのコンストラクタ関数をつなげたい。
9行目で、Object.setPrototypeOf()関数を実行して、 Itemコンストラクタ関数とIoriコンストラクタ関数を繋げる。
Object.setPrototypeOf()関数 プロトタイプチェーンを構築する際、利用される。
Object.setPrototypeOf( A.prototype , B.prototype ) ;
→ A.prototype.__proto__ と B.prototypeが同じになる。
Object.setPrototypeOf()関数を利用すると、 2つのコンストラクタ関数を繋ぐ事ができる。 具体的には、第一引数のprototypeオブジェクトの__proto__が 第二引数のprototypeオブジェクトを参照する。
009Object.setPrototypeOf(
010 Iori.prototype ,
011 Item.prototype
012) ;
013//[true]
014alert (
015 Iori.prototype.__proto__
016 ===
017 Item.prototype
018) ;
9行目で、Object.setPrototypeOf()関数を実行する。 第一引数にIoriコンストラクタ関数のprototypeオブジェクトを指定し、 第二引数にItemコンストラクタ関数のprototypeオブジェクトを指定する。 このプログラムが実行すると、 Ioriコンストラクタ関数のprototypeオブジェクトの__proto__プロパティが Itemコンストラクタ関数のprototypeオブジェクトを参照する。
Iori | Item |
prototype : { __proto__ : { 【参照X】 } } |
prototype : { 【参照X】 } |
Ioriコンストラクタ関数のprototypeオブジェクトの中に、 __proto__が存在する。この__proto__が参照するオブジェクトを 「参照X」と名づける。 この時、Itemコンストラクタ関数のprototypeオブジェクトは、 「参照X」と名づけられたオブジェクトである。
009Object.setPrototypeOf(
010 Iori.prototype ,
011 Item.prototype
012) ;
013//[true]
014alert (
015 Iori.prototype.__proto__
016 ===
017 Item.prototype
018) ;
プログラムの最後で、 Ioriコンストラクタ関数の__proto__と Itemコンストラクタ関数のprototypeを比較する。 「true」と表示する。
オブジェクトと、別定義したコンストラクタ関数とのプロトタイプチェーン
Ioriコンストラクタ関数をnewして作られたオブジェクトと、 別定義したItemコンストラクタ関数との関係を、 プロトタイプチェーンについて解説する。
002//コンストラクタ関数
003var Item = function () { };
004
005//コンストラクタ関数
006var Iori = function () { } ;
007
008//プロトタイプチェーンを構築
009Object.setPrototypeOf(
010 Iori.prototype ,
011 Item.prototype
012) ;
013//Iori型オブジェクト
014var a = new Iori () ;
015
016//[true]
017alert (
018 a.__proto__ ===
019 Iori.prototype
020) ;
021//[true]
022alert (
023 a.__proto__.__proto__ ===
024 Item.prototype
025) ;
026</script>
プログラムを実行すると「true」、「true」と表示する。
3行目でItemコンストラクタ関数を定義し、 6行目でIoriコンストラクタ関数を定義する。 9行目でObject.setPrototypeOf()関数を実行する。 この時、Ioriコンストラクタ関数のprototypeオブジェクトの__proto__プロパティは、 Itemコンストラクタ関数のprototypeオブジェクトを参照する。 14行目でIoriコンストラクタ関数をnewしてオブジェクトを作成し、 変数aへ代入する。
- オブジェクトの中には__proto__がある
- コンストラクタ関数には、prototypeがある
オブジェクト | Iori | Item | |
変数 a |
__proto__ : { 【参照X】 } |
prototype : { 【参照X】 __proto__ : { 【参照Y】 } } |
prototype : { 【参照Y】 } |
変数aが参照するオブジェクトの__proto__プロパティはオブジェクトを参照する。 __proto__プロパティが参照するオブジェクトを「参照X」と名づける時、 Ioriコンストラクタ関数のprototypeオブジェクトは「参照X」と名づけたオブジェクトである。
Ioriコンストラクタ関数のprototypeオブジェクトの __proto__プロパティは、オブジェクトを参照する。 __proto__プロパティが参照するオブジェクトを「参照Y」と名づける時、 Itemコンストラクタ関数のprototypeオブジェクトは「参照Y」である。
017alert (
018 a.__proto__ ===
019 Iori.prototype
020) ;
021//[true]
022alert (
023 a.__proto__.__proto__ ===
024 Item.prototype
025) ;
17行目と22行目は「true」と表示する。 22行目は、前回で学習したとおりである。
- a.__proto__===Iori.prototype
- Iori.prototype.__proto__===Item.prototype
「1」の「a.__proto__」の部分を「2」へ代入する。
- a.__proto__.__proto__===Item.prototype
このプログラムが成立する。
プロトタイプチェーンとメソッド
いまさらではあるが、プロトタイプチェーンの機能を今ここで学習する。
002//コンストラクタ関数
003var Item = function () { };
004
005//prototypeにmove()関数を設定
006Item.prototype.move = function () {
007 alert ( "item move" ) ;
008}
009//コンストラクタ関数
010var Iori = function () { } ;
011
012//プロトタイプチェーンを構築
013Object.setPrototypeOf(
014 Iori.prototype ,
015 Item.prototype
016) ;
017//Iori型オブジェクト
018var a = new Iori () ;
019
020//[item move]
021a.move () ;
022</script>
プログラムを実行すると「item move」と表示する。
プロトタイプチェーンの実践的サンプルである。 解説する。3行目でItemコンストラクタ関数を定義する。 6行目で、Itemコンストラクタ関数のprototypeオブジェクトに、 move()関数を定義する。先へ進む。
10行目でIoriコンストラクタ関数を定義して、 13行目で、Object.setPrototypeOf()関数を実行する。
オブジェクト | Iori | Item | |
変数 a |
__proto__ : { 【参照X】 } |
prototype : { 【参照X】 __proto__ : { 【参照Y】 } } |
prototype : { 【参照Y】 move : function } |
メモリ上、このような状態になる。Itemコンストラクタ関数のprototypeオブジェクトに注目する。 プログラムの6行目で、Itemコンストラクタ関数のprototypeオブジェクトにmove()関数を定義した。 図表の一番右側に、move()関数が定義されている。
021a.move () ;
サンプルプログラムの21行目で、変数aが参照するオブジェクトに対して、 move()関数を実行する。 変数aのオブジェクトは、move()関数を持っていない。 しかしこのプログラムは正常に動作し、「item move」とダイアログに表示する。 なぜだろうか?そもそも、文字列「item move」はなんだろうか?
オブジェクト | Iori | Item | |
変数 a |
__proto__ : { 【参照X】 } |
prototype : { 【参照X】 __proto__ : { 【参照Y】 } } |
prototype : { 【参照Y】 move : function } |
変数aが参照するオブジェクトにmove()関数は存在しない。 JavaScriptでは、オブジェクトに対して存在しない関数を実行した時、 プロトタイプチェーンをさかのぼり、 プロトタイプチェーン上から関数を探し出す。
プロトタイプチェーンの機能 存在しない関数が呼ばれた時、 プロトタイプチェーンをさかのぼり、 プロトタイプチェーン上から関数を探す。
プロトタイプチェーンの機能である。 オブジェクトに対して関数が呼ばれた時、 オブジェクトが関数を持っていなくても、 プロトタイプチェーン上から関数を探して実行する。
オブジェクト | Iori | Item | |
変数 a |
__proto__ : { 【参照X】 } |
prototype : { 【参照X】 __proto__ : { 【参照Y】 } } |
prototype : { 【参照Y】 move : function } |
- a.__proto__.__proto__===Item.prototype
変数aが参照するオブジェクトは、move()関数を持っていない。 この時、Ioriコンストラクタ関数のprototypeオブジェクトを見る。 無い。move()関数が無い。 その場合、さらにプロトタイプチェーンをさかのぼる。 Itemコンストラクタ関数のprototypeオブジェクトを見る。 有る。Itemコンストラクタ関数のprototypeオブジェクト内に、 move()関数が存在する。 move()関数が見つかったので、この関数が実行する。
002//コンストラクタ関数
003var Item = function () { };
004
005//prototypeにmove()関数を設定
006Item.prototype.move = function () {
007 alert ( "item move" ) ;
008}
009//コンストラクタ関数
010var Iori = function () { } ;
011
012//プロトタイプチェーンを構築
013Object.setPrototypeOf(
014 Iori.prototype ,
015 Item.prototype
016) ;
017//Iori型オブジェクト
018var a = new Iori () ;
019
020//[item move]
021a.move () ;
022</script>
21行目のプログラムは、つぎのように書き換える事ができる。
002//コンストラクタ関数
003var Item = function () { };
004
005//prototypeにmove()関数を設定
006Item.prototype.move = function () {
007 alert ( "item move" ) ;
008}
009//コンストラクタ関数
010var Iori = function () { } ;
011
012//プロトタイプチェーンを構築
013Object.setPrototypeOf(
014 Iori.prototype ,
015 Item.prototype
016) ;
017//Iori型オブジェクト
018var a = new Iori () ;
019
020//[item move]
021a.__proto__.__proto__.move () ;
022</script>
21行目を変更した。変数aのオブジェクトは、確かにmove()関数に繋がっている。 これが、プロトタイプチェーンである。
プロトタイプチェーンとオーバーライド
プロトタイプチェーン上に存在する関数の動作を変更したい場合、 prototypeオブジェクトに変更する関数と同じ名前で関数を作成する。
002//コンストラクタ関数
003var Item = function () { };
004
005//prototypeにmove()関数を設定
006Item.prototype.move = function () {
007 alert ( "item move" ) ;
008}
009//コンストラクタ関数
010var Iori = function () { } ;
011
012//プロトタイプチェーンを構築
013Object.setPrototypeOf(
014 Iori.prototype ,
015 Item.prototype
016) ;
017//prototypeにmove()関数を設定
018Iori.prototype.move = function () {
019 alert ( "Run IORI" ) ;
020}
021//Iori型オブジェクト
022var a = new Iori () ;
023
024//[Run IORI]
025a.move () ;
026</script>
プログラムを実行すると「Run IORI」と表示する。
プログラムをざっと解説する。 3行目でItemコンストラクタ関数を定義して、 6行目でmove()関数をprototypeオブジェクトへ追加する。 10行目でIoriコンストラクタ関数を定義する。 13行目で、Object.setPrototypeOf()関数を実行して、 Itemコンストラクタ関数とIoriコンストラクタ関数を プロトタイプチェーンで繋げる。解説を進める。 18行目でIoriコンストラクタ関数のprototypeオブジェクトに、 move()関数を追加する。22行目でIoriコンストラクタ関数をnewして、 変数aへオブジェクトを代入する。
オブジェクト | Iori | Item | |
変数 a |
__proto__ : { 【参照X】 } |
prototype : { 【参照X】 move : function __proto__ : { 【参照Y】 } } |
prototype : { 【参照Y】 move : function __proto__ : { } } |
025a.move () ;
プログラムの25行目で、変数aのオブジェクトに対してmove()関数を実行する。 現在、プロトタイプチェーン上にmove()関数が2つある。
実行する関数 オブジェクトに関数が存在しない場合、プロトタイプチェーンをさかのぼり、 一番最初に見つけた関数が実行する。
一番最初に見つけた関数が実行する。この場合、 Ioriコンストラクタ関数のprototypeオブジェクトに定義したmove()関数が実行する。 プログラムの解説は以上である。
では、まとめる。 Ioriコンストラクタ関数はItemコンストラクタ関数を利用したいため、 プロトタイプチェーンを構築した。
こんかい、Itemコンストラクタ関数にmove()関数しか存在しないので、 プロトタイプチェーンを構築する利点は無いように見える。 しかし、 もしItemコンストラクタ関数に便利な関数が存在するなら、 Itemコンストラクタ関数とプロトタイプチェーンを構築する事を意味のある事である。
さて、Itemコンストラクタ関数に定義された関数の内、 その動作を変更したい関数があると仮定する。 こんかいは、move()関数の動作を変更させたかった。
その場合、Ioriコンストラクタ関数の中で同じ名前の関数を定義すれば良い。 なぜなら、オブジェクトに関数が存在しない場合、 プロトタイプチェーンをさかのぼり、一番最初に見つけた関数を実行するからである。 Ioriコンストラクタ関数が生成したオブジェクトに対して関数を実行した場合、 Itemコンストラクタ関数ではなく、Ioriコンストラクタ関数に定義された関数を利用する。 これがJavaScriptのプロトタイプベースのオーバーライドである。
プロトタイプチェーンの必要性
プロトタイプチェーンの学習は、以下のプログラムから始まった。
002//コンストラクタ関数
003var Item = function () { ;
004 this.move = function () {
005 alert ( "item move" ) ;
006 }
007}
008//prototypeに定義
009Item.prototype.run = function () {
010 alert ( "item run" ) ;
011}
012</script>
Itemコンストラクタに、move()関数とrun()関数を定義する。 一般的にrun()関数の定義が推奨される。 その理由を述べる。
002//コンストラクタ関数
003var Item = function () { ;
004 this.move = function () {
005 alert ( "item move" ) ;
006 }
007}
008//コンストラクタ関数
009var Iori = function () { } ;
010
011//プロトタイプチェーンを構築
012Object.setPrototypeOf(
013 IORI.prototype ,
014 Item.prototype
015) ;
016//Iori型オブジェクト
017var iori = new Iori () ;
018
019//[ERROR]
020iori.move () ;
021</script>
3行目でItemコンストラクタ関数を定義する。 4行目で、Itemコンストラクタ関数内にmove()関数を定義する。 このmove()関数は、プロトタイプチェーン上に存在しない。
9行目でIoriコンストラクタ関数を定義して、12行目でItemコンストラクタ関数と プロトタイプチェーンを構築する。 17行目でIoriコンストラクタ関数をnewして、変数ioriへ代入する。
オブジェクト | Iori | Item | |
変数 iori |
__proto__ : { 【参照X】 } |
prototype : { 【参照X】 __proto__ : { 【参照Y】 } } |
move : function prototype : { 【参照Y】 __proto__ : { } } |
020iori.move () ;
20行目で変数ioriが参照するオブジェクトに対してmove()関数を実行する。 変数ioriが参照するオブジェクトにmove()関数は存在しない。 そのため、プロトタイプチェーンをさかのぼる。 Ioriコンストラクタ関数のprototypeオブジェクトの中にも、 move()関数は存在しない。 さらに、プロトタイプチェーンをさかのぼる。 Itemコンストラクタ関数のprototypeオブジェクトを見る。 Itemコンストラクタ関数の中にmove()関数は定義されている。 しかし、prototypeオブジェクトの中に、move()関数は存在しない。 そのため、move()関数を見つける事ができない。
ここで「ERROR」が発生する。以上がプログラムの解説である。
では、プログラミング設計の観点からプログラムを解説する。
002//コンストラクタ関数
003var Item = function () { ;
004 this.move = function () {
005 alert ( "item move" ) ;
006 }
007}
008//コンストラクタ関数
009var Iori = function () { } ;
010
011//プロトタイプチェーンを構築
012Object.setPrototypeOf(
013 IORI.prototype ,
014 Item.prototype
015) ;
016//Iori型オブジェクト
017var iori = new Iori () ;
018
019//[ERROR]
020iori.move () ;
021</script>
Itemコンストラクタ関数内にmove()関数を定義している。 このmove()関数はプロトタイプチェーン上に存在しないので、 他のコンストラクタ関数がプロトタイプチェーンを構築しても、 意味の無い関数である。つまり、move()関数について言うなら、 拡張性が無い関数と言える。
自分が定義したコンストラクタ関数の関数を、 別のコンストラクタ関数にも使ってほしい場合、 その関数をprototypeオブジェクトに追加する。 これにより、別のコンストラクタ関数がその関数を利用できる。
まとめ
Object.setPrototypeOf()関数 2つのオブジェクトを繋げる。
Object.setPrototypeOf( A.prototype , B.prototype ) ;
→ A.prototype.__proto__ と B.prototypeが同じになる。
プロトタイプチェーンの機能 存在しない関数が呼ばれた時、 プロトタイプチェーンをさかのぼり、 プロトタイプチェーン上から関数を探す。
実行する関数 オブジェクトに関数が存在しない場合、プロトタイプチェーンをさかのぼり、 一番最初に見つけた関数が実行する。