ぼーっと今日も。半分くらいきたかな。
ちょこちょこ勉強しながら「分かるようになってるのかなぁ?」とか思ってたけど、今日たまたま機会があって読んだら 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;
でもスーパークラスのコンストラクターやメソッドをサブクラスから呼び出すためのシンプルな方法はなかった。
extends
と super
ということで ES6。
extends
- ES6 以降だと単に
class
にextends
を使ってスーパークラスを指定できる extends
を使った場合は static メソッドも継承される。これはextends
の特別な機能として導入された。
new.target
new
キーワードを使わずに呼び出された関数ではundefined
になるけど、コンストラクター関数の中だと呼び出された関数の参照が入ってる。- これを使うと、スーパークラスのコンストラクターの中でサブクラスのコンストラクター関数を取得することができる。スーパークラスがサブクラスを知る必要はないので基本的には使うことはないけどログに
new.target.name
を出力できるのは便利かも。
super
- スーパークラスのコンストラクターは
super()
で、メソッドはsuper.methodName()
で呼び出すことができる extends
した場合はコンストラクター関数でsuper()
を必ず呼び出さないといけない。- コンストラクターを定義していない場合は、自動で定義されてその中で呼び出される。
super()
はコンストラクターの最初で呼び出さなくても大丈夫。だけどthis
を使うのはsuper()
を呼び出した後にすること。
## Ch. 10 Modules
モジュール化に関しては以前は JavaScript のビルトインサポートがなかったので
- みんな自分で頑張ってやってて
- Nodeが
require()
関数を使ったモジュール化を使用してたんだけど - ES6 でそれとは違う
export
とimport
が導入された
という流れ。
### 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
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 }
ちょっと exports
と module.exports
の違いがよくわかんなくて公式サイトの説明を見てみた:
Modules | Node.js v14.3.0 Documentation
なるほど。
exports
はmodule.exports
のショートカットで、わざわざmodule.
をつけなくても良いように用意してくれてる- けど、
exports
自体を別のオブジェクトに書き換えてしまうとmodule.exports
とexports
が別のオブジェクトになってしまうから 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 export
とexport 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
オブジェクトを介して呼び出すことができる - あんまり一般的ではないけど、もし
export
とexport 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
の場合はそもそも名前を持ってないからいつでも名前をつけてるけど、さっきの例みたいにexport
とexport 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 をうまく使えば、小さなバンドルに分けることができる。
なるほどねぇ。