1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

JavaScriptのPrototypeについて

Last updated at Posted at 2022-07-11

初めに

JavaScriptを勉強し始めてすぐ遭遇する概念ですが、なかなか全般的に理解できないのでそのまま放置してました。(読めるけどうまく使えなかった)最近もう一度JavaScriptを勉強することを機に、自分の言葉でわかりやすくまとめていきたいと思います。

まずはJavaScriptのオブジェクトは何?から始めたいと思います。
前に書いたものを振り返るとやっぱりあの頃の自分はまだ全般を理解していなかったと思った。例えばtypeof運算子でArrayobjectだと疑問があったけど、自分の中は実践が大事だから使いこなせるまでそのまま疑問を放置してました。
でもこうして振り返りの大切さ、そしてやりがいも感じました。わからないことをわかるようになる楽しみを味わいながら頑張っていきたいと思います!

継承方法についてほかの文章:
JavaScriptのinheritanceについて part1
JavaScriptのinheritanceについて part2

もう一度プロトタイプチェーンを考察した(相関図入り):
JavaScriptの.__proto__とprototypeとinstance

Object in JavaScript

JavaScriptのデータ型は大きく分けると、プリミティブとオブジェクトがあります。

Primitives
String
Number
BigInt
Boolean
undefined
Symbol(ES6)
null
七つのプリミティブはimmutable(変わらない)、つまりstate(状態)が変更できないということです。変数に新しい値を代入することができるが、値自体を変更したわけではありません。(この部分は、自分は「利用してない値がメモリーリサイクルされる」と理解している)

Object
Object
Array
Function
null(the end of prototype chain)
そしてオブジェクトはJavaScriptの核心概念として、すべてのデータ型を格納するデータ型で、さらに細かく分けるとArrayFunctionnullこれらがオブジェクトのサブタイピングです。
そのなかnullはプリミティブでありながら、すべてのObject.prototypeの原点(何も継承してないオブジェクト)でもあります。

const x = {
  a: 1
}

console.log(x.__proto__ === Object.prototype) // true
console.log(x.__proto__.__proto__ === null) //true

console.log(Object.prototype.__proto__ === null) //true
console.log(Object.getPrototypeOf(Object.prototype)) // null

Function関数はオブジェクト?
少し変な言い方かもしれませんが関数はオブジェクトです。確かにtypeofを使うとfunctionという結果が出てきたんですが、JavaScriptの関数のなかには[[Call]]という隠しプロパティを持って、callable object(呼び出せるオブジェクト)として使えます。

function callable() {
  const a = 1
}

callable.b = 2

console.log(callable.a) // undefined
console.log(callable.b) // 2

console.log(callable) // [Function: callable] { b: 2 }
console.log(callable.__proto__) // {}
console.log(callable.__proto__.__proto__ === Object.prototype) // true
console.log(callable.__proto__.__proto__.__proto__ === null) // true

Functionは外部からアクセス不能という特性を持っていますが、オブジェクトのようにbとその値を格納することができる。

実際ログしてみるとFunctionの後ろにはオブジェクトがついていてcallable.bによって値にアクセスできる。

また、Object.prototypeのアクセサプロパティ__proto__を使って、関数のプロトタイプ.__proto__(参照先)は{}、順次に上にさかのぼるとObject.prototypenullになります。

Prototype in JavaScript

ではPrototypePrototype Chainは何でしょう?

prototype:原型。(複製元) 継承とプロトタイプチェーン

まずはprototypeは原型という意味を持つ単語として、JavaScriptではプロパティやメソッドなどの元となるオブジェクトをprototypeと言い、すべてのオブジェクトには必ずprototypeが存在する。(nullにはありません。)

Prototypal Inheritance & Prototype Chain

Inheritanceというのは継承です。C++C#JavaではClassical inheritance、JavaScriptではPrototypal inheritanceを使用します。
classical inheritance vs prototypal inheritance in javascript - stackoverflow
Master the JavaScript Interview: What’s the Difference Between Class & Prototypal Inheritance? - Eric Elliott

Prototypal inheritanceを簡単に言うと、prototypeはオブジェクトがほかのオブジェクトからプロパティやメソッドを直接継承する機能として、もともと持つものの上に、ほかのprototypeと連携し拡張することができます。

自分のオブジェクトからプロパティを読み込んでも見つからなかった場合、自分のプロトタイプ.__proto__に読み込んでみる。それもなかった場合はさらに上、最後は必ずnullにたどり着くので、すべてのプロトタイプ.__proto__にはなかった場合はundefinedを返します。これがプロトタイプチェーンです。

