「Typescript」でprototypeの沼にハマったので、色々調べてみた。

Typescriptを学習中、prototypeに対する挙動について調べる機会があったので、
復習も兼ねて投稿します。
間違っている箇所等があればぜひご指摘ください！💦

---

2022/8/29　投稿の一部を下記の通り編集しました。
【修正前】■結論と解説　~ES2015仕様以前
**【修正後】■結論と解説　~ES5仕様以前**

---

# 【 前提知識 】
## 1. Javascriptは「プロトタイプベース」

Javascript本格入門より

>Javascriptには「インスタンス化・インスタンス」という概念は存在するが、
>「クラス」という概念はなく、プロトタイプ（雛形）と言う概念が存在する。


**■Javascriptのクラスの作成方法（ES2015以前）**
```javascript
let A = function() {};   // クラスの作成完了。

let a = new A();   // インスタンスの完成。

let B = function ( firstName, lastName ) { // クラスの生成とコンストラクタ
  this.firstName = firstName;  // プロパティの生成
  this.lastName = lastName;
  this.getName = function() {
    return this.firstName + this.lastName;
  } 
};

let b = new B("山田", "太郎");  
console.log(b.getName());    // 山田 太郎
```

### ポイント
**<font color="red">■Javascriptでは、function（functionオブジェクト）に擬似的なクラスとしての役割を与えていた。</font>**

**■functionで作成した擬似的クラスをnew演算子でインスタンス化する。**

**■クラスとして生成したfunctionの中に初期化処理を書く ＝ コンストラクタ**

**<font color="blue">■コンストラクタの生成はクラスの生成とほぼ同じ</font>**

## 2. prototypeは２種類ある。

Javascriptには「prototype」が２種類存在する。

**<font color="red">①prototypeプロパティ</font>**

**<font color="red">②[[prototype]]（__ proto __）</font>**


## ■prototypeプロパティについて

### - コンストラクタ上にあるプロパティ -

・`コンストラクタ`生成時に自動的に追加される`prototype`というプロパティ。

・prototypeプロパティは生成時、<font color="red">空オブジェクト（プロトタイプオブジェクト）</font>を参照している。

・同じ`prototype`でも、`コンストラクタが違えば`、それは違う`prototypeプロパティ`になる。

```javascript
const Sample = function(){
  // some code;
}

Sample {
  prototype: {}
}
```

・**クラス（コンストラクタ名）.prototype.プロパティ名** 

で、空オブジェクト（プロトタイプオブジェクト）内にプロパティが追加される。

```javascript
const Sample = function(){
  // some code;
}

Sample.prototype.methodA = function(){ 
  // some code. 
};

Sample {
  prototype: {
    methodA: function (){ /*some code*/ };
  }
}
```

### ■補足 Object.prototype

・`Objectオブジェクトのコンストラクタ`上にある、`prototype`プロパティ。

・`toString()`や`valueOf()メソッド`が利用できるのは、
`prototype`プロパティが指す、`プロトタイプオブジェクト内`に定義されている。

・Objectは全てのオブジェクトが継承するものであるので、全てのオブジェクトがこの`Object.prototype`の参照を持っている。

**※イメージ**

```javascript
Object {
   prototype: {
     toString: ...
     valueOf: ...
  }
}
```
---

## ■[[prototype]] （__ proto __）

### - インスタンス上の「内部プロパティ」 -

・`インスタンス上` に作成される**内部プロパティ**。

・ `.prototype`のように直接アクセスすることができないプロパティ。

・インスタンス生成時、`[[prototype]]の参照先`は、`コンストラクタ上`にあったprototypeプロパティが参照していたオブジェクト（プロトタイプオブジェクト）と同じ参照を持つようになる。

**※イメージ**

```javascript
const Sample = function(){
  // some code;
}

Sample.prototype.methodA = function(){ 
  // some code. 
};

let s1 = new Sample();

Sample {
  /* :some code分プロパティ */
  /* [[prototype]]: */　→　Sample.prototypeが参照していたプロトタイプを参照。
}

```

