第3版が利用可能です。こちらをお読みください!

第10章
モジュール

初心者のプログラマーは、アリが丘を築くように、大きな構造を考えずに、一度に1つのピースずつプログラムを書きます。彼女のプログラムは、まるでサラサラの砂のようです。しばらくは立つかもしれませんが、大きくなりすぎるとバラバラになってしまいます。

この問題に気づくと、プログラマーは構造について多くの時間を費やして考えるようになります。彼女のプログラムは、岩の彫刻のように、硬直した構造になります。それらは頑丈ですが、変更が必要になると、暴力的な修正が必要になります。

熟練のプログラマーは、いつ構造を適用し、いつ物事を単純な形で残すべきかを知っています。彼女のプログラムは、粘土のように、固体でありながら柔軟です。

マスター・ユアン・マ, プログラミングの書

すべてのプログラムには形があります。小規模では、この形は関数への分割と、それらの関数内のブロックによって決定されます。プログラマーは、プログラムを構造化する方法について、多くの自由度を持っています。形は、プログラムの意図された機能よりも、プログラマーの好みに由来することが多いのです。

大規模なプログラム全体を見ると、個々の関数は背景に溶け込み始めます。このようなプログラムは、より大きな組織単位があれば、より読みやすくなります。

モジュールは、ある基準によって、まとまっているコードの集まりにプログラムを分割します。この章では、そのような分割によって得られるいくつかの利点を探り、JavaScriptでモジュールを構築するためのテクニックを紹介します。

モジュールが役立つ理由

著者が本を章や節に分割する理由はいくつかあります。これらの分割により、読者は本がどのように構成されているかを理解しやすくなり、興味のある特定の箇所を見つけやすくなります。また、各セクションに明確な焦点を与えることで、著者も助かります。

プログラムをいくつかのファイルまたはモジュールに編成する利点も似ています。構造は、まだコードに慣れていない人が探しているものを見つけやすくし、プログラマーが関連するものを近くにまとめておくことを容易にします。

一部のプログラムは、従来のテキストのモデルに従って、読者がプログラムをたどることが推奨される明確な順序と、コードの一貫した説明を提供する多くの文章(コメント)とともに構成されています。これにより、プログラムを読むのがはるかに怖くなくなります(未知のコードを読むのは通常、恐ろしいものです)が、設定するのに手間がかかるという欠点があります。また、文章はコードよりも密接に相互接続する傾向があるため、プログラムの変更が難しくなります。このスタイルは、リテラシープログラミングと呼ばれます。この本の「プロジェクト」の章は、リテラシープログラムと見なすことができます。

一般的に、物事を構造化するにはエネルギーが必要です。プロジェクトの初期段階で、どこに何を入れるか、プログラムがどのようなモジュールを必要としているのかまだ確信がない場合は、最小限で構造化されていない態度を支持します。コードが安定するまで、どこでも都合の良い場所にすべてを配置してください。そうすることで、プログラムのピースをあちこちに移動するのに時間を無駄にすることはなく、実際にはプログラムに合わない構造に誤って閉じ込められることもありません。

名前空間

最新のプログラミング言語のほとんどには、グローバル(誰もが見ることができる)とローカル(この関数だけが見ることができる)の間の中間的なスコープレベルがあります。JavaScriptにはありません。したがって、デフォルトでは、トップレベル関数のスコープ外で可視にする必要のあるものはすべて、どこからでも可視です。

名前空間の汚染、つまり、関係のない多くのコードが単一のグローバル変数名のセットを共有しなければならないという問題は、第4章で言及されており、そこでMathオブジェクトは、数学関連の機能をグループ化することでモジュールのように機能するオブジェクトの例として挙げられました。

JavaScriptはまだ実際のモジュール構造を提供していませんが、オブジェクトを使用して公開アクセス可能なサブ名前空間を作成し、関数を使用してモジュール内に分離されたプライベート名前空間を作成できます。この章の後半で、JavaScriptが提供するプリミティブな概念の上に、合理的に便利な名前空間を分離するモジュールを構築する方法について説明します。