let animal = {
  eats: true
}

let rabbit = {
  jumps: true
}

rabbit.__proto__ = animal

console.log(rabbit.jumps) // true
console.log(rabbit.eats) // true // get property from 'animal'

console.log(rabbit.__proto__) // { eats: true } // inherit from 'animal'
console.log(rabbit.__proto__.__proto__) // [Object: null prototype] {}
console.log(rabbit.__proto__.__proto__.__proto__) // null
console.log(rabbit.a) // undefined

.__proto__の変更や使用は混乱を生じやすいので未推奨です。ここはただ説明の例として。)
Prototypal inheritance - javascript.info

JavaScriptのメソッドもこの形式で利用されています。
MDNでメソッドを調べるときよく見てるけど気づかなかったところがあります。
メソッドを探すとき文字列ならString.prototype.、配列ならArray.prototype.から始まり、以前はただの分類だと思っていたがprototypeを理解したら全部プロトタイプチェーンからメソッドを探し使っていることが分かりました。

ChromeのDevToolsで[[Property]]一覧表示できます。これで隠しプロパティ[[Property]]も見やすくなりますね。
captures_chrome-capture-2022-6-8.png

↓は別の例です。

// Base object
const point2D = {
  x: 10,
  y: 20,
}

// Inherit from 'point3D' object
const point3D = {
  z: 30,
  __proto__: point2D,
}

console.log(
  point3D.x, // 10, inherited from point2D
  point3D.y, // 20, inherited from point2D
  point3D.z  // 30, from its own property
)

Prototype - JavaScript. The Core: 2nd Edition

.__ proto __

Object.prototype.__ proto __ - MDN
.__proto__はオブジェクト内部の[[Property]]プロパティとは同じではなく、Object.prototypeのアクセサープロパティです。
今は非推奨になっているので、Object.getPrototypeOfObject.setPrototypeOf関数の利用がおすすめです。
[[Property]] の直接変更はパフォーマンスに影響を及ぼすので避けた方がいいです。)

Constructor Function & new vs. Prototype

constructor:コンストラクタ、構築子。 Object.prototype.constructor - MDN
Constructor function(コンストラクタ関数) というのは、普通の関数の内部にthisでExecution Context(実行コンテキスト)を変えさせて、そしてnewで新しいインスタンスを創り出していく関数です。

7/13補足:もう一度調べたらすべての関数にはconstructorプロパティを持ち、newでインスタンスを創るときにconstructorプロパティで継承対象(親)を参照し、その親がコンストラクタ関数と呼びます。
この文章のコンストラクタ関数は特にthisオブジェクトを所有するコンストラクタ関数を指しています。

newで創られたインスタンスのコンテキストは、自分のスコープにバインドするようになります。

function Cat(name, color) {
  this.name = name
  this.color = color
  this.species = 'cat'
  this.favorite = () => {
    console.log('eat and sleep')
  }
}

const cat1 = new Cat('Mio', 'white')
const cat2 = new Cat('Tora', 'orange')

console.log(cat1.name, cat1.color) // Mio white
console.log(cat2.name, cat2.color) // Tora orange

console.log(cat1.species) // cat
cat1.favorite() // eat and sleep
console.log(cat2.species) // cat
cat2.favorite() // eat and sleep

(コンストラクタ関数は頭文字が大文字という暗黙のルールがあります。)
初めてconsole.log(cat1.name, cat1.color)を読んだとき、多分私と同じ、なぜこれだけで値をアクセスできるの?って思っちゃいますね。

まずfunctioncallable objectとして、別のところからの値を保存しアクセルすることもできます。thisはここでの作用は指定された実行コンテキストで新しいオブジェクトを生成し値を保存する、ということで、オブジェクトのように値のアクセスができるわけです。なのでコンストラクタ関数にはthisで共有のオブジェクトでプロパティ(property name: property value)とメソッドを保存し、newですべてのイスタンスが同じプロパティやメソッドが利用できるようになります。

thisのコンテキストについて

しかしここには一つの問題があります。newで毎回新しいインスタンスを生成するたびに、中身が同じプロパティとメソッドを何度も複製していくということで、メモリーにとってデメリットになります。例えばthis.speciesthis.favorite()には固定された内容には、何かメモリーにやさしい方法がないでしょうか。