**■補足：プロトタイプチェーン**

**イメージ図**
```javascript
const Sample = function(firstName, lastName){
  this.firstName = firstName;
  this.lastName = lastName;
}

Sample.prototype.methodA = function(){ /* some code. */ };

let s1 = new Sample("Sample","man");


s1.firstName ~

s1.methodA ~

s1.toString();


s1 {
  firstName: "Sample",
  lastName : "man",

  /* [[prototype]] */: → { 
     methodA():function(){/*some code.*/} ,

  /* [[prototype]] */: → {
       toString(): ...
       valueOf():  ...
     }
   }
}


```

**図イメージ**
![スクリーンショット 2022-08-19 0.19.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/754263/ae760798-1bc6-4dc6-728c-8708bd1d957d.png)

上記のインスタンス化された`s1`が自身のプロパティにアクセスする時、

①`firstName`**はSample直属のプロパティにアクセス。**

②`methodA`**は直属のプロパティにはないので、**`[[prototype]]`**を経由して**`methodA`**にアクセス。**

③`toString`**は直属にも**`methodA`**があるプロトタイプオブジェクトにもないので、**`methodA`**内のプロトタイプの[[prototype]]を経由し、Objectのプロトタイプにアクセス。**

このように、[[prototype]]を経由した数珠繋ぎ状のオブジェクトの繋がりを、**プロトタイプチェーン**という。

# 【 本題 】
## ■prototypeに関する問題
Typescriptを学習中に以下のようなコードを拝見したのでコーディングしたところ、
結果はコンパイルエラーになり、Javascriptでコーディングすると問題なく実行ができたので、原因を調査しました。

### 【問題①】
```javascript
const foo = { a: 1 };
const date = new Date();
const arr = [1, 2, 3];
 
// どのオブジェクトもhiプロパティが無いことを確認
console.log(foo.hi, date.hi, arr.hi);
// undefined undefined undefined
 
// プロトタイプにプロパティを追加する
Object.prototype.hi = "Hi!";
 
// どのオブジェクトもhiプロパティを持つようになる
console.log(foo.hi, date.hi, arr.hi);
// Hi! Hi! Hi!

```
**問題の箇所**
```javascript
Object.prototype.hi = "Hi!";
```

## ■結論と解説
### 【 結論 】
Typescriptでは、
**<font color="red">予めオブジェクトに期待しているプロパティがなかったり、期待していないプロパティが代入されてしまうとエラーを起こす。</font>**
という制限をかけています。



### 【 解説 】
Typescriptの型定義ファイル`lib.es5.d.ts`には、Object型は以下のように定義されています。

**■lib.es5.d.ts**
```Javascript
interface Object {
    
    constructor: Function;

    toString(): string;

    toLocaleString(): string;

    valueOf(): Object;

    hasOwnProperty(v: PropertyKey): boolean;

    isPrototypeOf(v: Object): boolean;

    propertyIsEnumerable(v: PropertyKey): boolean;
}

interface ObjectConstructor {
    new(value?: any): Object;
    (): any;
    (value: any): any;

    /** A reference to the prototype for a class of objects. */
    readonly prototype: Object;

    getPrototypeOf(o: any): any;

    :
    :
    :
}

```
【前提知識】のところでも説明したように、`.prototype`でアクセスできるのは、コンストラクタ上にある`prototypeプロパティ`です。
そしてObjectのコンストラクタ上にある`prototype`は`Object型`となっています。

```javascript
interface ObjectConstructor {

    /** A reference to the prototype for a class of objects. */
    readonly prototype: Object;

}
```

TypescriptではObject型は下記のように予め定義されています。
```javascript
interface Object {
    
    constructor: Function;

    toString(): string;

    toLocaleString(): string;

    valueOf(): Object;

    hasOwnProperty(v: PropertyKey): boolean;

    isPrototypeOf(v: Object): boolean;

    propertyIsEnumerable(v: PropertyKey): boolean;
}
```
これが理由により、Typescriptで制限した行為に引っかかり、エラーになります。
```javascript
Object.prototype.hi = "Hi!";
```
`hi`は`Object型`の定義に含まれておらず、<font color="red">期待していないプロパティを勝手に代入しようとしている</font>と判断され、型エラーになリます。