再利用

モジュールのセットとして構造化されていない「フラットな」プロジェクトでは、特定の関数を使用するためにコードのどの部分が必要なのかが明らかではありません。敵をスパイするためのプログラム(第9章を参照)では、設定ファイルを読み取るための関数を作成しました。その関数を別のプロジェクトで使用したい場合は、古いプログラムの中から、必要な機能に関連していると思われる部分をコピーして、新しいプログラムに貼り付ける必要があります。その後、そのコードに間違いを見つけた場合、その時に作業しているプログラムでのみ修正し、他のプログラムでも修正することを忘れてしまいます。

このような共有された重複したコードがたくさんあると、それらを移動したり、最新の状態に保つために多くの時間とエネルギーを費やすことになります。

単独で機能する機能の一部を別々のファイルとモジュールに配置すると、モジュールを使用したいさまざまなコードが同じ実際のファイルからロードするため、追跡、更新、共有が容易になります。

モジュール間の関係(各モジュールがどの他のモジュールに依存しているか)が明示的に記述されている場合、このアイデアはさらに強力になります。その後、外部モジュール(ライブラリ)のインストールとアップグレードのプロセスを自動化できます。

このアイデアをさらに進めて、何十万ものそのようなライブラリを追跡および配布するオンラインサービスを想像してみてください。必要な機能を探して、見つけたら、プロジェクトを設定して自動的にダウンロードできるようにします。

このサービスは存在します。それはNPM(npmjs.org)と呼ばれています。NPMは、モジュールのオンラインデータベースと、プログラムが依存するモジュールをダウンロードおよびアップグレードするためのツールで構成されています。これは、第20章で説明するブラウザレスJavaScript環境であるNode.jsから発展しましたが、ブラウザ用のプログラミングにも役立ちます。

疎結合

モジュールのもう1つの重要な役割は、第6章のオブジェクトインターフェイスと同じように、コードの断片を互いに分離することです。適切に設計されたモジュールは、外部コードが使用するためのインターフェースを提供します。モジュールがバグ修正と新しい機能で更新されると、既存のインターフェースは同じまま(安定しています)なので、他のモジュールは、それ自体を変更することなく、新しい改良版を使用できます。

安定したインターフェースは、新しい関数、メソッド、または変数が追加されないことを意味するのではないことに注意してください。既存の機能が削除されず、その意味が変更されないことを意味するだけです。

優れたモジュールインターフェースは、古いインターフェースを壊すことなくモジュールを成長させることができるようにする必要があります。これは、モジュールの内部概念をできるだけ少なく公開すると同時に、インターフェースが公開する「言語」が、幅広い状況で適用できるほど強力で柔軟であることを意味します。

設定ファイルリーダーなど、単一の焦点を絞った概念を公開するインターフェースの場合、この設計は自然に得られます。外部コードがアクセスする必要があるさまざまな側面(コンテンツ、スタイル、ユーザーアクションなど)が多数あるテキストエディターのような他のインターフェースの場合、慎重な設計が必要です。

名前空間として関数を使用する

関数は、JavaScriptで新しいスコープを作成する唯一のものです。したがって、モジュールに独自のスコープを持たせたい場合は、関数に基づいてモジュールを構築する必要があります。

DateオブジェクトのgetDayメソッドによって返されるように、名前を曜日番号に関連付けるためのこの単純なモジュールを考えてみましょう。

var names = ["Sunday", "Monday", "Tuesday", "Wednesday",
             "Thursday", "Friday", "Saturday"];
function dayName(number) {
  return names[number];
}

console.log(dayName(1));
// → Monday

dayName関数はモジュールのインターフェースの一部ですが、names変数はそうではありません。グローバルスコープに漏らさないようにしたいと考えています。

これを行うことができます

