你是不是也有過這種瞬間:明明只是想「先 log 一下」,結果畫面直接噴錯,然後你盯著那行程式碼,腦袋只剩一句「我到底惹到誰?」😑
而且最討厭的是,有時候它不噴錯,還給你 undefined。更陰。
Hoisting は宣言が先に処理されるというだけの話
JavaScript の hoisting は、実行前に「変数宣言と関数宣言」がスコープの先頭で登録され、初期化や代入はその場のまま残る挙動を指す。
- 「コードが上に移動する」ってより、実行前に登録されるだけ
varは先にundefinedで存在するlet/constは TDZ で触ると即死(ReferenceError)- 関数宣言は本体ごと使える、関数式は変数だけ先に出る
- 混乱の大半は「宣言」と「代入」を同じ気持ちで見てるせい
嫌な真実:「hoisting を理解したらバグが消える」みたいな夢はないです。バグは減るけど、あなたのコードが雑なら普通に別の形で噴きます🙂
ただ、hoisting を知らないと「噴いた理由」が見えない。ここが地獄。
var の hoisting は undefined を置いていく
var は宣言が先に評価されるため、宣言前に参照しても ReferenceError ではなく undefined になることがある。
典型:
console.log(myVar); // undefined
var myVar = 10;
これ、初心者に優しい顔してるけど、実態は「気づきにくいバグ製造機」です。優しさじゃない。毒。
エンジン目線:内部的にはこういう順で解釈されるイメージ。
var myVar;
console.log(myVar); // undefined
myVar = 10;
ね。var myVar; だけ先に登録されて、代入は後。だから「存在はする」けど「値はない」。
地味。地味にヤバい。
余談:「じゃあ var 使わなきゃいいじゃん」って話にすぐなるけど、古いコードやライブラリ、ビルド設定次第で普通に出会う。逃げ切れないです。
let と const は TDZ で殴ってくる
let と const も hoist されるが、宣言行まで Temporal Dead Zone に入り、宣言前参照は ReferenceError になる。
こうなる:
console.log(myLet); // ReferenceError: Cannot access 'myLet' before initialization
let myLet = 10;
「hoist されるのに触れない」って言われると混乱するけど、感覚としては予約は入ってるけど受け取り前みたいな感じ。受け取ってないのに使うな、っていう。
まあ、var の「静かに undefined」よりはマシ。痛いけど、原因が分かる痛さ。
ブロックスコープ:{ } の中(if/for/while/関数など)で let/const は区切られる。で、区切られた中でも TDZ は発生する。
{
console.log(a); // ReferenceError
let a = 5;
}
これ、見た目は「同じブロック内じゃん」って思うんだよね。でも JS は「宣言行に到達する前の参照」を許さない。容赦ない。
関数宣言は丸ごと hoist されて呼べる
関数宣言は関数名と実装が実行前に登録されるため、宣言より前で呼び出しても動作する。
greet();
function greet() {
console.log("Hello!");
}
これは普通に動く。だから「関数は上で定義しろ」って教えられても、動くから油断する。
油断したところに次が来る。
関数式とアロー関数は変数だけ先に出る
関数式とアロー関数は「変数の宣言」だけが hoist され、関数の代入はその場なので、宣言前呼び出しは TypeError または ReferenceError になる。
var + 関数式:まず変数が undefined で存在しちゃうパターン。
greet(); // TypeError: greet is not a function
var greet = function() {
console.log("Hello!");
};
ここ、エラーが TypeError なのがいやらしい。greet は「ある」から。あるけど関数じゃない。つまり undefined を呼ぼうとして死ぬ。
const + アロー:TDZ で即死パターン。
sayHi(); // ReferenceError
const sayHi = () => {
console.log("Hi!");
};
で、「じゃあどっちがいいの?」ってなるけど、個人的には読みやすさ優先で統一が一番効く。混ぜると、未来の自分が刺される。
時間とお金で割り切る hoisting 対策のコスパ
hoisting 対策は「let/const を使う」「宣言前参照をしない」「ESLint を入れる」の3つが費用対効果が高く、学習時間は数時間、事故削減は長期で効く。
ここ、ふわっと「ベストプラクティスです」って言われても動けない人が多い。分かる。じゃあ算盤っぽく置くね。
前提:あなたの時給(自分の時間の価値)を 2,000円〜5,000円 くらいで仮置き。副業とかでコード書いてる人だと普通にこれ超えるけど、まあ仮。
ケースA:hoisting を放置して、たまにハマる
- ハマる頻度:月1回(少なめに見積もって)
- 1回の調査時間:30〜90分(ログ増やしたり、関数呼び出し順見たり…地味に溶ける)
- 精神コスト:高い(これが一番払ってる)
ケースB:最初に対策を入れて、事故の形を減らす
- 学習:hoisting と TDZ をちゃんと理解する 1〜2時間
- 設定:ESLint 導入&ルール有効化 30分〜2時間(環境による、ここが沼)
- コード:
varを基本避ける、関数は「使う前に置く」癖をつける 数日で定着
雑に計算:月1回×1時間ハマるなら、年間12時間。時給2,000円でも24,000円分。5,000円なら60,000円分。
ESLint 入れるのに2時間溶かしても、普通に回収できる。しかも精神が削れにくくなる。ここデカい。
…まあ、ESLint の設定で別の地獄を見る人もいるけどね。あるある🙂
使うツール:ESLint(静的解析)で「use-before-define」「no-var」みたいなルールを効かせると、hoisting 系の事故が早めに潰れる。
公式ドキュメントを見ながら入れるのが一番早い。変にブログのコピペすると、設定が古いまま残る。
ありがちな落とし穴:「関数宣言は hoist されるから、上に置かなくていい」ってやると、チーム開発で読みにくくなる。
動くかどうかと、読めるかどうかは別。ここ混ぜると、あとで揉める。
もう一個だけ:let/const にしても「TDZ で死ぬ」なら、結局は参照順の設計が雑ってこと。そこを直さないと、別の場所で死ぬ。地味にね。
FAQ みたいなやつ どうせここで詰まってる
規則:ここは即答だけ。細かい感情は後回し。
Q. hoisting って結局コードが上に移動してるの?
A. JavaScript はコードを並べ替えず、実行前に宣言をスコープへ登録するため「上にあるように見える」だけです。
Q. var はなんで undefined で通るの?
A. var は宣言時に変数が作られ初期値が undefined になるため、宣言前参照でもエラーにならない場合があります。
Q. let/const も hoist されるのに ReferenceError なのはなぜ?
A. let/const は宣言行まで Temporal Dead Zone に入り、宣言前アクセスが禁止されているからです。
Q. 関数宣言と関数式、どっちを使うべき?
A. hoisting の事故を減らすなら「呼ぶ前に定義する」方針で統一し、関数式やアローは特に宣言前呼び出しを避けるのが安全です。
結論の芯:Hoisting は「宣言が先に登録される」仕様で、var は undefined、let/const は TDZ、関数宣言は本体ごと、関数式は変数だけが先に出ます。
で、最後に。
私だったら、明日からやる最初の小さい動作はこれ。
小さい一手:怪しいファイルを開いたら、先に「そのスコープの先頭で宣言されてるもの」を目で追う。変数名と関数名だけ拾う。1分でいい。
それだけで「なんでここで undefined?」の回数、たぶん減る。少なくとも私は減った。🙂
