JavaScript ES6のモジュール機能のスコープと仕様を調査してみた

プログラミング
スポンサーリンク

はじめに

ES6では新たにclass/import/export文が加わり、モジュール機能が利用できるようになりました。これにより、グローバル空間へ汚染させることは少なくなるとは思いますが、ほかの影響点を探るべく、モジュールの名前空間/スコープの調査を行ってみました。

1. クラスのstatic変数へ書き込み

目的

エクスポートを持つクラス内でstatic変数に書き込みをした場合、別ファイルで同一クラスを作成した場合に影響を及ぼすか確認します。

ファイル構成

以下を同一ディレクトリ内に置きます。

  • index.html
  • Test.js
  • main1.js
  • main2.js

ファイルの内容は以下の通りです。

index.html

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script type="module" src="./main1.js" charset="utf-8"></script>
<script type="module" src="./main2.js" charset="utf-8"></script>
</head>
<body>
</body>
</html>

Test.js

export default class Test {
	
	constructor(name) {
		Test.staticData++;
		this.name = name;
	}
	
	show() {
		console.log(this.name + ":" + Test.staticData)
	}
	
}

Test.staticData = 0;

main1.js

import Test from "./Test.js";

const test1 = new Test("TEST_1");
test1.show();

main2.js

import Test from "./Test.js";

const test2 = new Test("TEST_2");
test2.show();

結果

実行したときの結果が以下の通りです。

Test.js:9 TEST_1:1
Test.js:9 TEST_2:2

index.htmlを実行すると、内部のmain1.jsmain2.jsが順番に実行されます。main1.jsにて、Testクラスがもつstatic変数(関数オブジェクトのプロパティへ保存した値)を書き換えします。main2.jsで作成したTestクラスのコンストラクタで、「TEST_2:2」と表示されていることから、main1.jsで変更したstatic変数の情報が、main2.jsに影響を及ぼしています。

2. グローバルオブジェクトへ書き込み

目的

エクスポートを持つクラス内でグローバルオブジェクトに書き込みをした場合、別ファイルで影響を及ぼすか確認します。

ファイル構成

先ほど、同一なので省略。

index.html

先ほど、同一なので省略。

Test.js

export default class Test {
	static writeString() {
		String.testData = 100;
	}
}

Test.staticData = 0;

main1.js

const wait1sec = function() {
	console.log(String.testData);
};

const wait3sec = function() {
	console.log(String.testData);
};

setTimeout(wait1sec, 1000);
setTimeout(wait3sec, 3000);

main2.js

import Test from "./Test.js";

const wait2sec = function() {
	Test.writeString();
};

setTimeout(wait2sec, 2000);

結果

実行したときの結果が以下の通りです。

main1.js:2 undefined
main1.js:6 100

main2.jsで、組み込みグローバルオブジェクトStringのプロパティを書き換えましたが、main1.jsでも、そのプロパティが反映されていることが確認できます。

3. 同一クラスを複数importした際の実行について

目的

少し思考を変えて、同一ファイルはimportするたびに実行されるのかを確認したいと思います。

ファイル構成

先ほど、同一なので省略。

index.html

先ほど、同一なので省略。

Test.js

console.log("A");

const Test = 1;
export default Test;

main1.js

import Test from "./Test.js";

main2.js

import Test from "./Test.js";

結果

実行したときの結果が以下の通りです。

Test.js:1 A

1回しかconsoleが実行されてないことから、importするたびに実行するものではないようです。従って毎回実行されることを期待したコードは書いてはいけないことが分かります。

4. 同一クラス内でexport していない変数を書き換え

目的

exportしていないものは、外部へ見せていないですが、これはグローバル汚染になっているのか確認してみます。

ファイル構成

先ほど、同一なので省略。

index.html

先ほど、同一なので省略。

Test.js

let hoge = 0;
export default class Test {
	constructor(name) {
		this.name = name;
	}
	add() {
		hoge++;
		console.log(this.name + ":" + hoge);
	}
}

main1.js

import Test from "./Test.js";
const test1 = new Test("main1");
test1.add();

main2.js

import Test from "./Test.js";
const test2 = new Test("main2");
test2.add();

結果

実行したときの結果が以下の通りです。