var dayName = function() {
  var names = ["Sunday", "Monday", "Tuesday", "Wednesday",
               "Thursday", "Friday", "Saturday"];
  return function(number) {
    return names[number];
  };
}();

console.log(dayName(3));
// → Wednesday

これで、namesは、(無名の)関数内のローカル変数になりました。この関数は作成され、即座に呼び出され、その戻り値(実際のdayName関数)が変数に格納されます。この関数には、100個のローカル変数を含む、何ページものコードを含めることができます。それらはすべてモジュール内部であり、モジュール自体には表示されますが、外部コードには表示されません。

同様のパターンを使用して、コードを外部の世界から完全に隔離することができます。次のモジュールは値をコンソールに出力しますが、他のモジュールが使用する実際の値は提供しません

(function() {
  function square(x) { return x * x; }
  var hundred = 100;

  console.log(square(hundred));
})();
// → 10000

このコードは100の二乗を出力するだけですが、実際には、一部のプロトタイプにメソッドを追加したり、Webページにウィジェットを設定したりするモジュールである可能性があります。内部で使用する変数がグローバルスコープを汚染するのを防ぐために、関数でラップされています。

名前空間関数を括弧のペアでラップしたのはなぜですか?これは、JavaScriptの構文の癖に関係しています。がキーワードfunctionで始まる場合、それは関数式です。ただし、ステートメントfunctionで始まる場合、それは関数宣言であり、名前が必要であり、式ではないため、その後に括弧を記述して呼び出すことはできません。追加の括弧を、関数を強制的に式として解釈させるためのトリックと考えることができます。

インターフェースとしてのオブジェクト

ここで、曜日モジュールに、曜日名から数値に変換する別の関数を追加したいと想像してみてください。関数を単に返すことはできなくなったため、2つの関数をオブジェクトでラップする必要があります。

var weekDay = function() {
  var names = ["Sunday", "Monday", "Tuesday", "Wednesday",
               "Thursday", "Friday", "Saturday"];
  return {
    name: function(number) { return names[number]; },
    number: function(name) { return names.indexOf(name); }
  };
}();

console.log(weekDay.name(weekDay.number("Sunday")));
// → Sunday

大きなモジュールでは、関数の最後にすべてのエクスポートされた値をオブジェクトにまとめるのは、エクスポートされる関数の多くが大きくなる可能性が高く、関連する内部コードの近くの別の場所に記述したいので、面倒になります。便利な代替手段は、(慣例的にexportsという名前の)オブジェクトを宣言し、エクスポートする必要があるものを定義するときはいつでも、そのオブジェクトにプロパティを追加することです。次の例では、モジュール関数はインターフェースオブジェクトを引数として受け取り、関数の外部のコードがそのオブジェクトを作成して変数に格納できるようにしています。(関数の外部では、thisはグローバルスコープオブジェクトを参照します。)

(function(exports) {
  var names = ["Sunday", "Monday", "Tuesday", "Wednesday",
               "Thursday", "Friday", "Saturday"];

  exports.name = function(number) {
    return names[number];
  };
  exports.number = function(name) {
    return names.indexOf(name);
  };
})(this.weekDay = {});

console.log(weekDay.name(weekDay.number("Saturday")));
// → Saturday

グローバルスコープからの分離

前のパターンは、ブラウザ向けのJavaScriptモジュールで一般的に使用されます。モジュールは単一のグローバル変数を宣言し、独自のプライベート名前空間を持つために、そのコードを関数でラップします。しかし、このパターンは、複数のモジュールが同じ名前を宣言する場合や、モジュールの2つのバージョンを並行してロードしたい場合に、依然として問題を引き起こします。

少しの手間で、1つのモジュールがグローバルスコープを介さずに別のモジュールのインターフェースオブジェクトを直接要求できるシステムを作成できます。私たちの目標は、モジュール名を指定すると、そのモジュールのファイル(実行中のプラットフォームに応じてディスクまたはWebから)をロードし、適切なインターフェース値を返すrequire関数です。