実はJavaScriptではすべてのコンストラクタ関数にprototypeプロパテ(オブジェクト)を持たされて、コンストラクタ関数で定義したプロパティやメソッドが、自分のprototypeプロパティに所有し、そしてnewで創られたインスタンスたちは親であるコンストラクタ関数からprototypeプロパティを継承する。

なので変わらない、あるいは変わる必要のないプロパティやメソッドをコンストラクタ関数のprototypeオブジェクトに直接定義していいです。

function Cat(name, color) {
  this.name = name
  this.color = color
}

Cat.prototype.species = 'cat'
Cat.prototype.favorite = () => {
  console.log('eat and sleep')
}
// console.log(Cat.prototype)
// { species: 'cat', favorite: [Function (anonymous)] }

ここから
コンストラクタ関数とインスタンスの関連
コンストラクタ関数がインスタンスのprototypeであるかどうかの確認、
インスタンスのプロパティはコンストラクタ関数からどう継承してきたかの確認
...いくつのメソッド紹介したいと思います。

instanceof

instanceOfを用いてコンストラクタ関数とインスタンスの関連を確認する。

console.log(cat1 instanceof Cat) // true
console.log(cat2 instanceof Cat) // true

isPrototypeOf()

isPrototypeOf()メソッドを用いて、親コンストラクタ関数のprototypeオブジェクトとイスタンスの関連を確認できます。

console.log(Cat.prototype.isPrototypeOf(cat1)) // true
console.log(Cat.prototype.isPrototypeOf(cat2)) // true

hasOwnProperty()

すべてのイスタンスにはhasOwnProperty()メソッドがあります。hasOwnProperty()メソッドはプロトタイプチェーンではなく自分のプロパティだけ検定するので、これであるプロパティが親コンストラクタ関数(ローカル)から継承したか、それとも親コンストラクタ関数のprototypeオブジェクトから継承したかを確認できます。

console.log(cat1.hasOwnProperty('name')) // true
console.log(cat1.hasOwnProperty('species')) // false
console.log(cat2.hasOwnProperty('favorite')) // false

'species''favorite'いずれもコンストラクタ関数からではなく、prototypeを経由して継承しているのでfalse。)

in

hasOwnProperty()とは逆にinは自身のプロパティだけでなくプロトタイプチェーンを全部チェックする。

console.log('name' in cat1) // true
console.log('species' in cat1) // true

constructor in instance?

よく間違われるところだと思いますが、実はインスタンスにはconstructorプロパティを所有しません。

console.log(cat1.hasOwnProperty('constructor')) // false
console.log(cat1.__proto__.hasOwnProperty('constructor')) // true

console.log(cat1.constructor === Cat) // true
console.log(cat2.constructor === Cat) // true

console.log(cat1.constructor === Cat)trueなのは、インスタンスのconstructorでなく、上のプロトタイプチェーンにコンストラクタ関数Catがいるから、Catconstructorと照らし合わせて自然とtrueが返されました。

インスタンスのconstructorプロパティは可変です。コンストラクタ関数のprototypeが変えてしまえば、新しいイスタンスのconstructorも変えてしまいます。(古いのはpass by sharingの影響でそのままです。)

Cat.prototype = {}
const cat3 = new Cat()
console.log(cat3.constructor === Cat) // false
console.log(cat3.constructor === Object) // true

console.log(cat1.constructor === Cat) // true
console.log(cat1.constructor === Object) // false
console.log(cat2.constructor === Cat) // true
console.log(cat2.constructor === Object) // false
// note: pass by sharing

参考資料まとめ

Primitive (プリミティブ) - MDN
継承とプロトタイプチェーン - MDN
Object.prototype.__ proto __ - MDN
Object.prototype.constructor - MDN
Object.getPrototypeOf() - MDN
Object.prototype.hasOwnProperty() - MDN

Prototype - JavaScript. The Core: 2nd Edition

classical inheritance vs prototypal inheritance in javascript - stackoverflow
Master the JavaScript Interview: What’s the Difference Between Class & Prototypal Inheritance? - Eric Elliott
Prototypal inheritance - javascript.info

感想

勉強メモとはいえ、公の場でいろんな方に見せるものでもありますので、正直外国人である自分の考えがうまく伝わったかどうかとても不安です。まだまだ初心者(学習歴8ヶ月)なのに、こういう角度でこうやって解釈していいのかなって文章を書くたびに思ってしまいます。でもやはりこれからもう一度振り返るときのために一度文章にする必要があると思います。
長文ですが、最後まで読んでいただきありがとうございます。お気づきの点がございましたら、ご指摘いただけると幸いです。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?