2026.09.23 泊まれる場所4,223 立ち寄りスポット59,693 営業中を確認したキャンプ場1,005 note を読む

ファイルを分けた途端、変数が初期化前だと怒られた。循環importの断ち切り方

片方だけ直せば、それで十分だった。

日本進化シミュレーション第4回 / 全17回目次

ファイル分割を始めて数日、コンソールに見慣れないエラーが出た。

ReferenceError: Cannot access 'scene' before initialization

意味は分かる。sceneという変数を、まだ用意できていないうちに使おうとしている。
でも、コードを目で追う限り、ちゃんと順番通りに書いてあるように見えた。
分からないまま、その日は終わった。

犯人は、お互いを参照しあう2つのファイル

翌日、落ち着いて僕が追ってみて分かった。
植生を扱うvegetation.tsが、シーンを管理するscene.tsからsceneという変数を読み込んで、そこへ木をscene.add(...)で足していた。
ところがscene.tsのほうも、vegetation.tsの値を使っていた。

AがBを読み込み、BもAを読み込んでいる。
ES modulesはこの循環自体は許してくれる。
ただし片方がまだ自分の本体を実行し終える前に、もう片方がそのconstをトップレベルで即座に参照すると、今回のエラーになる。

単一ファイルだった頃は、この問題はそもそも存在しなかった。
上から下へ順番に書かれているだけだったから。
ファイルを分けたことで、初めて「どっちが先に実行されるのか」という問題が生まれた。

両方直す必要はなかった

最初は「循環を断ち切るために、どちらのimportも消さないと」と身構えていた。
でも実際にやってみると、片方だけ直せば十分だった。

値を直接importするのをやめて、setter関数越しの間接参照に変える。

// 提供する側(scene.ts)let _leafMeshRound: any = null;export function setLeafMeshRoundRef(m: any) { _leafMeshRound = m; }// 使うときは _leafMeshRound.xxx// 供給する側(vegetation.ts)setLeafMeshRoundRef(leafMeshRound);

function宣言は、モジュール本体がまだ実行し終わっていなくても参照できる。
constのような一時的な参照不可の期間(TDZ)に入らないからだ。
だから、setter関数を介した呼び出しなら、循環の途中でも安全に呼べる。

片方だけ直せば足りる、という判断

この手のバグに出会うと、つい「循環そのものが悪」という気になって、依存関係を全部洗い出して整理し直したくなる。
実際にはそこまでしなくても直った。

片方を間接参照に変えるだけで、実行順の問題は解消する。
もう片方はそのままの直接importで構わない。
機械的に分割した約28ファイルの間には、似たような循環がまだいくつか残っているはずだ。
その全部を今すぐ洗い出すより、エラーが出たところから一つずつ直していく方針にした。

切り分けで一番効いたのは、新しいタブ

この調査の途中、直したはずなのに直らないように見える瞬間が何度かあった。
原因はコードではなく、開発サーバーのホットリロードが壊れた状態のまま残っていたタブを見ていたことだった。

新しいタブを開いてコンソールを見直すと、さっきまでのエラーが跡形もなく消えている。
以来、挙動が怪しいと感じたら、まず疑うのはコードより先にタブの鮮度になった。

ゲームを1本作るときの進め方を、noteにまとめています(書いているのは私です)。
noteで読む

まる子パパ

まる子パパ

会社員。受託開発のエンジニアを15年以上続けていて、その前は寿司職人・長距離運転手・農業。 技術ブログは、キャンプの合間に踏んだバグと、AI・Web開発の運用の失敗を書いています。 このサイト自体が、開発に携わっているCMS「コンテナ」の稼働中の実例です。 このサイトについて / 小さな自動化の相談

—

ほかにも書いています

受託・note・スタンプ

※受託の相談とnoteは、運営者が個人で受けている・書いているものです。

技術ブログ一覧へ戻る