このアプローチは、以前に述べた問題を解決し、プログラムの依存関係を明示的にするという追加の利点があり、必要だと明示せずに誤ってモジュールを使用することが難しくなります。

requireには2つのものが必要です。まず、指定されたファイルの内容を文字列として返すreadFile関数が必要です。(このような単一の関数は標準のJavaScriptには存在しませんが、ブラウザやNode.jsなどのさまざまなJavaScript環境では、独自のファイルアクセス方法が提供されています。今のところ、この関数があるふりをしましょう。)次に、この文字列をJavaScriptコードとして実際に実行できるようにする必要があります。

データをコードとして評価する

データ(コードの文字列)を受け取り、現在のプログラムの一部として実行する方法はいくつかあります。

最も明らかな方法は、現在のスコープでコードの文字列を実行する特別な演算子evalです。これは通常、スコープが通常持つ健全なプロパティ(外部の世界から隔離されているなど)をいくつか壊すため、悪い考えです。

function evalAndReturnX(code) {
  eval(code);
  return x;
}

console.log(evalAndReturnX("var x = 2"));
// → 2

データをコードとして解釈するより良い方法は、Functionコンストラクターを使用することです。これは、コンマ区切りの引数名のリストを含む文字列と、関数の本体を含む文字列の2つの引数を取ります。

var plusOne = new Function("n", "return n + 1;");
console.log(plusOne(4));
// → 5

これはまさに、モジュールに必要なものです。モジュールのコードを関数でラップすることができ、その関数のスコープがモジュールスコープになります。

Require

以下は、requireの最小限の実装です。

function require(name) {
  var code = new Function("exports", readFile(name));
  var exports = {};
  code(exports);
  return exports;
}

console.log(require("weekDay").name(1));
// → Monday

new Functionコンストラクターはモジュールコードを関数でラップするため、モジュールファイル自体で名前空間をラップする関数を記述する必要はありません。また、exportsをモジュール関数の引数にするため、モジュールはそれを宣言する必要もありません。これにより、モジュールの例から多くの混乱がなくなります。

var names = ["Sunday", "Monday", "Tuesday", "Wednesday",
             "Thursday", "Friday", "Saturday"];

exports.name = function(number) {
  return names[number];
};
exports.number = function(name) {
  return names.indexOf(name);
};

このパターンを使用する場合、モジュールは通常、依存するモジュールをロードするいくつかの変数宣言から始まります。

var weekDay = require("weekDay");
var today = require("today");

console.log(weekDay.name(today.dayNumber()));

前に示したrequireの単純な実装には、いくつかの問題があります。1つは、requireされるたびにモジュールをロードして実行するため、複数のモジュールが同じ依存関係を持つ場合や、複数の回呼び出される関数内にrequire呼び出しがある場合、時間とエネルギーが浪費されます。

これは、すでにロードされているモジュールをオブジェクトに保存し、複数回ロードされた場合は既存の値を返すだけで解決できます。

2番目の問題は、モジュールが関数などのexportsオブジェクト以外の値を直接エクスポートすることができないことです。たとえば、モジュールは、定義するオブジェクト型のコンストラクターのみをエクスポートしたい場合があります。現在、requireは常に、作成したexportsオブジェクトをエクスポートされた値として使用するため、それを行うことはできません。

この従来の解決策は、モジュールに、exportsプロパティを持つオブジェクトである別の変数moduleを提供することです。このプロパティは、最初はrequireによって作成された空のオブジェクトを指していますが、別のものをエクスポートするために別の値で上書きすることができます。

function require(name) {
  if (name in require.cache)
    return require.cache[name];

  var code = new Function("exports, module", readFile(name));
  var exports = {}, module = {exports: exports};
  code(exports, module);

  require.cache[name] = module.exports;
  return module.exports;
}
require.cache = Object.create(null);

