JavaScriptの勉強中:その6 Classes (後編) / Modules

ぼーっと今日も。半分くらいきたかな。

bufferings.hatenablog.com

ちょこちょこ勉強しながら「分かるようになってるのかなぁ?」とか思ってたけど、今日たまたま機会があって読んだら JavaScript のコードだいぶ理解できるようになってた。よかった。

### 9.4 既存のクラスへのメソッド追加

  • JavaScript のプロトタイプベースの継承の仕組みはダイナミック。なので、オブジェクトが生成された後にプロトタイプのプロパティを変更しても、その内容は反映される。
  • なので後からでもプロトタイプにメソッドを追加したりできる。ビルトインのクラス(Object とか String とか)の振る舞いも変更できてしまう。
  • けど、そういうことは基本的には、やらんほうが良い(わかる

### 9.5 サブクラス

サブクラスも、まずは ES6 より前のやり方から。

サブクラスとプロトタイプ

Range のサブクラスとして Span を作りたいときは、ES6 より前だとこんな感じになる:

// Range のプロトタイプから Span のプロトタイプを作る
Span.prototype = Object.create(Range.prototype);

// Range のコンストラクターじゃなくて Span を使いたいので書き換える
Span.prototype.constructor = Span;

でもスーパークラスコンストラクターやメソッドをサブクラスから呼び出すためのシンプルな方法はなかった。

extendssuper

ということで ES6。

extends

  • ES6 以降だと単に classextends を使ってスーパークラスを指定できる
  • extends を使った場合は static メソッドも継承される。これは extends の特別な機能として導入された。

new.target

super

## Ch. 10 Modules

モジュール化に関しては以前は JavaScript のビルトインサポートがなかったので

  • みんな自分で頑張ってやってて
  • Nodeが require() 関数を使ったモジュール化を使用してたんだけど
  • ES6 でそれとは違う exportimport が導入された

という流れ。

### 10.1 Module with Classes, Objects and Closures

  • クラスやオブジェクトを使えば、同じ名前のメソッドやプロパティがあってもぶつからないので、モジュールのように扱うことができる。
  • けど、内部実装の詳細をモジュールの中に隠す方法がない
  • なので、即時実行関数を使ったら内部実装の詳細をその関数に閉じ込めることができる
// aaa() や bbb() や CCC は外からは見えなくて
// Abcde だけが見える
const Abcde = (function () {
    function aaa(){
        // ...
    }
    function bbb(){
        // ...
    }
    const CCC = 1;

    return class Abcde {
        // ...
    }
}());
// 複数公開したいとき
const stats = (function () {
    function aaa(){
        // ...
    }
    function bbb(){
        // ...
    }
    const CCC = 1;

    function mean(data) {
        // ...
    }

    function stddev(data) {
        // ...
    }

    return { mean, stddev };
}());

stats.mean([1,2,3,4,5]);
stats.stddev([1,2,3,4,5]);

Closure-Based Modularity

  • そう考えると、単に JavaScript ファイルに書いてあるコードの前後に決まったテキストを入れて機械的に処理することで、モジュール化できることに気づく。
  • 例えば、ファイルの一覧を受け取ってそのコンテンツを即時実行関数で囲んで次のようにして一つの巨大なファイルを作ると:
const modules = {};
function require(moduleName) { return modules[moduleName]; }

modules["abcde.js"] = (function () {
    const exports = {};

    // abcde.js の中身
    function aaa() {
        // ...
    }

    exports.Abcde = class Abcde {
        // ...
    };

    return exports;
}());

modules["stats.js"] = (function () {
    const exports = {};

    // stats.js の中身
    exports.mean = function (data) {
        // ...
    }

    exports.stddev = function (data) {
        // ...
    }

    return exports;
}());
  • こんな風にして利用できる:
const Abcde = require("abcde.js");
const stats = require("stats.js");

let abcde = new Abcde();
let average = stats.mean([1,2,3,4,5]);
  • というのが、コードバンドリングツールのラフスケッチであり、Nodeなどで使われてる require() 関数の簡単な紹介でもある。

へー。

### 10.2 Modules in Node

Node では

  • require() 関数で import する
  • exports オブジェクトのプロパティに設定するか module.exports オブジェクト自体をリプレースするかで export する
  • 明示的に export したもの以外はそのファイル内でプライベートになる。

Node Exports

  • Node は exports というグローバル変数を定義している
  • export したかったら exports のプロパティとしてアサインすればいい:
const aaa = ...
const bbb = ...

exports.mean = data => ...
exports.stddev = function(d) {...}
  • でもだいたいの場合は export したいのは1個だけなのでそういうときは単に module.exports を使えば良い
module.exports = class Abcde { ... }
  • module.exports のデフォルト値は exports なので module.exports.mean = mean みたいに書くこともできるし、次のように書くこともできる:
module.exports = { mean, stddev }

ちょっと exportsmodule.exports の違いがよくわかんなくて公式サイトの説明を見てみた:

Modules | Node.js v14.3.0 Documentation

なるほど。

  • exportsmodule.exports のショートカットで、わざわざ module. をつけなくても良いように用意してくれてる
  • けど、 exports 自体を別のオブジェクトに書き換えてしまうと module.exportsexports が別のオブジェクトになってしまうから export されない
  • だからオブジェクト全体を書き換えなくていい場合には短く書ける exports を使えばいいし、全体を書き換えたい場合は module.exports を使えば良い

ってことか。へー。

Node Imports

  • import には require() 関数を使う
  • Node のシステムモジュールを import する場合はそのモジュールの名前を書くだけ:
const fs = require("fs");
const http = require("http");
  • 自分のモジュールを import したい場合はそのファイルからの相対パスで書く:
const stats = require('./stats.js');

(ダブルクォーテーションだったりシングルクォーテーションだったりするのは良くわからない。関係ないと思う。)

  • 絶対パスも書けるけど普通は使わない(そうだろうな
  • 拡張子( .js )を書かなくても大丈夫だけどつけるほうが一般的
  • 対象モジュールが複数のプロパティを export している場合は destructuring assignment で利用するものだけ受け取ることができる

んで ES6 で公式の JavaScript モジュールが出てきた。Node 13 では ES6 のモジュールがサポートされてるらしいけど、今のところは多くの Node のプログラムはまだ Node のモジュールを使ってる。(ふむー大変そう

### 10.3 Moudles in ES6

  • コンセプトとしては Node と同じ:export されていない限り各ファイルの中でプライベートになる
  • 違うのは構文。それと、Webブラウザーでモジュールを定義する方法も違う。

ES6 のモジュールは普通の JavaScript の "script" とはいくつかの点で異なる

  • 通常のスクリプトだとトップレベル宣言はグローバルになるけど、モジュールの場合はそれぞれのファイルがプライベートなコンテキストを持っている。
  • モジュールは自動的に strict mode になる。 'use strict' を書く必要はない。

ES6 Exports

  • 単に宣言の前に export をつければいい
export const PI = Math.PI;
export function abcde() {...}
export class Circle {...}
  • 一箇所にまとめたい場合は、それぞれの場所に export をつけるんじゃなくて、最後にひとまとめにして export すればいい
export { Circle, abcde, PI };
  • これオブジェクトのように見えるけどそうじゃなくて、 export の構文として波括弧の中でカンマ区切りのリストを受け取るようになってる
  • 1個だけ export したい場合、通常は export default を使う
export default class Abcde {
    // ...
}
  • export default を使うと import が少しだけ楽になる
  • 通常の export は名前がついてるものだけしかできないが eport default は匿名関数や匿名クラスを含むどんな式でも export できる
  • つまり export defeault はオブジェクトリテラルを export できるので、export default の次に波括弧で囲んであるものがあったら、さっきのと違ってそれは本当にオブジェクト。(へーww
  • exportexport default の両方を書くことは可能だけど普通はやらない。export default を書くならそれいっこだけ。
  • export はトップレベルにしかかけない。関数やクラスやループや分岐の中では使えない。つまり実行前に静的に決まるということ。

ES6 Imports

  • export default で export されてる場合はこんな風に書ける:
import Abcde from './abcde.js';
  • 識別子は定数になる
  • import は hoist されるけど普通は一番上に書く
  • export で export されてる場合は destructuring assignment のように書いて受け取る(「ように」ってことは実際はそうじゃないんだろうな)
import { mean, stddev } from "./stats.js";
  • スタイルガイドではちゃんと全部のシンボルを書くことが推奨されてるけど、全部まとめて import することもできる。 export が多い場合とか:
import * as stats from "./stats.js";
  • この場合 stats.mean()stats.stddev() のように stats オブジェクトを介して呼び出すことができる
  • あんまり一般的ではないけど、もし exportexport default が混ざってたらこんな風に書くことができる:
import Histogram, { mean, stddev } from "./histogram-stats.js";
  • もうひとつ別の使われ方としてこういう書き方もできる:
import "./analytics.js";
  • これはそのモジュールの関数を使う必要がない場合に使う。そのモジュールを呼び出すだけで処理をしてくれてそれでOKな場合など。
  • import するときに別名をつけることができる。名前がかぶる場合など:
import { render as renderImage } from "./imageutils.js";
import { render as renderUI } from "./ui.js";
  • export default の場合はそもそも名前を持ってないからいつでも名前をつけてるけど、さっきの例みたいに exportexport default が混ざっている場合にはこういうこともできる:
import { default as Histogram, mean, stddev } from "./histogram-stats.js";

Re-Exports

import したものを export したい場合には、こういう風にかけるが:

import { mean } from "./stats/mean.js";
import { stddev } from "./stats/stddev.js";
export { mean, stddev };

ES6 ではそれ用の特別な書き方を用意してくれてる:

export { mean } from "./stats/mean.js";
export { stddev } from "./stats/stddev.js";

ワイルドカードもいける:

export * from "./stats/mean.js";
export * from "./stats/stddev.js";

リネームもOK:

export { mean as average } from "./stats/mean.js";
export { stddev } from "./stats/stddev.js";

もし mean.js と stddev.js が export default を使っていた場合はこうなる:

export { default as mean } from "./stats/mean.js";
export { default as stddev } from "./stats/stddev.js";

逆に default として export したい場合には:

export { mean as default } from "./stats/mean.js";

Dynamic Imports with import()

  • ES2020 で導入。モジュールの動的インポート。Promise を返す。
  • クライアントWebアプリでは全部静的にインポートするより動的にインポートしたいからDOMを操作して実現してたけど、それが標準化された。
import("./stats.js").then(stats => {
    let average = stats.mean(data);
})

もしくは async 関数で使う

async analyzeData(data) {
    let stats = await import("./stats.js");
    return {
        average: stats.mean(data);
        stddev: stats.stddev(data);
    };
}
  • import の場合は文字列リテラルじゃないといけなかったけど、 import() の場合は文字列として評価される式ならなんでも大丈夫
  • import() は関数のように見えるけどそうじゃなくて operator 。
  • ブラウザだけじゃなくて、 webpack みたいなコードパッキングツールでも使える。static な import を使うと全部バンドルして大きなファイルになってしまうけど、 dynamic な import をうまく使えば、小さなバンドルに分けることができる。

なるほどねぇ。