JavaScript を思い出しながら TypeScript を学ぶ

Pocket

JavaScriptには、「JavaScript: The Good Parts ―『良いパーツ』によるベストプラクティス」が発売された頃に、時間をかけて取り組んだ記憶があります。一方、TypeScriptは JavaScript のスーパーセットで JavaScript に 静的型付けを追加したという認識はあるものの、なんとなく理解している程度の状態でした。そこで、正月休みを利用して評判の良い「プロを目指す人のためのTypeScript入門 安全なコードの書き方から高度な型の使い方まで (Software Design plus)」(以下、プロを目指す人のためのTypeScript入門)を読んでいます。

現在、「プロを目指す人のためのTypeScript入門」を読み進めており、まだ「第5章 TypeScript のクラス」までですが、以下の感想を持ちました。

  • 取り上げる範囲と厳密さのバランスが良い
  • サンプルコードがよく考えられていて簡潔で分かりやすい

この本は JavaScript の知識がなくても理解できる構成になっています。しかしせっかく時間があるので、特に「第5章 TypeScript のクラス」を JavaScript の prototype と比較しながら読むようにしています。

JavaScript の prototype と比較して読んだメモをまとめます。

JavaScript の知識

JavaScript の prototype について prototype プロパティと constructor プロパティおよび コンストラクタ関数 1を中心にまとめます。

以下の内容は「開眼! JavaScript ―言語仕様から学ぶJavaScriptの本質」を参考にしています。

概要

  1. JavaScript の関数は Function コンストラクタ関数のインスタンスです。しかし new 演算子とともに使用されたときにインスタンスを生成するコンストラクタ関数にもなります
    • JavaScript は new 演算子とともに関数が呼び出されると(つまり コンストラクタ関数 として呼び出されると)関数の this に戻り値であるインスタンスを設定します。詳細は後述する擬似コードを参照してください( ちなみに Function 自身もコンストラクタ関数です)
  2. Function コンストラクタ関数のインスタンスは prototype プロパティを持ちます
  3. インスタンスはコンストラクタ関数の prototype へのリンク __proto__ を持ちます(現在は __proto__ を直接参照することは非推奨であり Object.getPrototypeOf() を使って取得します)
  4. インスタンスは自身を生成したコンストラクタ関数にポイントされた constructor プロパティを持ちます

コンストラクタ関数の疑似コード

function Foo(x, y) {
    // this = new Object(); 返却するインスタン用オブジェクトを作成

    this.x = x;
    this.y = y;

    // return this; return がなくても this( Foo のインスタンス) を返す
}

すべてのコンストラクタ関数prototype プロパティを持つ

  • ビルトインコンストラクタ関数( ArrayNumber )は、Function コンストラクタ関数 のインスタンス
  • カスタムなコンストラクタ関数 function Hoge() { /* コード */} は、Function() コンストラクタ関数 のインスタンス
  • class はコンストラクタ関数の糖衣構文
  • つまり↑よりすべてのコンストラクタ関数prototype プロパティを持つ

確認

console.log(Array.prototype === Object.getPrototypeOf(['a', 'b'])); // true Array のインスタンスは Array.prototype のリンクを持つ(参照する)
console.log(Object.prototype === Object.getPrototypeOf(Array.prototype)); // true プロトタイプチェーンは Object.prototype までチェイン
console.log(Array.constructor === Function); // コンストラクタ関数は Function のインスタンス
console.log(Object.constructor === Function); // true
console.log(((x:number) => x).constructor === Function); // true
console.log(Function.prototype === Object.getPrototypeOf((x:number) => x)); // true
console.log(typeof Function.prototype); // function
console.log(typeof Object.prototype); // object

クラス宣言とプロトタイプ

クラスについて MDN に以下のように記載されています2

JS のクラスはプロトタイプに基づいて構築されていますが、一部の構文や意味はクラスに固有です。

出典:https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Classes

クラスとプロトタイプの関係を「プロを目指す人のためのTypeScript入門」に出てくる User クラスを例に記載してみます。