Javascriptの場合は動的に処理を実行してくれるので、なければシステムが勝手に追加する処理が働いてしまうので、特に意識しなくても処理が完了してしまいます。

---

### 【問題②】
次にObjectではなく、それぞれのオブジェクトの生成パターンでprototypeを利用してみたところ、それぞれ異なる結果が起きたので調べてみました。
```javascript

// ES2015以降
class Sample2 {
    name: string;
    constructor(name: string){
        this.name = name;
    }
}
Sample2.prototype.age = 26;  // コンパイルエラー。　プロパティ'age'は型'Sample2'に存在しません。


// オブジェクトリテラルでの生成
let Sample3 = {
    name: "Smith",
}
Sample3.prototype // コンパイルエラー。 プロパティ 'prototype' は型 '{ name: string; }' に存在しません。


// ES2015以前①
let Sample1 = function(name: string){
    this.name = name;
}
Sample1.prototype.age = 26;

// ES2015以前②
let Sample1 = function(){};
Sample1.prototype.name = "sample";
```


## ■結論と解説　~ES2015仕様以降~
### 【 結論 】
**<font color="red">Typescriptは「クラスベースのような振る舞いをする」ので、クラスやinterfaceで事前にプロパティの定義が必要</font>**

### 【 解説 】
①クラスも関数オブジェクト（functionオブジェクト）の一部なので、クラス生成時には自動的にprototypeも生成される。

→ **クラス名.prototypeがエラーにならない理由**

②Typescriptでは`ルール上`、クラスのプロパティにアクセスするには事前にプロパティを定義し、クラスの中にそのプロパティがあることを認識させなければいけない。
逆にプロパティに定義を記載すれば、プロパティがクラスのどこか（`クラスフィールド`・`インスタンスフィールド`・`プロトタイプオブジェクト内`）にある事を認識してくれる（そういう事のはず...）。
```javascript
class Sample2 {
    name: string;
    age: number;
    gender = "men";
    constructor(name: string){
        this.name = name;
    }
}
Sample2.prototype.age = 26;  // エラーなし
Sample2.prototype.gender = "women"; // エラーなし
```

### 【 番外 <font color="red">~Typescriptはあくまで「プロトタイプベース」</font>~ 】
Typescriptはクラスベースのような振る舞いをするだけなので、恐らく完全な静的型付けではないです。
理由としては、以下が挙げられます。

**■Prototypeを利用することもできる。**
```javascript
class Sample2 {
    age: number;
}
Sample2.prototype.age = 26;

let sample2 = new Sample2();

console.log(sample2.age);  // 26
```

**■ブラケット記法を使えば、動的にプロパティを追加することができてしまう。**
```javascript
class Sample2{
    age: number;
};
Dog.prototype.age =26;

let sample2 = new Sample2();
 sample2["firstName"] = "sample";
 console.log(sample2["firstName"]);  // sample
```

**■そもそもJavascriptは、クラスベースであるかのようにクラスが書けるだけの
<font color="blue">シンタックスシュガー（糖衣構文）</font>を提供しているだけである。**

>同じものを、よりわかりやすいとされる構文で書くために使われる構文のことを「シンタックスシュガー」 >(糖衣構文) という。

プロトタイプベースの書き方と比べてclassを用いる方が、 Java などの静的型付け言語を利用している方にとって馴染みのある書き方でわかりやすいから追加されただけで、`機能は従来から変更していない様子。`
要は書き方が違うだけでプロトタイプベースには変わりないということみたいです。

## ■結論と解説　~オブジェクトリテラルでの生成~
### 【 結論 】
**<font color="red">オブジェクトリテラルは、関数ではない。</font>**