これで、グローバルスコープを介さずに、モジュールが相互に検索して使用できるようにする単一のグローバル変数(require)を使用するモジュールシステムができました。

このスタイルのモジュールシステムは、最初にそれを指定した疑似標準にちなんで、CommonJSモジュールと呼ばれます。これはNode.jsシステムに組み込まれています。実際の実装は、私が示した例よりもはるかに多くのことを行います。最も重要なことは、モジュール名から実際のコードへの移行をはるかにインテリジェントに行う方法を備えており、現在のファイルからの相対パス名と、ローカルにインストールされたモジュールを直接指すモジュール名の両方を可能にすることです。

ロードが遅いモジュール

ブラウザ用にJavaScriptを作成するときにCommonJSモジュールスタイルを使用することは可能ですが、多少手間がかかります。その理由は、Webからファイル(モジュール)を読み込むのが、ハードディスクから読み込むよりもはるかに遅いためです。スクリプトがブラウザで実行されている間は、第14章で明らかになる理由から、それが実行されるWebサイトでは他に何も起こりません。つまり、すべてのrequire呼び出しが遠くのWebサーバーから何かをフェッチした場合、ページはスクリプトをロードしている間、痛ましいほど長い時間フリーズします。

この問題を回避する1つの方法は、Webページで提供する前に、Browserifyのようなプログラムをコード上で実行することです。これにより、requireへの呼び出しが検索され、すべての依存関係が解決され、必要なコードが1つの大きなファイルに収集されます。Webサイト自体は、このファイルをロードするだけで、必要なすべてのモジュールを取得できます。

別の解決策は、モジュールを構成するコードを関数でラップして、モジュールローダーが最初にバックグラウンドで依存関係をロードし、依存関係がロードされたときにその関数を呼び出してモジュールを初期化できるようにすることです。これが、Asynchronous Module Definition(AMD)モジュールシステムが行うことです。

依存関係のある自明なプログラムは、AMDでは次のようになります。

define(["weekDay", "today"], function(weekDay, today) {
  console.log(weekDay.name(today.dayNumber()));
});

このアプローチの中心となるのはdefine関数です。これは、最初にモジュール名の配列を受け取り、次に各依存関係に対して1つの引数を受け取る関数を受け取ります。依存関係(まだロードされていない場合)をバックグラウンドでロードし、ファイルがフェッチされている間もページが動作し続けるようにします。すべての依存関係がロードされると、defineは指定された関数を、それらの依存関係のインターフェースを引数として呼び出します。

この方法でロードされるモジュール自体には、defineへの呼び出しが含まれている必要があります。インターフェースとして使用される値は、defineに渡された関数によって返されたものです。これが再びweekDayモジュールです。

define([], function() {
  var names = ["Sunday", "Monday", "Tuesday", "Wednesday",
               "Thursday", "Friday", "Saturday"];
  return {
    name: function(number) { return names[number]; },
    number: function(name) { return names.indexOf(name); }
  };
});

defineの最小限の実装を示すために、ファイル名と関数を受け取り、ロードが完了するとすぐにファイルのコンテンツを引数として関数を呼び出すbackgroundReadFile関数があると仮定します。(第17章では、その関数を記述する方法を説明します。)

ロード中のモジュールを追跡するために、defineの実装では、モジュールの状態を記述するオブジェクトを使用します。これらのオブジェクトは、モジュールが利用可能かどうかを示し、利用可能になったときにインターフェースを提供します。

getModule関数は、名前が指定されると、そのようなオブジェクトを返し、モジュールがロードされるようにスケジュールします。キャッシュオブジェクトを使用して、同じモジュールを2回ロードすることを回避します。

var defineCache = Object.create(null);
var currentMod = null;

function getModule(name) {
  if (name in defineCache)
    return defineCache[name];

  var module = {exports: null,
                loaded: false,
                onLoad: []};
  defineCache[name] = module;
  backgroundReadFile(name, function(code) {
    currentMod = module;
    new Function("", code)();
  });
  return module;
}