class User {
    name: string;
    age: number;
    constructor(name: string, age: number) {
        this.name = name;
        this.age = age;
    }
}
/*
 * TypeScript は JavaScipt のスーパセットなので prototype 継承も有効
 */
// new User() でインスタンを作成
console.log(User.prototype === Object.getPrototypeOf(user1)); // true
console.log(user1.constructor === User); // true
// {}でインスタンスを作成 {} は new Object() の糖衣構文とも考えられる
console.log(User.prototype === Object.getPrototypeOf(user2)); // false
console.log(Object.prototype === Object.getPrototypeOf(user2)); // true
console.log(user2.constructor === User); // false
console.log(user2.constructor === Object) // true

クラスのメソッドは protoype のメソッドになる。

class Product {
    #name: string;
    #price: number;

    constructor(name: string, price: number) {
        this.#name = name;
        this.#price = price;
    }

    getOrder(num: number): string {
        return `${this.#name} is ${this.#price * num}`
    }
}


const product1 = new Product('product1', 100);
const product2 = new Product('product2', 200);

console.log(typeof product1.getOrder, typeof product2.getOrder); // ture
console.log(product1.getOrder === product2.getOrder); // true
console.log(product1.getOrder === Product.prototype.getOrder); // true
console.log(product2.getOrder === Product.prototype.getOrder); // true

メソッドとプロトタイプ

「プロを目指す人のためのTypeScript入門」( p221 )でメソッドについて以下の言及があります。

同じクラスの複数のインスタンスがある場合、それらが持つメソッドは同じ関数オブジェクトです。

この同じ関数オブジェクトは prototype に定義されています。

class Product {
    #name: string;
    #price: number;

    constructor(name: string, price: number) {
        this.#name = name;
        this.#price = price;
    }

    getOrder(num: number): string {
        return `${this.#name} is ${this.#price * num}`
    }
}

const product1 = new Product('product1', 1000);
const product2 = new Product('product2', 2000);

console.log(typeof product1.getOrder, typeof product2.getOrder); // function, function
console.log(product1.getOrder === product2.getOrder); // true
console.log(product1.getOrder === Product.prototype.getOrder); // true
console.log(product2.getOrder === Product.prototype.getOrder); // true

// メソッドは prototype ベースなので Production.prototype.getOrder を Function.prototype.apply() で呼び出すこともできる
console.log(Product.prototype.getOrder.apply(product1, [2])); // 2000

console.log(typeof product1.getOrder); // function
// なので Function.prototype.apply() を使用できる
console.log(product1.getOrder.apply(product2, [2])); // this に production2 を指定しているので 4000 になる

ES5 にトランスパイル

prototype の例として TypeScript クラスを ES 5 にトランスパイルしてみます。

// typescript
class Foo {
  public x: number;
  public echo: (x:number) => number;

  constructor(x:number) {
    this.x = x;
    // インスタンスメソッドを定義
    this.echo = x => x
  };
  // プロトタイプベースのメソッド定義
  double(): number {
      return this.x**2
  }
}

const foo1 = new Foo(3);
foo1.double();

ts.config の target を es5 にしてトランスパイルされたコードです。

"use strict";
// JavaScript
var Foo = /** @class */ (function () {
    function Foo(x) {
        this.x = x;
        // インスタンスメソッドを定義
        this.echo = function (x) { return x; };
    }
    ;
    // プロトタイプベースのメソッド定義
    Foo.prototype.double = function () {
        return Math.pow(this.x, 2);
    };
    return Foo;
}());
var foo1 = new Foo(3);
foo1.double();

※ コードの インスタンスメソッド の記載は一般にそのようによばれてかわかりませんが、コンストラクタ関数を使用したクラス宣言でインスタンスメソッド( prototype ではなくインスタンスに定義)がありました。


  1. 関数が単純な関数なのかコンストラクタ関数なのかは形式ではなく new での呼び出しを想定しているかで決まります。 

  2. 「一部の構文や意味はクラスに固有です」と記載があるので class が ES 5 のプロトタイプベースの糖衣構文と言い切れるかはわかりません💦。 

コメント

No comments yet.

コメントの投稿

改行と段落タグは自動で挿入されます。
メールアドレスは表示されません。