### 【 解説 】
オブジェクトリテラルで生成したオブジェクトは、厳密には関数（Functionオブジェクト）ではないそうです。
よって、`内部プロパティ（__proto__,[[prototype]]）`は持っていても、`prototypeプロパティ`は持っていないことになります。

```javascript
let Sample3 = {
    name: "Smith",
}

console.log(Sample3.toString());  // [object Object]
Sample3.prototype.~ // コンパイルエラー
```

**【 補足 】**
オブジェクトリテラルで生成したオブジェクトは、`Object`を元に新しい`インスタンスオブジェクト`を生成していることと同等の扱いになるそうです。
生成されたインスタンスから`prototypeプロパティ`はアクセスできません。
これはnew演算子で生成したオブジェクトも同様にエラーになります。

https://jsprimer.net/basic/object/

```javascript
class Sample2 {
    name: string;
    age: number;
    gender = "men";
    constructor(name: string){
        this.name = name;
    }
}

let s2 = new Sample2("sample");
s2.prototype  // プロパティ 'prototype' は型 'Sample2' に存在しません。
```

解決方法としては、`Object.create`メソッドを使用して、新たにオブジェクトを生成することでとりあえず解決できました。
```javascript
let Sample3 = {
    name: "Smith",
}

let s3 = Object.create(Sample3);
s3.prototype.name = "Michael";  // エラーは出ない。
```

### 【 番外 ~ Object.createしたその後 ~ 】
　`Object.createメソッド`により、prototypeにアクセスできるようになりましたが、これは結果的にはコンパイルは通るも実行できず、エラーになります。

なぜ`コンパイルは通り`、`実行でエラーになるのか`調べてみました。
```javascript
let Sample3 = {
    name: "Smith",
}

let s3 = Object.create(Sample3);
s3.prototype.name = "Michael";  
// 実行後　→ Cannot read property 'firstName' of undefined
```

### 【 結論 】
**<font color="red">①Object.createで作成された新たなオブジェクトは、any型になる。</font>**

**<font color="red">②Object.createで生成された新たなオブジェクトは、新たなプロトタイプは持たない。</font>**

### 【 解説① 】
Object.createメソッドで作成された新たなオブジェクトはany型になります。

![スクリーンショット 2022-08-23 2.08.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/754263/7ff99e8d-74a1-20ba-e7fa-fb890e525c1e.png)

```javascript

```
any型は特殊な型になり、以下のような特徴を持ちます。

**■どんな型のものも代入できる。**

**<font color="red">■any型の値を参照する際の制約もない</font>**

特に重要なのは2つ目の特徴で、これがコンパイルエラーにならない理由になります。
なので、以下のようなコードが全てコンパイルを通過するようになり、実行時でエラーもしくは、そのまま実行できてしまいます。
```javascript
let Sample3 = {
    name: "Smith",
}

let s3 = Object.create(Sample3);

/**
 * 以下は全て実行時にエラーとなる。
 */
 s3.prototype.age = 26;
 s3.prototype.gender = "man";
 s3.prototype.weight = 55.5;

/**
 * 以下は実行時もエラーにならず、動的に追加できてしまう。
 */
s3.age = 26;
s3.gender = "man";
s3.weight = 55.5;

console.log(s3.age + "," + s3.gender + "," + s3.weight);
// 26,man,55.5
```

prototypeを操作しようとするソースの場合、新たに記載した3つのプロパティは存在しませんが、any型なので参照に制限がなく、コンパイルが通ってしまいます。

### 【 解説② 】
こちらは下記のサイトに解説が載っています。

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Object/create

> - いくつかの解決しない方法 
>
>不足しているオブジェクトメソッドを新しいオブジェクトの「プロトタイプ」に直接追加してもうまくいきません。
なぜなら、新しいオブジェクトは本当のプロトタイプを持っておらず、プロトタイプを直接追加することはできないからです。
```javascript
ocn = Object.create( null ); // "null" オブジェクトを生成 (既出と同じ)

ocn.prototype.toString = Object.toString; 
// エラー: Cannot set property 'toString' of undefined

ocn.prototype = {};                       // プロトタイプを生成してみる
ocn.prototype.toString = Object.toString; 
// 新しいオブジェクトにはメソッドがないので、標準オブジェクトから代入してみる

> ocn.toString() 
// エラー: ocn.toString is not a function
```