ロードされたファイルにも、(単一の)defineへの呼び出しが含まれていると想定します。currentMod変数は、ロードが完了したときにこのオブジェクトを更新できるように、現在ロードされているモジュールオブジェクトに関する情報をこの呼び出しに伝えるために使用されます。このメカニズムについては、後で説明します。

define関数自体は、getModuleを使用して、現在のモジュールの依存関係のモジュールオブジェクトをフェッチまたは作成します。そのタスクは、これらの依存関係がロードされたときにいつでも実行されるようにmoduleFunction(モジュールの実際のコードを含む関数)をスケジュールすることです。この目的のために、まだロードされていないすべての依存関係のonLoad配列に追加される関数whenDepsLoadedを定義します。この関数は、まだロードされていない依存関係がある場合はすぐに戻るため、最後の依存関係のロードが完了したときにのみ実際の作業を行います。また、ロードする必要のある依存関係がない場合に備えて、define自体からすぐに呼び出されます。

function define(depNames, moduleFunction) {
  var myMod = currentMod;
  var deps = depNames.map(getModule);

  deps.forEach(function(mod) {
    if (!mod.loaded)
      mod.onLoad.push(whenDepsLoaded);
  });

  function whenDepsLoaded() {
    if (!deps.every(function(m) { return m.loaded; }))
      return;

    var args = deps.map(function(m) { return m.exports; });
    var exports = moduleFunction.apply(null, args);
    if (myMod) {
      myMod.exports = exports;
      myMod.loaded = true;
      myMod.onLoad.forEach(function(f) { f(); });
    }
  }
  whenDepsLoaded();
}

すべての依存関係が利用可能になると、whenDepsLoadedはモジュールを保持する関数を呼び出し、依存関係のインターフェースを引数として渡します。

defineが最初に行うことは、呼び出されたときにcurrentModが持っていた値をmyMod変数に保存することです。モジュールのコードを評価する直前に、getModuleが対応するモジュールオブジェクトをcurrentModに格納したことを思い出してください。これにより、whenDepsLoadedはモジュール関数の戻り値をそのモジュールのexportsプロパティに格納し、モジュールのloadedプロパティをtrueに設定し、モジュールのロードを待機しているすべての関数を呼び出すことができます。

このコードは、require関数よりもずっと追跡が困難です。その実行は、単純で予測可能なパスをたどりません。代わりに、複数の操作が将来の不特定な時間に発生するように設定されており、コードの実行方法が不明瞭になります。

実際のAMD実装は、モジュール名を実際のURLに解決する点において、再び、以前に示したものよりもはるかに賢く、一般的に堅牢です。RequireJSrequirejs.org)プロジェクトは、このスタイルのモジュールローダーの人気のある実装を提供しています。

インターフェース設計

モジュールとオブジェクト型のインターフェースを設計することは、プログラミングの微妙な側面の1つです。自明でない機能は、さまざまな方法でモデル化できます。うまく機能する方法を見つけるには、洞察力と先見の明が必要です。

優れたインターフェース設計の価値を学ぶ最良の方法は、多数のインターフェースを使用することです。良いものもあれば、悪いものもあります。経験が、何が機能し、何が機能しないかを教えてくれます。つらいインターフェースが「そういうものだ」と決して決めつけないでください。修正するか、より使いやすい新しいインターフェースでラップしてください。

予測可能性

プログラマーがあなたのインターフェースの動作を予測できる場合、彼ら(またはあなた)は、その使い方を調べる必要性に頻繁に気を散らされることはありません。したがって、慣例に従うようにしてください。あなたが実装しているものと似たようなことをしている別のモジュールや標準のJavaScript環境の一部がある場合、あなたのインターフェースを既存のインターフェースに似せるのが良いかもしれません。そうすれば、既存のインターフェースを知っている人には馴染み深く感じられるでしょう。

