BLOGJavaScript中級者を目指すよ②【プロトタイプ -基礎編-】

ヤマダ
  1. HOME
  2. ブログ
  3. フロントエンド
  4. JavaScript中級者を目指すよ②【プロトタイプ -基礎編-】

前回の続き。
今回はプロトタイプとクラスの基礎について。

目次

  1. プロトタイプ(prototype)
    1. 【前提知識】クラス(Class)について
    2. プロトタイプ(prototype)とは
    3. プロトタイプ(prototype)の使い方・メリット
  2. プロトタイプチェーン(継承)について
  3. プロトタイプのルール
    1. データプロパティの場合(例外あり & 後述)
    2. アクセサプロパティ (ゲッターとセッター)の場合
  4. ES6とClass&prototype

プロトタイプ(prototype)

【前提知識】クラス(Class)について

Classをどう書くは一旦置く。
そもそもクラスとはなんぞやを理解してからの方が、プロトタイプを理解しやすかったので、まずはそのまとめから。

クラスとは「設計図」。 意味としては、コンストラクタと同じ。 ただ、あくまでコンストラクタは関数。 クラスの中に、コンストラクタがある、といった構成になる。

コンストラクタについて、振り返ると、↓

もう少しざっくり書くとクラスをnewした瞬間に実行される関数のこと

https://wa3.i-3-i.info/word13646.html

つまり、コンストラクタ(関数)は、クラスに内包された存在になる。

クラスは下記の要素で成り立っている。

  • コンストラクタ
  • プロパティ
  • メソッド

オブジェクト指向の言語は、基本的にこのクラスをグルングルン回して組んでいくイメージ。

ただ、JavaScriptにクラスの概念はない(正確には「なかった」) 。
なので、どうしていたか。

プロトタイプ(prototype)とは

クラスの概念が「なかった」JavaScriptは、プロトタイプベースで表現をしていた。 「プロトタイプ(prototype)」ってなんじゃ。

まずはざっくりと。

  • prototypeとは「オブジェクト」。
  • そして、コンストラクタ(というよりは関数)がもつプロパティ
  • 感覚としては「親」に近い
  • コンストラクタのprototypeがもつプロパティやメソッドは、インスタンスのプロパティやメソッドとして参照することができる

参考記事はこちら

https://qiita.com/takeharu/items/809114f943208aaf55b3
https://uhyohyo.net/javascript/9_2.html

で?って感じ。
よくわからん。実際にどう使うん?
使うメリットは何なのか?

プロトタイプ(prototype)の使い方・メリット

まず前提として、prototypeは関数を生成するときに自動で生成されている(詳しくは後述)。

function hogehoge() {} // hogehoge.prototypeは自動で生成される

そして、こんな感じで使う。

// パターン1
// コンストラクタを定義
function Greeting() {}

// prototypeにプロパティを追加
Greeting.prototype.message = function() {
	alert("おはよう!");
}

// インスタンスを生成
let morning = new Greeting();
morning.message(); // おはよう!

だからなんだって話。下みたいにやってもいいじゃん。

// パターン2
// コンストラクタを定義
function Greeting() {
	this.message = function() {
		alert("おはよう!");
	};
}

//インスタンスを生成
let morning = new Greeting();
morning.message(); // おはよう!

これでも、挙動は同じになる。

だけどこれにはデメリットがある。メモリが無駄に消費されてしまうこと。

// パターン2で起きていること
// ~ 中略 ~
// インスタンスを生成
function Greeting {
	// let this = {};
	this.message = function() {
		alert("おはよう!");
	};
	// return this;
}

この動きについては、newのときに話した通り。
つまり、毎回空オブジェクトを作成し、毎回新しく関数(message)を定義してしまっている。

上記のような簡単なものであればいいが、複雑になればなるほどパフォーマンスに影響が出てしまう。

先述した通り、prototypeは関数(コンストラクタ)生成時に自動で作られる、その親のようなもの。

そして子【 = 関数(コンストラクタ) 】は、親(prototype)のプロパティを共通で使うことができる。

これがメモリ削減に大きな効果がある。

function Greeting(timeZone) {
	this.timeZone = timeZone;
}
Greeting.prototype.message = function() {
	console.log(this.timeZone);
};

let morning = new Greeting("おはよう");
morning.message();

let evening = new Greeting("こんにちは");
evening.message();

// この場合、newをしたときに動きは下記だけになる。
function Greeting(timeZone) {
	// let this = {};
	this.timeZone = timeZone;
	// return this;
}

これによって、無駄な関数を生成せずに済むし、つくられたインスタンス(たとえばmorning)もGreeting関数を使うことができる。

プロトタイプチェーン(継承)について

さきほど触れたプロトタイプの性質について、更に補足する。

まず前提として、prototypeは関数を生成するときに自動で生成されている。

https://doctype.jp/blog/frontend/415/#chapter-3

実は、prototypeをもってるのは関数だけじゃない。

prototypeは全てのオブジェクトがもっているプロパティで、いわば「隠れステータス」みたいなもの。