Test.js:8 main1:1
Test.js:8 main2:2

hogeはexportしていないはずですが、書き換えをした場合に、他の処理にも影響を及ぼすようです。

5. 同一クラスを別ファイルパスにおいた場合の動作

目的

これまで3と4は、同一ファイルをimportしているから起きたものである可能性があります。これをはっきりさせるためにファイルパスを別にしてみます。

ファイル構成

以下を同一ディレクトリ内に置きます。

  • index.html
  • Test1/Test.js
  • Test2/Test.js
  • main1.js
  • main2.js

ファイルの内容は以下の通りです。

index.html

先ほど、同一なので省略。

./Test1/Test.js, ./Test2/Test.js

console.log("A");

let hoge = 0;
export default class Text {
	constructor(name) {
		this.name = name;
	}
	add() {
		hoge++;
		console.log(this.name + ":" + hoge);
	}
}

main1.js

import Test from "./Test1/Test.js";
const test1 = new Test("main1");
test1.add();

main2.js

import Test from "./Test2/Test.js";
const test2 = new Test("main2");
test2.add();

結果

実行したときの結果が以下の通りです。

Test.js:1 A
Test.js:10 main1:1
Test.js:1 A
Test.js:10 main2:1

今回は、読み込むクラスを ./Test1/Test.jsと ./Test2/Test.jsに分けてみました。ファイルの中身は全く同一ですが、別の空間で実行されるようです。

6. HTMLファイルから同一モジュールを読み込む場合の動作

目的

3と似ていますが、importではなくhtmlファイルから直接同一ファイルを参照したらどうなるのか、ついでに確認します。

ファイル構成

以下を同一ディレクトリ内に置きます。

  • index.html
  • main.js

ファイルの内容は以下の通りです。

index.html

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script type="module" src="./main.js" charset="utf-8"></script>
<script type="module" src="./main.js" charset="utf-8"></script>
</head>
<body>
</body>
</html>

main.js

console.log("main.js");

結果

実行したときの結果が以下の通りです。

main.js:2 main.js

やはり1度しか実行されないことが分かりました。

7. HTMLファイルから同一スクリプトを読み込む場合の動作

目的

6の結果を見て、JavaScriptってESモジュール以前の仕様からそうだったのかと気になりました。そこで、スクリプトとして読み込む従来の方法を確認してみます。

ファイル構成

以下を同一ディレクトリ内に置きます。

  • index.html
  • main.js

ファイルの内容は以下の通りです。

index.html

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script src="./main.js" charset="utf-8"></script>
<script src="./main.js" charset="utf-8"></script>
</head>
<body>
</body>
</html>

main.js

"use strict";
console.log("main.js");

結果

実行したときの結果が以下の通りです。

main.js:2 main.js
main.js:2 main.js

2回呼ばれています。6の結果は1回しか呼ばれないことから、ESモジュールを使用した場合、同一スクリプトは1度しか実行されない仕様となることが分かりました。

まとめ

  • 同一ファイルを色々な箇所で複数回importしても、ファイル内のスクリプトは最初の1度しか実行されない。
  • HTMLファイルから、同一のモジュールを読み込んでも、最初の1度しか実行されない。
  • グローバルオブジェクトへの書き換えは、どのファイルに対しても影響がある。
  • 名前空間はファイル内に収まる。
  • 同一クラスだとしても、ファイル(ファイルパス)が異なれば別の名前空間になる。
  • ファイル内に記載されたクラス外のローカル変数は、その同一ファイルを利用した箇所に対して影響を及ぼす。

おわりに

class/import/export文が追加されましたが、扱い方や条件が整えば、グローバル汚染が発生することが確認できました。クラス内にstatic変数を持たせ、何かの情報を切り替えるようなライブラリを作成してしまうと、偶然別のプログラムがそのクラスを使用した場合に、問題になる可能性があることが分かりました。

また、最終的にモジュールの動作自体が、1回しか実行されないという私が思っていない動作をしていることに気が付き、話が少し脱線していきましたが、今回色々試してみたことでモジュールの理解が深まり、とてもよかったと思います。

モジュールを使う場合は上記のような注意点があることを忘れないようにし、プログラムを作成していけたらいいなと思いました。

コメント

タイトルとURLをコピーしました