⛓️

JavaScript の import された各変数 は参照変数(=エイリアス)であるという話

2024/11/27に公開

JavaScript には参照変数(=エイリアス)にあたるものはあるという話

今回は esmodule に於ける export した変数は参照変数(参照渡しでいうところの 実引数に対する仮引数の挙動)という話をします。

module を import するということ

まず、 変数 と その setter を公開します。

module.js
export let value;
export function setValue(v) {
    value = v;
}

ここでなぜ setter が必要だというと、 esmodule に於いて、 export した それぞれ は const ……つまり上書きによる元の値への反映ができないからです。

具体的にはこう

import { value } from "./module.js";
value = 1; // -> Assignment to constant variable.

let で宣言して export しようとも それは 受け取る側では const です。
いや、そもそも function であろうと同じなので それはそう です。

import { setValue } from "./module.js";
setValue = 2; // -> Assignment to constant variable.

ちなみにこれは dynamic import の直下のプロパティでも同様です。

const module = await imprt('./module.js');
module.value = 3; // -> Cannot assign to read only property 'value' of object '[object Module]'

本題

Import した 変数(関数も変数という扱いにつき)は 再代入禁止なのがわかったところで今回の本題、

ソースはこちら
https://gist.github.com/juner/ed21a255fac53ec03318e589b703fb41

動作するサンプルへのリンク
https://gistcdn.githack.com/juner/ed21a255fac53ec03318e589b703fb41/raw/

script.js
import { format, addMessage } from "./sub.js";
import { value as newValue, setValue } from "./module.js";
// #region set
{
    const name = "set";
    const type = "set";
    setValueButton.addEventListener('click', () => {
        const time = format.format(new Date());
        const value = Symbol(`${time}`);
        setValue(value);
        const dataset = { type, time, description: value.description };
        addMessage({ name, time, dataset });
    });
}
// #endregion
// #region change 1
(async () => {
    const name = "change 1";
    const type = "change1";
    const timeout = null;
    let oldValue;
    while (true) {
        await new Promise(resolve => setTimeout(resolve, timeout));
        if (oldValue === newValue) continue;
        console.log('%o: %O', name, newValue);
        oldValue = newValue;
        const time = format.format(new Date());
        const dataset = { type, time, description: newValue.description };
        addMessage({ name, time, dataset });
    }
})();
// #endregion
// #region change 2
(async () => {
    const name = "change 2";
    const type = "change2";
    const timeout = null;
    let oldValue;
    const module = await import('./module.js');
    while (true) {
        await new Promise(resolve => setTimeout(resolve, timeout));
        if (oldValue === module.value) continue;
        console.log('%o: %O', name, module.value);
        oldValue = module.value;
        const time = format.format(new Date());
        const dataset = { type, time, description: module.value.description };
        addMessage({ name, time, dataset });
    }
})();
// #endregion

注目すべきは以下

set

setValue() を呼び出して値の設定を行います。
画面でいうところの call set value ボタンの挙動のところです。

change 1

Import 経由で とってきた newValue もとい value の値を永久ループで確認します。
(参照変数の変更をチェックしています)

change 2

dynamic import 経由でとってきた module.value の値を 永久ループで確認します。
(こっちは 参照変数 ではなく プロパティの更新確認ではありますが、動作の違いが無いかの確認用です)

結果

参照変数の特性である エイリアスの機能としてちゃんとボタンを押すことでそれぞれのチェックが動作しているところがわかると思います。
(※時間出してるのはチェックタイミングの確認用で、設定された値の確認用ではありません。)

参考

インポートした値はエクスポートしたモジュールだけが変更できる

インポートした識別子は「動的バインド」と呼ばれます。エクスポートしているモジュールが再代入するとインポートしている値も変わるからです。しかしながら、当の変数をインポートしているモジュールは再代入できません。それでも、エクスポートしたオブジェクトを保持しているモジュールは、インポートしたオブジェクトを書き換えることができますし、変更した値は同じ値をインポートしているすべてのモジュールが観測できるようになっています。

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Statements/import#インポートした値はエクスポートしたモジュールだけが変更できる

Discussion