30歳からのプログラミング

30歳無職から独学でプログラミングを開始した人間の記録。

ECMAScript(ES2015,Babel)におけるモジュールについて

JavaScriptには元来、モジュール機能が存在しなかった。
そこで、Node.jsではmodule.exportsという機能を導入してモジュール機能を実現し、さらに、その機能をブラウザでも利用できるようにするためのツールとしてbrowserifyなどが開発された。

そういった話については以下を参照。
Node.jsのexportsについて
Node.jsの概要(1) モジュール機能

だがES2015でついに、JavaScriptの標準仕様としてモジュール機能が策定された。

Babelによるトランスパイルが必須

このモジュール機能は、仕様として存在しているだけで、現時点では実装はほとんど進んでいない。
ブラウザでもそうだし、Node.jsでも動かない(v6.4.0で確認)。
そのため、実際に動かすためにはトランスパイルが必要になる。

以後、この記事では、バージョン6.14.0のBabelでトランスパイルしてコードを実行していく。

(参考)Babelの導入

まずbabel-cliをグローバルインストール。

npm install -g babel-cli

次に、作業用のルートディレクトリで、プリセットに関する作業を行う。

echo '{ "presets": ["es2015"] }' > .babelrc
npm install babel-preset-es2015

これで、使えるようになる。
使い方は以下。

babel-node 実行するファイル名

基本的な使い方

基本的な考え方はNode.jsにおけるモジュール機能と同じで、exportしたものをimportすればいい。
以後、この記事では、export.jsのコードをimport.jsで読み込んで実行するという形を取る。

// export.js
const myStr = '文字列';
function myFunc(){
    return '関数';
};
export {myStr, myFunc};
export const myNum = 10;
export function myFunc2(){
    return '関数2';
};

// import.js
import {myStr, myFunc, myNum, myFunc2} from './export.js';
console.log(myNum); // 10
console.log(myStr); // 文字列
console.log(myFunc());  // 関数
console.log(myFunc2()); // 関数2

この状態でbabel-node import.jsを実行すると、コードが動く。

変数や関数の宣言の前にexportを付けるだけで、その変数や関数をモジュールとして使えるようになる。
また、{}で囲むことで、既に存在する変数や関数をまとめてexportすることも出来る。

importするには、以下の構文を使う。
import { 読み込む変数や関数の名前 } from '読み込むファイルのパス';

as

asを使うことで、exportimportの際に名前を付けることも出来る。

exportでのas

// export.js
const myStr = '文字列';
function myFunc(){
    return '関数';
};
export {myStr as A, myFunc as B};

// import.js
import {A,B} from './export.js';
console.log(A); // 文字列
console.log(B());   // 関数

importでのas

// export.js
const myStr = '文字列';
function myFunc(){
    return '関数';
};
export {myStr, myFunc};

// import.js
import {myStr as X, myFunc as Y} from './export.js';
console.log(X); // 文字列
console.log(Y());   // 関数

全てのexportを一つのオブジェクトとしてまとめてimportする

import * as 任意の名前 from '読み込むファイルのパス';
この構文を使うと、任意の名前のオブジェクトが作られ、exportされているものが全て、そのオブジェクトのプロパティとして格納される。

// export.js
const myStr = '文字列';
function myFunc(){
    return '関数';
};
export {myStr, myFunc};

// import.js
import * as importObj from './export.js';
console.log(importObj.myStr);   // 文字列
console.log(importObj.myFunc());    // 関数
console.log(Object.getOwnPropertyNames(importObj));    // [ '__esModule', 'myStr', 'myFunc' ]

default

exportの後にdefaultをつけることも出来る。
そうすると、importの際に任意の名前を付けることができ、{}で囲む必要もない。

// export.js
let myNum = 10;
export default myNum;

// import.js
import data from './export.js'
console.log(data);  // 10

defaultは、一つのファイルに一つしか指定できない。

defaultとそれ以外を組み合わせることも可能。

// export.js
let num = 10;
let bool = true;
export default function returnHello(){
    return 'Hello';
};
export {num, bool};

// import.js
import myFunc, {num, bool} from './export.js'
console.log(myFunc());  // Hello
console.log(num);   // 10
console.log(bool);  // true

*でのdefault

先述の*を使った場合、defaultexportされている対象は、defaultという名前のプロパティとして格納される。

// export.js
let num = 10;
let bool = true;
export default function returnHello(){
    return 'Hello';
};
export {num, bool};

// import.js
import * as importObj from './export.js'
console.log(importObj.default()); // Hello
console.log(importObj.num); // 10
console.log(importObj.bool);    // true
console.log(Object.getOwnPropertyNames(importObj));    // [ '__esModule', 'default', 'num', 'bool' ]

npmパッケージの読み込み

npmでインストールしたパッケージは、requireで読み込む時と同様、パッケージ名のみを指定すればよい。

// npm install react を行ってからこのファイルを実行する

import React from 'react';
console.log(React.hasOwnProperty('createClass'));  // true
console.log(Object.getOwnPropertyNames(React));
// [ 'Children',
//   'Component',
//   'PureComponent',
//   'createElement',
//   'cloneElement',
//   'isValidElement',
//   'PropTypes',
//   'createClass',
//   'createFactory',
//   'createMixin',
//   'DOM',
//   'version',
//   '__spread' ]

module.exportsとの共通点

以下の記事に書いたような、読み込まれたファイルは1度だけ実行される、クロージャのような仕組みを作れる、などの特徴は、ES2015にも共通している。

Node.jsのexportsについて

module.exportsとの相違点

module.exportsで読み込んだモジュールは自由に変更できるが、ES2015のモジュールは、読み込み専用となる。
変更を行おうとすると、エラーになる。

// export.js
export let ESnum = 5;
export let ESobj = {
    prop: 'オブジェクトのプロパティ'
};
module.exports.num = 5;
module.exports.obj = {
    prop: 'オブジェクトのプロパティ'
};

// import.js
import {ESnum, ESobj} from './export.js'
let requireModule = require('./export.js');

console.log(ESnum); // 5
console.log(ESobj); // { prop: 'オブジェクトのプロパティ' }
console.log(requireModule.num); // 5
console.log(requireModule.obj); // { prop: 'オブジェクトのプロパティ' }

// requireで読み込んだものは、自由に操作できる
requireModule.num++;
console.log(requireModule.num); // 6
requireModule.obj.prop = '変更';
console.log(requireModule.obj); // { prop: '変更' }
requireModule.obj = 'オブジェクトを文字列に変更';
console.log(requireModule.obj); // オブジェクトを文字列に変更

// ES2015でも、オブジェクトのプロパティは操作できる
ESobj.prop = 'プロパティを変更';
console.log(ESobj); // { prop: 'プロパティを変更' }

// 以下はエラーになる
ESnum++;    // "ESnum" is read-only
ESobj = {};  // "ESobj" is read-only