## ■結論と解説　~ES5仕様以前~
### 【 結論 】
**<font color="red">オブジェクトリテラルでの生成の時と同様、any型の値の参照は制約がない</font>**

### 【 解説 】
```javascript
// ES2015以前①
let Sample1 = function(name: string){
    this.name = name;
}
Sample1.prototype.age = 26;

// ES2015以前②
let Sample1 = function(){};
Sample1.prototype.name = "sample";
```
`function(){}`で生成しているのは関数、つまり`Functionオブジェクト`ということになります。
そしてTypescriptの定義ファイル（lib.es5.ts）では、Functionは以下のように定義されています。

```javascript
interface Function {
    /**
     * Calls the function, substituting the specified object for the this value of the function, and the specified array for the arguments of the function.
     * @param thisArg The object to be used as the this object.
     * @param argArray A set of arguments to be passed to the function.
     */
    apply(this: Function, thisArg: any, argArray?: any): any;

    /**
     * Calls a method of an object, substituting another object for the current object.
     * @param thisArg The object to be used as the current object.
     * @param argArray A list of arguments to be passed to the method.
     */
    call(this: Function, thisArg: any, ...argArray: any[]): any;

    /**
     * For a given function, creates a bound function that has the same body as the original function.
     * The this object of the bound function is associated with the specified object, and has the specified initial parameters.
     * @param thisArg An object to which the this keyword can refer inside the new function.
     * @param argArray A list of arguments to be passed to the new function.
     */
    bind(this: Function, thisArg: any, ...argArray: any[]): any;

    /** Returns a string representation of a function. */
    toString(): string;

    prototype: any;
    readonly length: number;

    // Non-standard extensions
    arguments: any;
    caller: Function;
}
```
`prototype`は`any型`と定義されています。
よって参照の制限がないのでコンパイルには引っかからず、`prototype.~`という構文を自由に書けるようになってしまいます。

**【補足】**
Typescriptで上記の2つのオブジェクトは、

`tsconfig.json`で`"noImplicitAny": true`

としていると,どちらもインスタンス化できないです。
また、
**<font color="red">Typescriptでは上記2つのクラスの生成方法は非推奨になります。</font>**

# ■総括
難しいことを考える前に、
**まずは普通にクラスを使ってインスタンスを生成しましょう！**

# 【 参考文献 】

https://techplay.jp/column/618

https://javascript.keicode.com/lang/prototypes.php

https://jsprimer.net/basic/prototype-object/

https://developer.mozilla.org/ja/docs/Learn/JavaScript/Objects/Object_prototypes

https://cpoint-lab.co.jp/article/202005/15244/

https://qiita.com/papi_tokei/items/93e5e05a70b5794a83b4

https://jsprimer.net/basic/class/

https://books.google.co.jp/books?id=qFnMBAAAQBAJ&pg=PA73&lpg=PA73&dq=typescript+prototype+%E3%81%84%E3%82%89%E3%81%AA%E3%81%84&source=bl&ots=1xZlYvjg-O&sig=ACfU3U03Fa44rD1c2VTftfb17TiCC2kpCQ&hl=ja&sa=X&ved=2ahUKEwjS1KXmpNj5AhWFed4KHWBHAo4Q6AF6BAgbEAM#v=onepage&q&f=false

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/Property_Accessors

https://zawatech.com/?p=344

https://www.pgls-kl.com/article/article_26.html

https://typescript-jp.gitbook.io/deep-dive/future-javascript/classes/classes-emit

https://future-architect.github.io/typescript-guide/function.html

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Function

https://www.monster-dive.com/blog/web_creative/20220128_002066.php

https://future-architect.github.io/typescript-guide/function.html

https://teratail.com/questions/29330