予測可能性が重要なもう1つの領域は、コードの実際の動作です。より使いやすいという理由で、不必要に巧妙なインターフェースを作成したくなることがあります。たとえば、あらゆる種類の異なる型と引数の組み合わせを受け入れ、それらすべてに対して「正しいこと」を行うことができます。または、モジュールの機能のわずかに異なるフレーバーを提供する、数十の特殊な便利な関数を提供できます。これらは、インターフェースを基に構築するコードを少し短くするかもしれませんが、モジュールの動作に関する明確なメンタルモデルを構築することをはるかに難しくします。

構成可能性

インターフェースでは、可能な限り単純なデータ構造を使用し、関数には単一の明確なことを実行させようとします。可能な限り、純粋関数にしてください(第3章を参照)。

たとえば、モジュールが独自の配列のようなコレクションオブジェクトを提供し、要素を数えて抽出するための独自のインターフェースを持つことは珍しくありません。このようなオブジェクトには、mapforEachメソッドがなく、実際の配列を期待する既存の関数はどれもそれらを操作できません。これは、貧弱な構成可能性の例です。モジュールを他のコードと簡単に構成できません。

1つの例は、テキストエディタを作成するときに必要な、テキストのスペルチェックを行うモジュールです。スペルチェッカーは、エディタが使用する複雑なデータ構造を直接操作し、エディタの内部関数を直接呼び出して、ユーザーにスペルの候補を選択させることができます。もしそのように進むと、モジュールは他のプログラムでは使用できません。一方、スペルチェックインターフェースを、単純な文字列を渡すと、スペルミスと思われる位置と、修正候補の配列が返されるように定義すると、JavaScriptでは常に文字列と配列が使用できるため、他のシステムとも構成できるインターフェースができます。

階層化されたインターフェース

電子メールの送信など、複雑な機能のインターフェースを設計する場合、多くの場合、ジレンマに遭遇します。一方では、インターフェースのユーザーに詳細を過負荷にしたくありません。電子メールを送信できるようになるまでに、20分もインターフェースを勉強する必要はないはずです。一方、すべての詳細を隠したくもありません。モジュールで複雑なことをする必要がある場合は、それもできるようにする必要があります。

多くの場合、解決策は2つのインターフェースを提供することです。複雑な状況に対応する詳細な低レベルインターフェースと、日常的な使用のための単純な高レベルインターフェースです。2番目は、通常、1番目のインターフェースが提供するツールを使用して簡単に構築できます。電子メールモジュールでは、高レベルインターフェースは、メッセージ、送信者アドレス、受信者アドレスを受け取り、電子メールを送信するだけの関数にすることができます。低レベルインターフェースは、電子メールヘッダー、添付ファイル、HTMLメールなどを完全に制御できるようにします。

まとめ

モジュールは、コードを異なるファイルと名前空間に分割することにより、大規模なプログラムに構造を提供します。これらのモジュールに明確に定義されたインターフェースを与えることで、使いやすさと再利用性が向上し、モジュール自体が進化しても使い続けることが可能になります。

JavaScript言語はモジュールに関しては特徴的に役に立ちませんが、提供する柔軟な関数とオブジェクトにより、非常に優れたモジュールシステムを定義できます。関数スコープはモジュールの内部名前空間として使用でき、オブジェクトはエクスポートされた値のセットを保存するために使用できます。

このようなモジュールには、2つの人気のある、明確に定義されたアプローチがあります。1つはCommonJSモジュールと呼ばれ、名前でモジュールを取得してインターフェースを返すrequire関数を中心に展開します。もう1つはAMDと呼ばれ、モジュール名の配列と関数を受け取り、モジュールをロードした後、それらのインターフェースを引数として関数を実行するdefine関数を使用します。

練習問題

月の名前

