JavaScript のプロトタイプベースについて [やまそうのブログ]

こんにちは。やまそうです。

フロントエンドの開発を主に担当するようになって早1年以上が経ちました。弊社では 主に Vue を使っての開発なので Vue についての知見は蓄えられて来ているのですが、よく Vue と比較されて世界的に最も使われている UI 構築ライブラリである React のことをあまり知らないでいました。

ということで数ヶ月前から今更ながらではありますが React をプライベートで勉強しています。

本日のお題

やまそう個人的に Vue と React を比較すると、React は素直な JavaScript のコードでほぼ開発できてしまうことが大きな違いの一つかなと思っています。
React でコンポーネントを作るときは関数を作り ( クラスでも作れるが非推奨 ) その中で jsx を return するだけであり、jsx も React.createElement の糖衣構文で結局は JavaScript の式なのでロジックから view まで JavaScript のコードで書けてしまうんじゃないかって感じがします。

そこで、JavaScript の理解を深めることで React での開発に役立つのではないか?ということで JavaScript の理解を深めていっている次第でございます。
今回はその第一弾としてまずは JavaScript のプロトタイプベースについてさらっと復習していくような内容で書いていこうと思います。

プロトタイプベースとは

JavaScript はプロトタイプベースのオブジェクト指向言語です。まずはプロトタイプベースとはなんぞやを簡単におさらいしようと思います。

JavaScriptオブジェクト指向の言語であるが、他のオブジェクト指向の言語、例えば Ruby などのクラスベースのオブジェクト指向の言語とは違いプロトタイプベースとなっています。
クラスベースのオブジェクト指向は抽象クラスから具象オブジェクトを生成するのに対して、プロトタイプベースでは抽象クラスが存在せず実態のあるオブジェクトがオブジェクトを直接継承します。その、継承元になったオブジェクトをプロトタイプと言います。

ECMAScript 2015 からクラス構文が使用できるようになりましたが、実際にはプロトタイプベースの継承の仕組みを使ってクラスを表現しています。

JavaScript の型について

型もさらっとおさらいしておきます。
JavaScript は大きくプリミティブ型と構造型の2つの型に分けられます。

プリミティブ型

  • undefined
  • Boolean
  • Number
  • String
  • BigInt
  • Symbol
  • Null
    • 特別なプリミティブ型

構造型

  • Function
    • Function オブジェクト
  • Object
    • その他のオブジェクト

詳しくは以下を参照すると良いです。
JavaScript のデータ型とデータ構造 - JavaScript | MDN

オブジェクトのプロトタイプを調べる

JavaScript はプロトタイプベースなので各オブジェクトにはプロトタイプのオブジェクトがあります。
いくつかのオブジェクトのプロトタイプをたどってみたいと思います。

オブジェクトのプロトタイプを調べるには __proto__ プロパティ、または Object.getPrototypeOf() で確認することが出来ます。__proto__ プロパティでプロトタイプを参照することは非推奨となっていますが、今回は検証するだけなので __proto__ を使って検証します。
なお、動作検証は node 環境でバージョンは 12.19.0 で行っています。14.5 以降のバージョンですと非推奨の __proto__ を使うと挙動が異なるそうなので注意です。

{} ( key と value のペアをもったオブジェクト ) の場合

const obj = {} // 変数 obj に空のオブジェクトを定義
obj.__proto__ // {}
obj.__proto__.__proto__ // null

[] ( 配列 ) の場合

const ary = [] // 変数 ary に空の配列を定義
ary.__proto__ // []
ary.__proto__.__proto__ // {}
ary.__proto__.__proto__.__proto__ // null

"" ( 文字列 ) の場合

const str = "" // 変数 str に空の文字列を定義
str.__proto__ // [String: '']
str.__proto__.__proto__ // {}
str.__proto__.__proto__.__proto__ // null

どうやらオブジェクトのプロトタイプは {} で、そのプロトタイプは null となるようです。
配列であれば [] -> {} -> null
文字列であれば "" -> {} -> null、になります。
どのオブジェクトのプロトタイプをたどっても ( null は除く ) 最終的には {} -> null となります。

オブジェクトはどうやって作られるのか

ここまででオブジェクトはプロトタイプのオブジェクトを継承して存在していることがわかりました。
では、オブジェクトは一体どのように作られているのか。
クラスベースのオブジェクト指向言語である Ruby であれば抽象クラスをインスタンス化して具象オブジェクトが作成されます。

JavaScript ではどうなっているのか。
例えば下のコードではは変数 obj に新しい空の {} を代入しています。

const obj = {} // 変数 obj に空のオブジェクトを定義

{}リテラル表記であり実態は new Object() です。
なので、const obj = {}const obj = new Object() とは同じになります。
Object()JavaScript の関数でコンストラクタ関数です。

コンストラクタ関数とは、その関数に設定されているプロトタイプを継承した新しいオブジェクトを作成する関数のことを言います。
JavaScript の関数 ( Function オブジェクトのインスタンス ) には prototype というプロパティがあります。このプロパティにはコンストラクタ関数が新しいオブジェクトを作成するときに継承元となるオブジェクトが登録されています。
試しに Object の prototype を見てみます。

Object.prototype // {} が返ってくる

new <コンストラクタ関数> とすることで、そのコンストラクタ関数の prototype に入っているオブジェクトを継承元にして新しいオブジェクトを作成しているというわけです。

なので、Object のプロパティである {} を継承して作られた obj の中身のプロパティと、Object.prototype は同じになります。

const obj = {}
obj.__proto__ === Object.prototype // 結果は true

実際は new 演算子が更に色々やってくれているのですが、長くなりそうなので割愛します。 何をしているか詳しくは以下を見ると良いかと思います。
new 演算子 - JavaScript | MDN

ここまでオブジェクトの作成の流れをダラダラと書いてしまいましたが、超簡単にまとめると

  1. new Object() する
  2. Object.prototype を見る
  3. 2 を継承元にして新しくオブジェクトを作る

みたいな流れになります。

この流れはオブジェクトだけでなく、配列であれば Array コンストラクタ関数、文字列であれば String コンストラクタ関数を使ってオブジェクトが作成されます。
ちなみにオブジェクトのコンストラクタ関数は constructor プロパティで確認出来ます。

obj.constructor // [Function: Object]
([]).constructor // [Function: Array]
("").constructor // [Function: String]

ざっくり JavaScript のプロトタイプベースをざっくり復習できました

プロトタイプベースを見てきましたが、これを知ってるから開発現場で役に立つかと言われると、残念ながらおそらく役に立たないんだろうなと。

今の JavaScript 仕様であればプロトタイプを意識しなくても書けそうですし、プロトタイプをいじっていくこともそうそうないのではないかと。

とはいえ、根幹はプロトタイプベースなので知らないよりは知ってて損はないかなと言うことで良しとしようと思います。

最後まで読んでいただきありがとうございました。