prototypeにオブジェクトをセットする方法はいくつかあるけど、代表的な方法としては__proto__を使う。

let Yamada = {
	gender: "male",
	age: 25,
};

Yamada.__proto__ = {from: "Shizuoka"}; // Yamadaオブジェクトのprototypeにプロパティ(とその値)をセット

console.log(Yamada.from); // Shizuoka

そして、さらっと実行してしまったけど、大事なことが1つ。

上記の例では、Yamadaオブジェクトにあるはずのないfromプロパティを呼び出したところ、問題なく値が返ってきてる。

ざっくり言ってしまえば、プロトタイプチェーン(継承)とはこの機能を指す。

Yamadaオブジェクトからあるプロパティを読みたいときにそれが見つからなかった場合、JavaScriptには自動的に、そのオブジェクトのprototypeから取得しようとする機能が備わっている。

プロパティだけじゃなくて、メソッドもいけるし、こんな書き方も可能。

let Mind = {
	message() {
		alert("JavaScriptムズい。"); // Mindにメソッドを持たせる。
	}
};

let Yamada = {
	gender: "male",
	age: 25,
	__proto__: Mind // プロトタイプにメソッドをセット
};

console.log(Yamada.gender); // male
Yamada.message(); // JavaScriptムズい。

そして、コンストラクタ関数を使ってプロトタイプを作って、インスタンス生成しながらグルングルン回す~みたいなやり方もある。

まさしく、この記事の初めの方で紹介した方法なので、詳しくは遡ってもらえれば。

プロトタイプのルール

プロトタイプの呼び出され方にはルールがあるので、おさえておく。

まず前提として、プロトタイプはプロパティを読むだけに使われる。

そして、プロトタイプの参照のされ方には2つのパターンがあるので、そこも頭に入れとく。

データプロパティの場合(例外あり & 後述)

書き込みや削除の際には、オブジェクトを直接操作する必要がある。

let Mind = {
	message() {
		// Mindにメソッドを持たせる。
	}
};

let Yamada = {
	__proto__: Mind // プロトタイプにメソッドをセット
};

Yamada.message = function() {
	alert("JavaScriptエグい。"); // オブジェクト本体にメソッドをセット
};

Yamada.message(); //JavaScriptエグい。

この場合、Yamada.message(); はプロトタイプを参照せず、オブジェクト内のメソッドを見に行き、実行している。

アクセサプロパティ (ゲッターとセッター)の場合

ゲッターとセッターってなんじゃって話だけど、まとめだすと本題にいけないので下記をどうぞ。

ゲッターとセッターの定義

https://developer.mozilla.org/ja/docs/Web/JavaScript/Guide/Creating_New_Objects/Defining_Getters_and_Setters

プロパティ getters と setters

https://ja.javascript.info/property-accessors

要約すると、

  • アクセサプロパティと呼ばれる。
  • データプロパティと違い、値を持たない。
  • 一見、普通のプロパティ。
  • しかし、実は関数。
  • アクセサプロパティにアクセスにするには、get / setが必要。

このゲッターとセッター、全く意味がわからんかったんだけど、「Webサービスを作る」をイメージするとめっちゃ分かりやすかった。

  • ユーザー名を引用する…getter
  • ユーザー名を変更する(引数を利用する)…setter

さて、「アクセサプロパティ」と「プロトタイプ」の話に戻る。

この「アクセサプロパティ」の場合、プロパティを読み書きする際には、プロトタイプが参照され、呼び出される。

let Yamada = {
	hanma: "JavaScriptが簡単だと",
	baki: "そんなふうに考えていた時期が俺にもありました",

	set message(value) {
		[this.hanma, this.baki] = value.split(" ");
	},

	get message() {
		return `${this.hanma} ${this.baki}`;
	}
};

let Masaki = {
	__proto__: Yamada,
}

alert(Masaki.message); //引数がないため、getが動く

Masaki.message = "強くなりたくば 喰らえ!!!"; // 引数があるため、setが動く。

alert(Masaki.message); // 強くなりたくば 喰らえ!!!

またこのとき、

  • thisは常にMasakiを見てる(恥ずい)。
  • メソッドが呼び出された際、thisはプロトタイプの影響を全く受けず、現在のオブジェクトで動作。

って点も大事なので理解しておく。

プロトタイプ継承

https://ja.javascript.info/prototype-inheritance

ES6とClass&prototype

ただ、実際のところES6以上の環境下で、prototypeでメソッド生やしたり~で開発することはないっぽい。 Classでいけてしまうので。

ただ、古い技術だから覚えなくていいってわけじゃない。
必要に応じて書く・書く能力は必要だから。
毎回都合よく、モダンな環境で書けるとは限らないし。

それ以前に、クラスで書くにしても、babelでトランスパイルするにしても、あくまでJavaScriptはprototypeベースで動いていることには変わらない。

なので「使う・使わない」とかでなく、その概念自体は覚えておいて損はないと思う。

以上、prototypeの「超」基礎的な話。

続けて、クラス構文の話をするよ。

でも長くなってしまったので、次回に続く。