weekDayモジュールと同様の単純なモジュールを作成し、月番号(Date型のようにゼロベース)を名前に変換したり、名前を番号に変換したりできるようにします。月名の内部配列が必要になるため、独自の名前空間を与え、モジュールローダーシステムを使用せずに、プレーンなJavaScriptを使用します。

// Your code here.

console.log(month.name(2));
// → March
console.log(month.number("November"));
// → 10

これは、weekDayモジュールとほぼ完全に同じです。即時呼び出される関数式は、エクスポートする必要がある2つの関数とともに、名前の配列を保持する変数をラップします。関数はオブジェクトに配置され、返されます。返されたインターフェースオブジェクトは、month変数に格納されます。

電子生命への回帰

第7章がまだいくらか記憶に残っていることを期待して、その章で設計されたシステムを思い出し、コードをモジュールに分割する方法を考えてください。復習のために、この章で定義された関数と型を、登場順に以下に示します。

Vector
Grid
directions
directionNames
randomElement
BouncingCritter
elementFromChar
World
charFromElement
Wall
View
WallFollower
dirPlus
LifelikeWorld
Plant
PlantEater
SmartPlantEater
Tiger

誇張してモジュールを作成しすぎないでください。ページごとに新しい章が始まる本は、タイトルに無駄なスペースがすべて使用されているという理由だけでも、おそらくあなたをイライラさせるでしょう。同様に、小さなプロジェクトを読むために10個のファイルを開く必要があるのは役に立ちません。3〜5個のモジュールを目指してください。

一部の関数をモジュール内部にして、他のモジュールからアクセスできないようにすることもできます。

ここに単一の正しい解決策はありません。モジュールの編成は、主に好みの問題です。

私が思いついたのは次のとおりです。内部関数を括弧で囲みました。

Module "grid"
  Vector
  Grid
  directions
  directionNames

Module "world"
  (randomElement)
  (elementFromChar)
  (charFromElement)
  View
  World
  LifelikeWorld
  directions [reexported]

Module "simple_ecosystem"
  (randomElement) [duplicated]
  (dirPlus)
  Wall
  BouncingCritter
  WallFollower

Module "ecosystem"
  Wall [duplicated]
  Plant
  PlantEater
  SmartPlantEater
  Tiger

(エコシステムのような)それを基に構築されたモジュールがgridモジュールの存在を知ったり心配したりする必要がないように、gridモジュールからdirections配列をworldから再エクスポートしました。

また、異なるコンテキストで内部の詳細として使用され、これらのモジュールのインターフェースに属さないため、2つの汎用的な小さなヘルパー値(randomElementWall)を複製しました。

循環依存

依存関係管理における厄介な問題は、モジュールAがBに依存し、BもAに依存する循環依存です。多くのモジュールシステムは、これを単純に禁止しています。CommonJSモジュールでは、制限付きの形式が許可されています。モジュールがデフォルトのexportsオブジェクトを別の値に置き換えず、ロードが完了した後にのみお互いのインターフェースへのアクセスを開始する限り、機能します。

この機能のサポートを実装できる方法を思いつきますか? requireの定義を振り返り、この許可のために関数が何をする必要があるかを考えてください。

秘訣は、モジュール用に作成されたexportsオブジェクトを、実際にモジュールを実行するrequireのキャッシュに追加することです。これは、モジュールがmodule.exportsをオーバーライドする機会がまだないため、他の値をエクスポートしたいかどうかはわからないことを意味します。ロード後、キャッシュオブジェクトはmodule.exportsでオーバーライドされます。これは、異なる値になる可能性があります。

しかし、モジュールのロード中に、最初のモジュールを要求する2番目のモジュールがロードされた場合、その時点でまだ空の可能性が高いデフォルトのexportsオブジェクトがキャッシュにあり、2番目のモジュールはそれへの参照を受け取ります。最初のモジュールのロードが完了するまで、オブジェクトに対して何も行おうとしなければ、問題なく動作します。