import ステートメントで変数を使用できますか?import ステートメントで分割代入を使用できますか?eval()できますか?JavaScriptには以前からモジュールがありましたが、言語に組み込まれたものではなく、ライブラリを介して実装されていました。ES6は、JavaScriptが組み込みモジュールを持つ最初の機会です。
ES6モジュールはファイルに保存されます。ファイルごとに正確に1つのモジュールがあり、モジュールごとに1つのファイルがあります。モジュールからエクスポートする方法は2つあります。 これら2つの方法は混在させることもできますが、通常は別々に使用する方が良いでしょう。
複数の名前付きエクスポートが存在する可能性があります。
//------ lib.js ------
export const sqrt = Math.sqrt;
export function square(x) {
return x * x;
}
export function diag(x, y) {
return sqrt(square(x) + square(y));
}
//------ main.js ------
import { square, diag } from 'lib';
console.log(square(11)); // 121
console.log(diag(4, 3)); // 5
モジュール全体をインポートすることもできます。
//------ main.js ------
import * as lib from 'lib';
console.log(lib.square(11)); // 121
console.log(lib.diag(4, 3)); // 5
単一のデフォルトエクスポートが存在する可能性があります。例えば、関数。
//------ myFunc.js ------
export default function () { ··· } // no semicolon!
//------ main1.js ------
import myFunc from 'myFunc';
myFunc();
またはクラス。
//------ MyClass.js ------
export default class { ··· } // no semicolon!
//------ main2.js ------
import MyClass from 'MyClass';
const inst = new MyClass();
関数やクラス(匿名宣言)をデフォルトでエクスポートする場合、末尾にセミコロンがないことに注意してください。
| スクリプト | モジュール | |
|---|---|---|
| HTML要素 | <script> |
<script type="module"> |
| デフォルトモード | 非厳格 | 厳格 |
| トップレベルの変数は | グローバル | モジュールローカル |
トップレベルでのthisの値 |
window |
undefined |
| 実行 | 同期 | 非同期 |
宣言的なインポート(importステートメント) |
いいえ | はい |
| プログラムによるインポート(PromiseベースのAPI) | はい | はい |
| ファイル拡張子 | .js |
.js |
JavaScriptには組み込みのモジュールはありませんでしたが、コミュニティはES5以前のライブラリでサポートされている、シンプルなスタイルのモジュールに収束しています。このスタイルはES6でも採用されています。
'../model/user'):これらのパスは、インポート元のモジュールの場所を基準にして解釈されます。ファイル拡張子.jsは通常省略できます。'/lib/js/helpers'):インポートするモジュールのファイルを直接指します。'util'):モジュール名が何を参照するかを設定する必要があります。このモジュールのアプローチはグローバル変数を回避し、グローバルなのはモジュール指定子のみです。
言語からの明示的なサポートなしにES5モジュールシステムがどれほどうまく機能するかは印象的です。最も重要(そして残念ながら互換性がない)な2つの標準は次のとおりです。
上記はES5モジュールの簡単な説明にすぎません。より詳細な資料が必要な場合は、Addy Osmaniによる「AMD、CommonJS、ESハーモニーを使用したモジュラーJavaScriptの記述」をご覧ください。
ECMAScript 6モジュールの目標は、CommonJSとAMDの両方のユーザーが満足できる形式を作成することでした。
言語に組み込まれているため、ES6モジュールはCommonJSとAMDを超えることができます(詳細は後で説明します)。
ES6モジュール標準には2つの部分があります。
エクスポートには、名前付きエクスポート(モジュールごとに複数)とデフォルトエクスポート(モジュールごとに1つ)の2種類があります。後で説明するように、両方を同時に使用することは可能ですが、通常は別々に保つのが最善です。
モジュールは、宣言の前にキーワードexportを付けることで、複数のものをエクスポートできます。これらのエクスポートは名前で区別され、名前付きエクスポートと呼ばれます。
//------ lib.js ------
export const sqrt = Math.sqrt;
export function square(x) {
return x * x;
}
export function diag(x, y) {
return sqrt(square(x) + square(y));
}
//------ main.js ------
import { square, diag } from 'lib';
console.log(square(11)); // 121
console.log(diag(4, 3)); // 5
名前付きエクスポートを指定する他の方法(後で説明します)もありますが、これは非常に便利だと思います。まるで外部の世界がないかのようにコードを記述し、エクスポートしたいものすべてにキーワードでラベルを付けるだけです。
必要に応じて、モジュール全体をインポートし、プロパティ表記を使用して名前付きエクスポートを参照することもできます。
//------ main.js ------
import * as lib from 'lib';
console.log(lib.square(11)); // 121
console.log(lib.diag(4, 3)); // 5
CommonJS構文での同じコード:しばらくの間、Node.jsでのモジュールエクスポートを冗長にしないようにいくつかの巧妙な戦略を試しました。今では、モジュールパターンを明らかにすることを彷彿とさせる、以下に示すシンプルですが少し冗長なスタイルを好んでいます。
//------ lib.js ------
var sqrt = Math.sqrt;
function square(x) {
return x * x;
}
function diag(x, y) {
return sqrt(square(x) + square(y));
}
module.exports = {
sqrt: sqrt,
square: square,
diag: diag,
};
//------ main.js ------
var square = require('lib').square;
var diag = require('lib').diag;
console.log(square(11)); // 121
console.log(diag(4, 3)); // 5
単一の値をエクスポートするだけのモジュールは、Node.jsコミュニティで非常に人気があります。しかし、フロントエンド開発でも一般的です。モデルやコンポーネントのクラスがあり、モジュールごとに1つのクラスがあることがよくあります。ES6モジュールは、メインのエクスポート値であるデフォルトエクスポートを選択できます。デフォルトエクスポートは、特にインポートが簡単です。
次のECMAScript 6モジュールは単一の関数「です」。
//------ myFunc.js ------
export default function () {} // no semicolon!
//------ main1.js ------
import myFunc from 'myFunc';
myFunc();
デフォルトエクスポートがクラスであるECMAScript 6モジュールは次のようになります。
//------ MyClass.js ------
export default class {} // no semicolon!
//------ main2.js ------
import MyClass from 'MyClass';
const inst = new MyClass();
デフォルトエクスポートには2つのスタイルがあります。
関数宣言(またはジェネレーター関数宣言)またはクラス宣言の前にキーワードexport defaultを付けると、デフォルトエクスポートにすることができます。
export default function foo() {} // no semicolon!
export default class Bar {} // no semicolon!
この場合、名前を省略することもできます。これにより、デフォルトエクスポートは、JavaScriptに匿名関数宣言と匿名クラス宣言がある唯一の場所になります。
export default function () {} // no semicolon!
export default class {} // no semicolon!
前の2行のコードを見ると、export default のオペランドは式であると予想するでしょう。それらが宣言であるのは、一貫性のためだけです。オペランドは名前付き宣言にすることができ、匿名バージョンを式として解釈すると(新しい種類の宣言を導入するよりもさらに)混乱を招くからです。
オペランドを式として解釈させたい場合は、括弧を使用する必要があります。
export default (function () {});
export default (class {});
値は式によって生成されます。
export default 'abc';
export default foo();
export default /^xyz$/;
export default 5 * 7;
export default { no: false, yes: true };
これらの各デフォルトエクスポートは、次の構造を持っています。
export default «expression»;
これは次と同等です。
const __default__ = «expression»;
export { __default__ as default }; // (A)
A行のステートメントは、エクスポート句です(これは後のセクションで説明します)。
2番目のデフォルトエクスポートスタイルが導入されたのは、変数宣言が複数の変数を宣言する場合、意味のあるデフォルトエクスポートに変換できないためです。
export default const foo = 1, bar = 2, baz = 3; // not legal JavaScript!
3つの変数 foo、bar、baz のうち、どれがデフォルトエクスポートになるでしょうか?
後で詳しく説明するように、ES6モジュールの構造は静的であり、条件付きでインポートやエクスポートを行うことはできません。これにより、さまざまなメリットが得られます。
この制限は、モジュールのトップレベルでのみインポートとエクスポートを許可することで、構文的に強制されます。
if (Math.random()) {
import 'foo'; // SyntaxError
}
// You can’t even nest `import` and `export`
// inside a simple block:
{
import 'foo'; // SyntaxError
}
モジュールのインポートは巻き上げられます(内部的に現在のスコープの先頭に移動されます)。したがって、モジュール内でどこで言及しても問題はなく、次のコードは問題なく動作します。
foo();
import { foo } from 'my_module';
ES6モジュールのインポートは、エクスポートされたエンティティの読み取り専用ビューです。つまり、モジュール本体内で宣言された変数への接続は、次のコードで示されるように、ライブのままになります。
//------ lib.js ------
export let counter = 3;
export function incCounter() {
counter++;
}
//------ main.js ------
import { counter, incCounter } from './lib';
// The imported value `counter` is live
console.log(counter); // 3
incCounter();
console.log(counter); // 4
その仕組みについては、後のセクションで説明します。
ビューとしてのインポートには、次の利点があります。
2つのモジュールAとBが、Aが(間接的/推移的に)Bをインポートし、BがAをインポートする場合、互いに循環的に依存しています。可能であれば、循環依存関係は避けるべきです。循環依存関係は、AとBが密結合になるため、一緒に使用および発展させることしかできません。
では、なぜ循環依存関係をサポートするのでしょうか?時折、それらを回避できないことがあり、それがそれらのサポートが重要な機能である理由です。後のセクションでより詳細な情報が提供されています。
CommonJSとECMAScript 6が循環依存関係をどのように処理するかを見てみましょう。
次のCommonJSコードは、互いに循環的に依存する2つのモジュールaとbを正しく処理します。
//------ a.js ------
var b = require('b');
function foo() {
b.bar();
}
exports.foo = foo;
//------ b.js ------
var a = require('a'); // (i)
function bar() {
if (Math.random()) {
a.foo(); // (ii)
}
}
exports.bar = bar;
モジュールaが最初にインポートされる場合、i行では、モジュールbは、エクスポートが追加される前にaのエクスポートオブジェクトを取得します。したがって、bはそのトップレベルでa.fooにアクセスできませんが、aの実行が終了すると、そのプロパティが存在します。その後bar()が呼び出されると、ii行のメソッド呼び出しが機能します。
一般的なルールとして、循環依存関係がある場合、モジュールの本体でインポートにアクセスできないことを覚えておいてください。それは現象に固有のものであり、ECMAScript 6モジュールで変更されることはありません。
CommonJSアプローチの制限事項は次のとおりです。
module.exports = function () { ··· };
モジュールaがそれを行った場合、モジュールbの変数aは、代入が行われた後に更新されません。元のエクスポートオブジェクトを参照し続けます。
bは、次のようにfooをインポートできません。 var foo = require('a').foo;
fooは単にundefinedになります。言い換えれば、a.foo経由でfooを参照する以外に選択肢はありません。
これらの制限は、エクスポート側とインポート側の両方が循環依存関係を認識し、明示的にサポートする必要があることを意味します。
ES6モジュールは、循環依存関係を自動的にサポートします。つまり、前のセクションで言及したCommonJSモジュールの2つの制限事項はありません。デフォルトエクスポートと、非修飾の名前付きインポート(次の例のi行とiii行)が機能します。したがって、互いに循環的に依存するモジュールを次のように実装できます。
//------ a.js ------
import {bar} from 'b'; // (i)
export function foo() {
bar(); // (ii)
}
//------ b.js ------
import {foo} from 'a'; // (iii)
export function bar() {
if (Math.random()) {
foo(); // (iv)
}
}
このコードが機能するのは、前のセクションで説明したように、インポートがエクスポートのビューであるためです。つまり、非修飾インポート(ii行のbarやiv行のfooなど)でさえ、元のデータを参照する間接参照です。したがって、循環依存関係に直面した場合、名前付きエクスポートに非修飾インポート経由でアクセスするか、モジュール経由でアクセスするかは問題ではありません。どちらの場合も間接参照が関与し、常に機能します。
ECMAScript 6には、いくつかのインポートスタイルがあります2
import localName from 'src/my_lib';
import * as my_lib from 'src/my_lib';
import { name1, name2 } from 'src/my_lib';
名前付きインポートの名前を変更できます。
// Renaming: import `name1` as `localName1`
import { name1 as localName1, name2 } from 'src/my_lib';
// Renaming: import the default export as `foo`
import { default as foo } from 'src/my_lib';
import 'src/my_lib';
これらのスタイルを組み合わせる方法は2つしかなく、表示される順序は固定されています。デフォルトエクスポートは常に最初にきます。
import theDefault, * as my_lib from 'src/my_lib';
import theDefault, { name1, name2 } from 'src/my_lib';
モジュール内で名前付きのものをエクスポートするには、2つの方法があります。
1つは、キーワードexportで宣言をマークすることです。
export var myVar1 = ···;
export let myVar2 = ···;
export const MY_CONST = ···;
export function myFunc() {
···
}
export function* myGeneratorFunc() {
···
}
export class MyClass {
···
}
もう1つは、モジュールの最後にエクスポートしたいものをすべてリストすることです(これは、リビーリングモジュールパターンにスタイルが似ています)。
const MY_CONST = ···;
function myFunc() {
···
}
export { MY_CONST, myFunc };
異なる名前でエクスポートすることもできます。
export { MY_CONST as FOO, myFunc };
再エクスポートとは、別のモジュールのエクスポートを現在のモジュールのエクスポートに追加することを意味します。別のモジュールのエクスポートをすべて追加することもできます。
export * from 'src/other_module';
デフォルトエクスポートはexport *によって3無視されます。
または、より選択的になることもできます(オプションで名前を変更しながら)。
export { foo, bar } from 'src/other_module';
// Renaming: export other_module’s foo as myFoo
export { foo as myFoo, bar } from 'src/other_module';
次のステートメントは、別のモジュールfooのデフォルトエクスポートを、現在のモジュールのデフォルトエクスポートにします。
export { default } from 'foo';
次のステートメントは、モジュールfooの名前付きエクスポートmyFuncを、現在のモジュールのデフォルトエクスポートにします。
export { myFunc as default } from 'foo';
ECMAScript 6には、いくつかのエクスポートスタイルがあります4
export * from 'src/other_module';
export { foo as myFoo, bar } from 'src/other_module';
export { default } from 'src/other_module';
export { default as foo } from 'src/other_module';
export { foo as default } from 'src/other_module';
export { MY_CONST as FOO, myFunc };
export { foo as default };
export var foo;
export let foo;
export const foo;
export function myFunc() {}
export function* myGenFunc() {}
export class MyClass {}
export default function myFunc() {}
export default function () {}
export default function* myGenFunc() {}
export default function* () {}
export default class MyClass {}
export default class {}
export default foo;
export default 'Hello world!';
export default 3 * 7;
export default (function () {});
次のパターンは、JavaScriptでは驚くほど一般的です。ライブラリは単一の関数ですが、追加のサービスはその関数のプロパティを介して提供されます。例としては、jQueryとUnderscore.jsがあります。以下は、CommonJSモジュールとしてのUnderscoreのスケッチです。
//------ underscore.js ------
var _ = function (obj) {
···
};
var each = _.each = _.forEach =
function (obj, iterator, context) {
···
};
module.exports = _;
//------ main.js ------
var _ = require('underscore');
var each = _.each;
···
ES6の観点から見ると、関数_はデフォルトエクスポートであり、eachとforEachは名前付きエクスポートです。実際には、名前付きエクスポートとデフォルトエクスポートを同時に持つことができます。例として、前のCommonJSモジュールをES6モジュールとして書き換えると、次のようになります。
//------ underscore.js ------
export default function (obj) {
···
}
export function each(obj, iterator, context) {
···
}
export { each as forEach };
//------ main.js ------
import _, { each } from 'underscore';
···
CommonJSバージョンとECMAScript 6バージョンは、おおまかに似ているだけであることに注意してください。後者はフラットな構造を持ち、前者はネストされています。
私は一般的に、2種類のエクスポートを分けておくことを推奨します。モジュールごとに、デフォルトエクスポートのみを持つか、名前付きエクスポートのみを持つかのどちらかにします。
ただし、それは非常に強い推奨事項ではありません。場合によっては、2種類を混在させるのが理にかなう場合があります。1つの例は、エンティティをデフォルトエクスポートするモジュールです。ユニットテストでは、名前付きエクスポートを介して内部の一部を追加で利用可能にすることができます。
デフォルトエクスポートは、実際には、特別な名前defaultを持つ名前付きエクスポートにすぎません。つまり、次の2つのステートメントは同等です。
import { default as foo } from 'lib';
import foo from 'lib';
同様に、次の2つのモジュールは同じデフォルトエクスポートを持っています。
//------ module1.js ------
export default function foo() {} // function declaration!
//------ module2.js ------
function foo() {}
export { foo as default };
default:エクスポート名としてはOKだが、変数名としてはNG 予約語(defaultやnewなど)を変数名として使用することはできませんが、エクスポートの名前として使用することはできます(ECMAScript 5ではプロパティ名として使用することもできます)。このような名前付きエクスポートを直接インポートする場合は、適切な変数名に名前を変更する必要があります。
つまり、defaultは、名前変更インポートの左側にのみ表示できます。
import { default as foo } from 'some_module';
そして、名前変更エクスポートの右側にのみ表示できます。
export { foo as default };
再エクスポートでは、asの両側がエクスポート名です。
export { myFunc as default } from 'foo';
export { default as otherFunc } from 'foo';
// The following two statements are equivalent:
export { default } from 'foo';
export { default as default } from 'foo';
モジュールを操作するための宣言型構文に加えて、プログラムによるAPIもあります。これにより、次のことが可能になります。
ローダーは、モジュール指定子(import-fromの末尾にある文字列ID)の解決、モジュールの読み込みなどを処理します。そのコンストラクタはReflect.Loaderです。各プラットフォームは、グローバル変数System(システムローダー)にデフォルトのインスタンスを保持しており、これは特定のモジュール読み込みスタイルを実装しています。
Promisesに基づくAPIを介して、プログラムでモジュールをインポートできます。
System.import('some_module')
.then(some_module => {
// Use some_module
})
.catch(error => {
···
});
System.import()を使用すると、以下のことができます。
<script>要素内でモジュールを使用する(モジュール構文がサポートされていない場合。詳細については、モジュールとスクリプトに関するセクションを参照してください)。System.import()は単一のモジュールを取得します。複数のモジュールをインポートするには、Promise.all()を使用できます。
Promise.all(
['module1', 'module2', 'module3']
.map(x => System.import(x)))
.then(([module1, module2, module3]) => {
// Use module1, module2, module3
});
ローダーには他にもメソッドがあります。重要なものが3つあります。
System.module(source, options?)source内のJavaScriptコードをモジュールに評価します(Promise経由で非同期的に配信されます)。System.set(name, module)System.module()で作成したもの)。System.define(name, source, options?)source内のモジュールコードを評価し、結果を登録します。モジュールローダーAPIには、読み込みプロセスを設定するためのさまざまなフックがあります。ユースケースには以下が含まれます。
設定可能なモジュール読み込みは、Node.jsとCommonJSが制限されている領域です。
ブラウザでES6モジュールがどのようにサポートされているかを見てみましょう。
ブラウザには、スクリプトとモジュールの2種類のエンティティがあります。構文がわずかに異なり、動作も異なります。
これは相違点の概要であり、詳細は後で説明します。
| スクリプト | モジュール | |
|---|---|---|
| HTML要素 | <script> |
<script type="module"> |
| デフォルトモード | 非厳格 | 厳格 |
| トップレベルの変数は | グローバル | モジュールローカル |
トップレベルでのthisの値 |
window |
undefined |
| 実行 | 同期 | 非同期 |
宣言的なインポート(importステートメント) |
いいえ | はい |
| プログラムによるインポート(PromiseベースのAPI) | はい | はい |
| ファイル拡張子 | .js |
.js |
スクリプトは、JavaScriptを埋め込んだり、外部JavaScriptファイルを参照したりするための従来のブラウザの方法です。スクリプトには、インターネットメディアタイプがあり、以下のように使用されます。
<script>要素の属性typeの値。HTML5の場合、<script>要素にJavaScriptが含まれているか参照している場合は、type属性を省略することが推奨されています。以下が最も重要な値です。
text/javascript: これはレガシー値であり、スクリプトタグでtype属性を省略した場合のデフォルトとして使用されます。Internet Explorer 8以前では最も安全な選択肢です。application/javascript: 現在のブラウザでは推奨されています。スクリプトは通常、同期的にロードまたは実行されます。JavaScriptスレッドは、コードがロードまたは実行されるまで停止します。
JavaScriptの通常のrun-to-completionセマンティクスに沿って、モジュールの本体は中断することなく実行される必要があります。そのため、モジュールをインポートするには2つのオプションがあります。
ECMAScript 6は、両方の長所を備えています。Node.jsの同期構文と、AMDの非同期ロードです。両方を可能にするために、ES6モジュールはNode.jsモジュールよりも構文的に柔軟性が低くなっています。インポートとエクスポートは、トップレベルで発生する必要があります。つまり、条件付きにすることもできません。この制限により、ES6モジュールローダーは、モジュールによってインポートされるモジュールを静的に分析し、その本体を実行する前にロードできます。
スクリプトの同期的な性質により、スクリプトがモジュールになるのを防ぎます。スクリプトは、モジュールを宣言的にインポートすることもできません(そうする場合は、プログラムによるモジュールローダーAPIを使用する必要があります)。
モジュールは、完全に非同期の新しい<script>要素のバリアントを介してブラウザから使用できます。
<script type="module">
import $ from 'lib/jquery';
var x = 123;
// The current scope is not global
console.log('$' in window); // false
console.log('x' in window); // false
// `this` is undefined
console.log(this === undefined); // true
</script>
ご覧のとおり、この要素には独自のスコープがあり、その「内部」の変数はそのスコープにローカルです。モジュールコードは暗黙的にstrictモードであることに注意してください。これは素晴らしいニュースです。もう'use strict'は必要ありません。
通常の<script>要素と同様に、<script type="module">を使用して外部モジュールをロードすることもできます。たとえば、次のタグは、mainモジュールを介してWebアプリケーションを起動します(属性名importは私が発明したものであり、まだどの名前が使用されるかは不明です)。
<script type="module" import="impl/main"></script>
カスタム<script>タイプを介してHTMLでモジュールをサポートする利点は、ポリフィル(ライブラリ)を介して古いエンジンにそのサポートを簡単に導入できることです。最終的に、モジュール専用の要素(例:<module>)が存在する可能性があります。
ファイルがモジュールであるかスクリプトであるかは、インポートまたはロードされる方法によってのみ決定されます。ほとんどのモジュールにはインポートまたはエクスポートのいずれかがあり、したがって検出できます。ただし、モジュールにどちらもない場合は、スクリプトと区別できません。例えば
var x = 123;
このコードのセマンティクスは、モジュールとして解釈されるかスクリプトとして解釈されるかによって異なります。
xはモジュールスコープで作成されます。xはグローバル変数になり、グローバルオブジェクト(ブラウザではwindow)のプロパティになります。より現実的な例は、何かをインストールするモジュール、例えば、グローバル変数またはグローバルイベントリスナーにポリフィルをインストールするモジュールです。このようなモジュールは、何もインポートもエクスポートもせず、空のインポートを介してアクティブ化されます。
import './my_module';
インポートはCommonJSとES6で動作が異なります。
次のセクションでは、それが何を意味するのかを説明します。
CommonJS(Node.js)モジュールでは、比較的使い慣れた方法で動作します。
変数値を変数にインポートする場合、値は2回コピーされます。1回はエクスポート時(A行)、もう1回はインポート時(B行)です。
//------ lib.js ------
var counter = 3;
function incCounter() {
counter++;
}
module.exports = {
counter: counter, // (A)
incCounter: incCounter,
};
//------ main1.js ------
var counter = require('./lib').counter; // (B)
var incCounter = require('./lib').incCounter;
// The imported value is a (disconnected) copy of a copy
console.log(counter); // 3
incCounter();
console.log(counter); // 3
// The imported value can be changed
counter++;
console.log(counter); // 4
エクスポートオブジェクトを介して値にアクセスする場合でも、エクスポート時に1回コピーされます。
//------ main2.js ------
var lib = require('./lib');
// The imported value is a (disconnected) copy
console.log(lib.counter); // 3
lib.incCounter();
console.log(lib.counter); // 3
// The imported value can be changed
lib.counter++;
console.log(lib.counter); // 4
CommonJSとは対照的に、インポートはエクスポートされた値に対するビューです。言い換えれば、すべてのインポートはエクスポートされたデータへのライブ接続です。インポートは読み取り専用です。
import x from 'foo')は、constで宣言された変数のようなものです。foo(import * as foo from 'foo')のプロパティは、凍結されたオブジェクトのプロパティのようなものです。次のコードは、インポートがビューのようなものであることを示しています。
//------ lib.js ------
export let counter = 3;
export function incCounter() {
counter++;
}
//------ main1.js ------
import { counter, incCounter } from './lib';
// The imported value `counter` is live
console.log(counter); // 3
incCounter();
console.log(counter); // 4
// The imported value can’t be changed
counter++; // TypeError
アスタリスク(*)を介してモジュールオブジェクトをインポートすると、同じ結果が得られます。
//------ main2.js ------
import * as lib from './lib';
// The imported value `counter` is live
console.log(lib.counter); // 3
lib.incCounter();
console.log(lib.counter); // 4
// The imported value can’t be changed
lib.counter++; // TypeError
インポートの値を変更することはできませんが、インポートが参照しているオブジェクトを変更できることに注意してください。例えば
//------ lib.js ------
export let obj = {};
//------ main.js ------
import { obj } from './lib';
obj.prop = 123; // OK
obj = {}; // TypeError
確立された慣行から逸脱する、比較的複雑なインポートのメカニズムを導入したのはなぜですか?
私の経験では、ES6のimportはただ機能するだけで、内部で何が起こっているかを気にする必要はほとんどありません。
importは、内部でどのようにexportのビューとして機能するのでしょうか? exportは、exportエントリというデータ構造を介して管理されます。すべてのexportエントリ(再exportのエントリを除く)には、次の2つの名前があります。
エンティティをimportした後、そのエンティティは常にモジュールとローカル名の2つのコンポーネントを持つポインタを介してアクセスされます。言い換えれば、そのポインタはモジュール内のバインディング(変数の格納場所)を参照します。
さまざまな種類のexportによって作成されたexport名とローカル名を見てみましょう。次の表(ES6仕様から引用)に概要を示し、以降のセクションで詳細を説明します。
| ステートメント | ローカル名 | エクスポート名 |
|---|---|---|
export {v}; |
'v' |
'v' |
export {v as x}; |
'v' |
'x' |
export const v = 123; |
'v' |
'v' |
export function f() {} |
'f' |
'f' |
export default function f() {} |
'f' |
'default' |
export default function () {} |
'*default*' |
'default' |
export default 123; |
'*default*' |
'default' |
function foo() {}
export { foo };
foofoofunction foo() {}
export { foo as bar };
foobarこれはインラインexportです。
export function foo() {}
これは次のコードと同等です。
function foo() {}
export { foo };
したがって、次の名前があります。
foofooデフォルトexportには2種類あります。
次のコードは、式123の結果をデフォルトexportします。
export default 123;
これは次のものと同等です。
const *default* = 123; // *not* legal JavaScript
export { *default* as default };
式をデフォルトexportすると、次のようになります。
*default*defaultローカル名は、他のローカル名と競合しないように選択されました。
デフォルトexportでもバインディングが作成されることに注意してください。ただし、*default*は有効な識別子ではないため、モジュール内からそのバインディングにアクセスすることはできません。
次のコードは、関数宣言をデフォルトexportします。
export default function foo() {}
これは次のものと同等です。
function foo() {}
export { foo as default };
名前は次のとおりです。
foodefaultつまり、fooに異なる値を代入することで、モジュール内からデフォルトexportの値を変更できます。
デフォルトexportの場合(のみ)、関数宣言の名前を省略することもできます。
export default function () {}
これは次と同等です。
function *default*() {} // *not* legal JavaScript
export { *default* as default };
名前は次のとおりです。
*default*defaultジェネレーター宣言とクラス宣言のデフォルトexportは、関数宣言のデフォルトexportと同様に機能します。
このセクションでは、ECMAScript 2015(ES6)言語仕様へのポインタを示します。
importの管理
さまざまな種類のexportによって作成されるexport名とローカル名は、「Source Text Module Records」セクションの表42に示されています。 「Static Semantics: ExportEntries」セクションには、より詳細な情報があります。 exportエントリは静的に(モジュールを評価する前に)設定されることがわかります。exportステートメントの評価は、「Runtime Semantics: Evaluation」セクションで説明されています。
ECMAScript 6モジュールを理解するには、その設計に影響を与えた目標を理解すると役立ちます。主なものは次のとおりです。
次のサブセクションでは、これらの目標について説明します。
デフォルトexportがモジュール「である」ことを示唆するモジュール構文は少し奇妙に見えるかもしれませんが、デフォルトexportをできるだけ便利にすることが主要な設計目標の1つであったことを考えると理にかなっています。David Hermanの言葉を引用すると
ECMAScript 6は、単一/デフォルトexportスタイルを優先し、デフォルトをimportするための最も洗練された構文を提供します。名前付きexportのimportは、わずかに簡潔でなくてもかまいません。
現在のJavaScriptモジュール形式は動的な構造を持っています。importおよびexportされるものは、実行時に変更できます。 ES6が独自のモジュール形式を導入した理由の1つは、静的な構造を有効にすることです。これにはいくつかの利点があります。しかし、それらの利点に入る前に、静的な構造が何を意味するのかを調べてみましょう。
つまり、コンパイル時(静的に)にimportとexportを決定できるということです。ソースコードを見るだけでよく、実行する必要はありません。 ES6はこれを構文的に強制します。最上位レベルでのみimportおよびexportでき(条件文の中にネストすることはできません)。また、importおよびexportステートメントには動的な部分(変数などは許可されていません)がありません。
以下は、静的な構造を持たないCommonJSモジュールの2つの例です。最初の例では、何がimportされるかを知るにはコードを実行する必要があります。
var my_lib;
if (Math.random()) {
my_lib = require('foo');
} else {
my_lib = require('bar');
}
2番目の例では、何がexportされるかを知るにはコードを実行する必要があります。
if (Math.random()) {
exports.baz = ···;
}
ECMAScript 6モジュールは柔軟性が低く、静的であることを強制します。その結果、いくつかの利点が得られます。これについては次に説明します。
フロントエンド開発では、モジュールは通常次のように処理されます。
バンドルの理由は次のとおりです。
理由#1は、ファイルをリクエストするコストが比較的高いHTTP/1で重要です。これはHTTP/2で変更されるため、この理由はこの時点では重要ではありません。
理由#3は依然として説得力があります。これは、静的な構造を持つモジュール形式でのみ実現できます。
モジュールバンドラーRollupは、ES6モジュールを効率的に組み合わせることができることを証明しました。これは、名前の衝突を解消するために変数の名前を変更した後、すべてが単一のスコープに収まるためです。 これは、ES6モジュールの2つの特性により可能です。
例として、次の2つのES6モジュールを考えてください。
// lib.js
export function foo() {}
export function bar() {}
// main.js
import {foo} from './lib.js';
console.log(foo());
Rollupは、これら2つのES6モジュールを次の単一のES6モジュールにバンドルできます(未使用のexportbarが削除されていることに注意してください)。
function foo() {}
console.log(foo());
Rollupのアプローチのもう1つの利点は、バンドルがカスタム形式を持っておらず、単なるES6モジュールであることです。
CommonJSでライブラリをrequireすると、オブジェクトが返されます。
var lib = require('lib');
lib.someFunc(); // property lookup
したがって、lib.someFuncを介して名前付きexportにアクセスするということは、プロパティのルックアップを実行する必要があることを意味し、これは動的であるため低速です。
対照的に、ES6でライブラリをimportすると、その内容を静的に認識し、アクセスを最適化できます。
import * as lib from 'lib';
lib.someFunc(); // statically resolved
静的なモジュール構造では、モジュール内の任意の場所でどの変数が表示されるかを常に静的に把握できます。
これは、特定の識別子が正しくスペルされているかどうかを確認するのに非常に役立ちます。この種のチェックは、JSLintやJSHintなどのリンターで一般的な機能です。ECMAScript 6では、その大部分をJavaScriptエンジンで実行できます。
さらに、名前付きインポート(lib.fooなど)へのアクセスも静的にチェックできます。
マクロはまだJavaScriptの将来のロードマップに載っています。JavaScriptエンジンがマクロをサポートしている場合、ライブラリを介して新しい構文を追加できます。Sweet.jsは、JavaScriptの実験的なマクロシステムです。以下は、Sweet.jsのWebサイトからの例です。クラスのマクロです。
// Define the macro
macro class {
rule {
$className {
constructor $cparams $cbody
$($mname $mparams $mbody) ...
}
} => {
function $className $cparams $cbody
$($className.prototype.$mname
= function $mname $mparams $mbody; ) ...
}
}
// Use the macro
class Person {
constructor(name) {
this.name = name;
}
say(msg) {
console.log(this.name + " says: " + msg);
}
}
var bob = new Person("Bob");
bob.say("Macros are sweet!");
マクロの場合、JavaScriptエンジンはコンパイルの前に前処理ステップを実行します。パーサーによって生成されたトークンストリーム内のトークンのシーケンスが、マクロのパターン部分と一致する場合、それはマクロの本体を介して生成されたトークンで置き換えられます。前処理ステップは、マクロ定義を静的に見つけることができる場合にのみ機能します。したがって、モジュールを介してマクロをインポートする場合は、静的な構造が必要です。
静的型チェックは、マクロと同様の制約を課します。型定義を静的に見つけることができる場合にのみ実行できます。繰り返しますが、型は静的な構造を持っている場合にのみモジュールからインポートできます。
型は、パフォーマンスが重要なコードを記述できる、静的型付けされた高速なJavaScript方言を可能にするため魅力的です。そのような方言の1つがLow-Level JavaScript(LLJS)です。
マクロと静的型を持つ言語をJavaScriptにコンパイルすることをサポートしたい場合、前の2つのセクションで述べた理由から、JavaScriptのモジュールは静的な構造を持つ必要があります。
ECMAScript 6モジュールは、エンジンがモジュールを同期的にロード(サーバーなど)するか非同期的にロード(ブラウザーなど)するかに関係なく機能する必要があります。その構文は同期ロードに適しており、非同期ロードはその静的な構造によって有効になります。すべてのインポートを静的に決定できるため、モジュールの本体を評価する前に(AMDモジュールを彷彿とさせる方法で)それらをロードできます。
循環依存関係のサポートは、ES6モジュールの重要な目標でした。理由は次のとおりです。
循環依存関係は本質的に悪いものではありません。特にオブジェクトの場合、このような依存関係が必要になることもあります。たとえば、一部のツリー(DOMドキュメントなど)では、親が子を参照し、子が親を参照し返します。ライブラリでは、通常、注意深く設計することで循環依存関係を回避できます。ただし、大規模なシステムでは、特にリファクタリング中に発生する可能性があります。その場合、リファクタリング中にシステムが壊れないため、モジュールシステムがそれらをサポートしていると非常に役立ちます。
Node.jsのドキュメントでは、循環依存関係の重要性を認めており、Rob Sayreが追加の証拠を提供しています。
データポイント:以前、私はFirefox用に[ECMAScript 6モジュール]のようなシステムを実装しました。出荷後3週間で、循環依存関係のサポートを求められました。
Alex Fritzeが発明し、私が取り組んだそのシステムは完璧ではなく、構文もあまりきれいではありません。しかし、7年経った今でも使用されているため、何か正しいことをしたに違いありません。
importステートメントは完全に静的です。そのモジュール指定子は常に固定されています。ロードするモジュールを動的に決定する場合は、プログラムによるローダーAPIを使用する必要があります。
const moduleSpecifier = 'module_' + Math.random();
System.import(moduleSpecifier)
.then(the_module => {
// Use the_module
})
importステートメントは常にモジュールの最上位レベルにある必要があります。つまり、ifステートメントや関数などの内部にネストすることはできません。したがって、モジュールを条件付きまたはオンデマンドでロードする場合は、プログラムによるローダーAPIを使用する必要があります。
if (Math.random()) {
System.import('some_module')
.then(some_module => {
// Use some_module
})
}
importステートメントで変数を使用できますか? いいえ、できません。インポートされるものは、実行時に計算されるものに依存してはならないことを忘れないでください。したがって、
// Illegal syntax:
import foo from 'some_module'+SUFFIX;
importステートメントで分割代入を使用できますか? いいえ、できません。importステートメントは分割代入のように見えますが、完全に異なります(静的、インポートはビューなど)。
したがって、ES6ではこのようなことはできません。
// Illegal syntax:
import { foo: { bar } } from 'some_module';
CommonJSのように、オブジェクトをデフォルトでエクスポートできるのに、なぜ名前付きエクスポートが必要なのか疑問に思うかもしれません。答えは、オブジェクトを介して静的な構造を強制することはできず、関連するすべての利点を失うためです(これについてはこの章で説明します)。
eval()できますか? いいえ、できません。モジュールはeval()するには高すぎるレベルの構成要素です。モジュールローダーAPIは、文字列からモジュールを作成する手段を提供します。構文的に、eval()はスクリプト(importとexportを許可しない)を受け入れ、モジュールは受け入れません。
一見、ECMAScript 6に組み込まれたモジュールを持つことは、退屈な機能のように見えるかもしれません。結局のところ、すでにいくつかの優れたモジュールシステムがあります。しかし、ECMAScript 6モジュールにはいくつかの新機能があります。
ES6モジュールは、現在主流の標準であるCommonJSとAMD間の断片化も終えることが期待されています。モジュールの単一のネイティブ標準を持つことは、
navigatorのプロパティではなく、モジュールになります。MathやJSONなどのオブジェクトは、ECMAScript 5で関数の名前空間として機能します。将来的には、そのような機能はモジュールを介して提供できます。