作って理解JavaScript：JOKE開発記その7 - オブジェクトとクラス

# 今回のスコープ

前回から一か月近く経ってる。
今回の実装内容自体が手間のかかるものであったという理由もありますが主な理由はゲームなど他のことをしていたためです（ぉぃ

さて、[前回のあとがき](https://qiita.com/junjis0203/items/d9e0e8c82cd811df4caa#%E3%81%82%E3%81%A8%E3%81%8C%E3%81%8D)でも書いたように制御構造も一通りできて次は配列かと思ったのですが、配列はオブジェクトとクラスの仕組みを作ってからの方が作りやすいだろうと思ったため、今回の内容はオブジェクトとクラスです。具体的には以下の内容です。

- ステップ9：オブジェクト
  - まずは連想配列的な意味でのオブジェクト
  - Shorthand property namesやスプレッドもサポート
  - 次にメソッド。というかthisの扱い
- ステップ10：クラス
  - 昔ながらのprototype。合わせてnew
  - 今どきのclass
  - 
JOKE実装で使っちゃったから
継承もサポート
  - Arrayで使う予定なのでgetter/setterもサポート

# ステップ9：オブジェクト

ステップ9段階のコードは以下にあります。
https://github.com/junjis0203/joke/tree/step0009

## オブジェクトリテラル

プログラム内でオブジェクトが定義できないと話が始まらないのでまずはオブジェクトリテラルです。
http://www.ecma-international.org/ecma-262/6.0/#sec-object-initializer

オブジェクトの作成は以下のように行っています。

1. 空のオブジェクトを作成しスタックにプッシュ（`MAKE_OBJECT`）
2. 名前を`PUSH`
3. 値を`PUSH`
4. オブジェクト、名前、値をスタックからポップしプロパティとして設定（`PROP_DEFINE`[^1]）
5. 2.～4.をリテラルに書かれてるプロパティ分繰り返す

[^1]: ステップ10終わりに名前の見直しを行ったのでHEADでは`DEFINE_PROPERTY`。

[Assemblerの該当部分](https://github.com/junjis0203/joke/blob/step0009/lib/assembler.js#L37-L59)（後述するスプレッドの処理も含みます）
[Vmの該当部分](https://github.com/junjis0203/joke/blob/step0009/lib/vm.js#L238-L253)

### Shorthand property names

ES2015で導入されたShorthand property names、こういうやつですね。

```js
const foo = 123;
const bar = 'abc';
const obj = {foo, bar};
```

構文解析的に`Identifier`の後ろに`:`がなければShorthand property namesとみなすようにしました。
また、[「展開」の処理](https://github.com/junjis0203/joke/blob/step0009/lib/parser.js#L115-L150)は`Parser`内でしてしまうことで`Assembler`や`Vm`は変更なしで動作するようにしました。

ちなみにComputed property namesも対応はできますがJOKE実装では使ってないしサポートしてません:stuck_out_tongue:

### スプレッド

スプレッドの仕様見当たらないな(。´･ω･)?
とよく考えたらオブジェクトのスプレッドはES2018からでした。まあでも便利なので（JOKE実装で使ってるので）こちらはサポートします。
https://www.ecma-international.org/ecma-262/9.0/index.html#sec-object-initializer

スプレッドで問題となるのは「プロパティの数がアセンブル時点でわからない」ということです。
初めに作成したオブジェクトリテラル実装では「プロパティの数を`MAKE_OBJECT`の引数にし、VM実行時にスタックから引数分の名前と値を取り出す」ということをしており、スプレッドをなんとかこの仕組みに乗せれないかいろいろ試行錯誤したのですが[^2]、結局「専用の[COPY_OBJECT命令](https://github.com/junjis0203/joke/blob/step0009/lib/vm.js#L254-L265)を作った方が実装がシンプルになる」ということで既存部分を修正しました。
一度作ったものをなんとか使いまわそうとして変な実装になるよりかはそもそもの実装を変えてしまった方がいいという教訓を得ました。[^3]

[^2]: `// len is added in runtime`というコメントは試行錯誤してたときの残骸コメントですね・・・
[^3]: その際に大事なのは既存部分が壊れてないかを確認できるテストですね:grinning:

## プロパティへの代入

プロパティの参照はステップ1で作っているので飛ばして代入処理です。
「変数への代入」と「オブジェクトプロパティへの代入」で処理を分けたくなかったので「変数とはスコープオブジェクトのプロパティである」のような一般化をしようと思いましたがめんどくさいのでやめました（プロパティへの代入を行う[MEMBER_ASSIGN命令](https://github.com/junjis0203/joke/blob/step0009/lib/vm.js#L142-L151)[^4]を作りました）。後述のsetterのことを考えるとちゃんと分けてよかったのかなと思います。

[^4]: HEADでは`SET_PROPERTY`に名前変更。

余談ですがPythonやRubyは仕組みは違うもののどちらも「プロパティへの代入」はメソッド呼び出しとして行われます。いつものように「PythonやRubyってどうしてるっけ？」と参考にしようとしたら参考になりませんでした(笑)

## this

さて`this`です。JavaScriptの`this`はややこしいことで有名ですがなんちゃって処理系なので「複雑な使い方」は当面サポートしない方向でいきます。つまり、

- オブジェクトからメソッドを取り出して関数的に呼ぶのはサポートしない
- Object.bindもサポートしない
- アロー関数での「thisを束縛しない」動作もサポートしない
- すなわち環境からthisを決定するのはサポートしない

`this`に関する仕様（動作）はここら辺が参考になります。
http://www.ecma-international.org/ecma-262/6.0/#sec-function-calls

仕様を正確に実装していませんが（ぉぃ、以下のように実装を行い`this`を実現しました。この処理はステップ10でもう少し改造が入ります。

1. [setProperty](https://github.com/junjis0203/joke/blob/step0009/lib/object.js#L38-L46)時に`value`がオブジェクトの場合は`value`に`thisValue`を設定する（関数はオブジェクトで定義されているので`thisValue`が設定される）
2. [callFunction](https://github.com/junjis0203/joke/blob/step0009/lib/vm.js#L63-L73)でオブジェクトに設定されてるthisValueを取り出し`context`に設定、呼び出す
3. `this`に対応する命令の[THIS](https://github.com/junjis0203/joke/blob/step0009/lib/vm.js#L168-L170)では単に`context`に設定されている`thisValue`を取り出す（スタックにプッシュする）

# ステップ10：クラス

オブジェクトと関連が深いからということでクラスについてもまとめて説明します。JavaScriptの場合、「クラス」というと話は長くなりますがES2015で定義された`class`のこととします。
と言いつつ、実体は・・・なのですが、そもそも[JOKEを作ってみようと思った](https://qiita.com/junjis0203/items/07ae97c336cc9566938c)因縁の機能（？）ですね。

ステップ10段階のコードは以下にあります。
https://github.com/junjis0203/joke/tree/step0010

## prototype形式とnew

V8などの内部実装がどうなってるのかはわからないですが、とりあえずJOKEでは`class`で定義されたクラスもprototype形式に変換して動かすことにしました。
ところでVSCodeだとprototype形式で書いているとこんなことが言われます。最近のエンジンは「クラス形式」に変換されてるっぽいですね。

![prototype.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/141091/c19e0d7d-a286-a680-afee-740b7dbb59d5.jpeg)

ステップ10の前準備として「オブジェクト」に対応する`JokeObject`[^5]を継承した（このために継承も実装することになりました）[JokeFunction](https://github.com/junjis0203/joke/blob/step0010/lib/object.js#L78-L82)を定義し、`prototype`を初めから持たせておきます。こうしておけば「prototypeにメソッドを設定する」処理はステップ9で作ったプロパティへの代入がそのまま動きます。

[^5]: ステップ1で「とりあえずのオブジェクト表現」を作りそのまま進めてきましたがリファクタリングしました。Jokeと付いているのは素の`Object`と被るためです。

```js:object.js抜粋
export class JokeFunction extends JokeObject {
    constructor() {
        super();
        this.setProperty('prototype', new JokeObject());
    }
```

`new`は「空のオブジェクトを作り、それを`this`としてコンストラクタ関数に渡す（[MDNでの説明ページ](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/new)）」というものなので[その通りに実装](https://github.com/junjis0203/joke/blob/step0010/lib/vm.js#L254-L264)しました。そこまで考えていなかったのですが（ぉぃ、ステップ9で作った「contextにthisを入れておく」仕組みがうまく使いまわせました。

```js:vm.js抜粋
    case I.NEW:
        {
            const args = setupArguments();
            const target = stack.pop();

            const newObject = new JokeObject();
            newObject.setProperty('constructor', target);
            callFunction({...context, thisValue: newObject}, target, args);
            stack.push(newObject);
        }
        break;
```

ややこしいのはメソッド（prototypeに設定されているプロパティ）の[取得処理](https://github.com/junjis0203/joke/blob/step0010/lib/vm.js#L254-L264)です。JavaScriptで書いているため「constructorが設定されてなくても素のconstructorが取得される[^6]」という点、「`this`がprototypeに設定されているのを書き換える必要がある（コメントに書いてあるようにこの実装は気に入ってません）」点などちゃんと動くまで苦労しました。

[^6]: JokeObjectは「JOKEを動かしているJavaScriptエンジン」での「Object（を継承したクラス）のインスタンス」のため、そちらが取得される。

```js:object.js抜粋
    getProperty(name) {
        let value;
        // need because native "constructor" is found if not set
        if (this.names.includes(name)) {
           value  = this.properties[name];
        }
        if (!value && this.names.includes('constructor')) {
            const constructor = this.properties['constructor'];
            value = constructor.searchProperty(name);
            // change thisValue. any more better way?
            if (value instanceof JokeObject) {
                value = {
                    ...value,
                    thisValue: this
                };
            }
        }
        return value;
    }
```

## class形式

さて`class`です。`class`に関する動作仕様はこちらに書かれています。
http://www.ecma-international.org/ecma-262/6.0/#sec-runtime-semantics-classdefinitionevaluation
超長いし条件分岐もある( ﾟДﾟ)
というわけで読む気が起こるのも読み解くのも時間かかったわけですが、最終的に以下のように実装しました。仕様準拠ではないですが大体ちゃんと動作します。

- [構文解析](https://github.com/junjis0203/joke/blob/step0010/lib/parser.js#L1195-L1216)の段階で`constructor`を分けておく
- [アセンブラ](https://github.com/junjis0203/joke/blob/step0010/lib/assembler.js#L452-L491)ではまず`constructor`を関数として定義しその後に`prototype`にメソッドを設定していく。継承については後で説明します。それと`// TODO: cache`とありますが当面効率は無視です（[開発記その1](https://qiita.com/junjis0203/items/07ae97c336cc9566938c#%E5%AE%9F%E8%A3%85%E8%A8%80%E8%AA%9E%E3%81%A8%E3%81%9D%E3%82%8C%E3%81%AB%E3%82%88%E3%81%A3%E3%81%A6%E7%94%9F%E3%81%98%E3%81%9F%E7%9B%AE%E6%A8%99)の方針参照）

こうすれば「prototypeで書いたのと同じこと」になるのでVMの変更はありません。
正確に言うと`new`付けないで関数呼び出しされたら例外投げるとかはあるのですがとりあえず保留です。

### 継承

めんどくさそうなので作る気がなかった継承ですが、実装に使ってしまったので諦めて仕様を読んでみたところおもしろいことがわかりました。
http://www.ecma-international.org/ecma-262/6.0/#sec-class-definitions

```
ClassHeritage :
    extends LeftHandSideExpression
```

そう、`extends`の後ろはLeftHandSideExpressionなのでハードコードなクラス名だけではなく変数でも関数呼び出しでも書けるのです！
つまり以下のコードは正しいJavaScriptプログラムです。

```js
class Foo {}

const cls = Foo;
class Bar extends cls {}

const func = () => Foo;
class Baz extends func() {}
```

まあフレームワークの中ぐらいでしか使わない、というか今のところフレームワークの中でも見たことないですが(笑)

もう一つ文法的な話。`super`を`console.log`しようとすると`SyntaxError`になるの何故なのかと思っていたのですがBNFを見て納得しました。単独に`super`と書く文法は存在しない（`super()`や`super.method()`しか正しい文法ではない）のですね。
http://www.ecma-international.org/ecma-262/6.0/#sec-super-keyword

さて実装ですが[Assembler](https://github.com/junjis0203/joke/blob/step0010/lib/assembler.js#L460-L465)ではスーパークラス（`extends`の後ろにあるもの）を評価しておき、[Vm](https://github.com/junjis0203/joke/blob/step0010/lib/vm.js#L293-L299)では単にスタックに着かれているものを`superClass`プロパティに設定しておきます。
スーパークラスのコンストラクタ呼び出しに対応する[SUPER_CALL](https://github.com/junjis0203/joke/blob/step0010/lib/vm.js#L265-L273)とスーパークラスのプロパティ参照[^7]に対応する[SUPER_PROPERTY](https://github.com/junjis0203/joke/blob/step0010/lib/vm.js#L361-L368)では設定されている`superClass`を使ってスーパークラスにアクセスします。

[^7]: 使うことがあるかはわかりませんが仕様的には「スーパークラスのメソッド呼び出し」だけではなく、「（オーバーライドして`this`ではアクセスできないスーパークラスのプロパティ」を取ることができます。

### getter/setter

`Array`の`length`は特別処理しようかなと考えていたのですがよく思うとgetter/setterとして一般化できるので実装しました。

getter/setterに関する仕様はこちら。要するに`get `や`set `を付けたメソッドを作ればいいようです（ちゃんと読み込むとそれだけではない気がする）
http://www.ecma-international.org/ecma-262/6.0/#sec-method-definitions-runtime-semantics-propertydefinitionevaluation

[Vm](https://github.com/junjis0203/joke/blob/step0010/lib/vm.js#L332-L360)では`SET_PROPERTY`時にsetterがあるか、`GET_PROPERTY`時にgetterがあるかを調べるようにしました。

# デバッグ機能

今までは`-d`オプション一つで「構文解析結果」「アセンブル結果」「今実行している命令」を出力していましたが出力が多くなってきたのでそれぞれ別オプションに分けました。
また、「今実行している命令」は追加情報として「今のスタック」を表示するようになりました。クラスまで来ると「動きがおかしいのだけどスタックに積まれてるデータどうなってるの？」ということが多くなってくるので役立っています（今までは都度`console.log`を入れていた）

# あとがき

以上今回はオブジェクトとクラスの実装について説明しました。これでだいぶJavaScriptっぽいプログラムまで対応できるようになったと思います。
・・・「普通の使い方」についてはそこそこ動くと思うのですが「変わった書き方」や「仕様にも書かれている細かな文法エラー・実行時エラー」はあまりサポートできていません。まあそのうち。

ともかく次は満を持して配列（メソッド含む）です。
ただその前に、`map`などに渡すコードバックはアロー関数で書きたいのでアロー関数を実装する予定です。