変数構文関数・メソッド

【JavaScript】 変数・関数の巻き上げってなんだ?

更新日:2021/08/23

JavaScriptの変数や関数は実行時に巻き上げられます。

これってどんな意味なのか、お伝えします!

 

巻き上げとは

変数はvar/let/constのどれかを使って定義します。

実際には定義前に定義されています。

意味がわからない・・・

次のコードを見てください。


console.log( x );  // undefined
console.log( y );  // ReferenceError: y is not defined
var x = 100;

後ろで定義されている x は、undefinedと表示されました。
しかし全く定義されていない y は、ReferenceErrorと表示されました。

xについては、コードの評価時に次のような処理がおこなわれています。


var x; // ←定義が巻き上げられた!
console.log( x );  // undefined
x = 100;

このようにコード実行時に、定義だけが先頭に移動されることを巻き上げといいます。

 

巻き上げの罠

今ではほとんどvarは使用されることがありません。
ですが説明の都合上、ここではvarを使用しています。

JavaScriptの初心者がよくやってしまう、巻き上げが原因の失敗を紹介します。

JavaScriptは関数内部から、外側の変数を参照できます。


var x = 100;

function a(){
    console.log( x );  // 100
}

100と表示されました。

では100と表示された後に、新しい変数を定義して200と表示させましょう。


var x = 100;

function a(){
    console.log( x ); // 100と表示させたい ← undefinedになる
    
    var x = 200;
    console.log( x ); // 200と表示させたい
}

a();

undefinedになってしまいました!

関数内部で巻き上げがおこって、外部の変数を参照できなくなっているのです。


var x = 100;

function a(){
    var x; // ローカル変数として定義された
    console.log( x ); // 値をセットしていないのでundefined
    
    x = 200;
    console.log( x ); // 200
}

undefinedは、定義されていないという意味。
でも実際は、undefinedという値が変数にセットされています。

【JavaScript】 undefinedの判定方法とその正体

 

変数の巻き上げ

varの巻き上げと、let/varの巻き上げは少し違う点があります。

巻き上げの初期値

var/let/constで定義した変数を、定義前に表示してみます。


console.log( x );   // undefined
console.log( y );   // ReferenceError: can't access lexical declaration `y' before initialization
console.log( z );   // ReferenceError: can't access lexical declaration `z' before initialization
var x = 100;
let y = 200;
const z = 300;

実際には、ReferenceErrorが出た時点で処理が止まります。
そのため3行目は実行されません。

varで定義した変数は、undefinedと表示されました。
その他はReferenceErrorで処理が止まりました。

let/constもvar同様に巻き上げがおこっています。
しかしvarが暗黙的にundefinedがセットされるのに対し、let/constは"初期化されていない"というフラグがセットされます。

このフラグがセットされている変数を参照すると、エラーとなって処理が停止するのです。

ブロックでの巻き上げ

varは関数スコープで評価されます。
そのため、関数内の同名変数は全て同じものを指します。


function a(){
    var x = 100;
    if( x > 0 ){
        var x = 300;
    }
    for( var i = 0 ; i < 3 ; i ++){
        var x = 500 * i;
    }
}

上の関数は評価時に、次のように内部の変数が巻き上げられます。


function a(){
    var x; // xが巻き上げられた
    x = 100;
    if( x > 0 ){
        x = 300;
    }
    for( var i = 0 ; i < 3 ; i ++){
        x = 500 * i;
    }
}

一方、let/constはブロックスコープです。
{ } で囲まれた範囲で巻き上げがおこります。


function a(){
    let x = 100;
    if( x > 0 ){
        let x = 300;
    }
    for( let i = 0 ; i < 3 ; i ++){
        let x = 500 * i;
    }
}

巻き上げで、次のように処理されます。


function a(){
    let x;
    x = 100;
    if( x > 0 ){
        let x;
        x = 300;
    }
    for( let i = 0 ; i < 3 ; i ++){
        let x;
        x = 500 * i;
    }
}

ブロックごとに定義されていますね。

 

関数の巻き上げ

関数の定義も巻き上げがおこります。

関数式と関数定義については、次のページを読んでみてください。
【JavaScript】 関数の定義と変数代入(関数式)の違い

関数宣言の巻き上げ

次のコードは関数宣言の前に、その関数を実行しています。


// いろいろな処理
f();
// いろいろな処理
function f(){
    // 処理
}

コード評価時に、巻き上げで次のようになります。


function f(){
    // 処理
}
// いろいろな処理
f();
// いろいろな処理

関数がコードの先頭に移動するので、問題なく動きます。

関数式の巻き上げ

次のコードは関数式で関数を定義して、その関数を実行しています。


f();
const f =  function(){
    // 処理
}

これは動きません。

これはあくまで変数の巻き上げだからです。

コード実行時に、次のように処理されています。


const f;
f();
f =  function(){
    // 処理
}

 

巻き上げがおこらないとどうなる?

巻き上げがおこらないと、コードの途中で変数の参照先が変わります。

例えば次のようなコードで巻き上げがおこらないとしたら、どうでしょうか?


let x = 100; // (a)

function a(){

    console.log( x ); // (1) : (a) を参照

    let x = 200; // (b)

    console.log( x ); // (2): : (b) を参照
}

コードの途中で関数外部の変数から、関数内部の変数に参照先が変わっていますね。

この程度のコードなら問題ありませんがもっと長いものになると、いつ参照先が変わったのかを把握するのが難しくなります。
その結果バグを生み出す原因にもなります。

同一スコープ内では、同じ名前の変数は同じものを参照していた方がプログラマ的にも楽なのです。

 

内部的な処理の話

ここまで巻き上げについてお伝えしてきましたが、JavaScriptの内部では巻き上げのための処理をおこなっていません。

JavaScriptは最初にファイルなどからコードを読み取り、内容を評価して実行できるような形に変換します。
そのときブロック({ })毎に変数や関数名の一覧(環境レコード)を作成しています。

この一覧は、内側から外側に変数を解決していく仕組みに利用されます。

巻き上げ

この仕組みについては、次のページを読んでみてください。
【JavaScript】 必須知識?スコープとレキシカル環境

実際にブロック内のコードが実行されると、最初のコードが実行される前に変数や関数の実体が作成されます。

■関数の初期値

関数宣言されたものは、名前に関数の実体が結びつけられます。

■var変数の初期値

var変数は、undefined値がセットされます。
var変数の定義を行っているコード処理時に、値を代入している場合は、その値がセットされます。

■let/const変数の初期値

そしてletやconstは、”初期化されていない”という意味のフラグがセットされます。
変数を参照して、”初期化されていない”フラグがセットされていると、エラーが発生します。

コードの処理を続け、変数を定義しているコードで値を代入している場合は、その値がセットされます。
代入していない場合は、undefined値がセットされます。


let a; // undefinedがセットされる
let b = 1; // 1がセットされる
const c; // 構文エラー

 

まとめ

巻き上げは実際に定義が移動しているのではなく、移動しているように見える処理を内部的におこなっています。

巻き上げは、これを使ってコードをどうこうするという話ではありません。

しかし知識として頭の片隅において、プログラムを作成する必要はありそうです。

参考記事:
【JavaScript】 ECMAScript2020から変数宣言を読み解く

更新日:2021/08/23

書いた人(管理人):けーちゃん

スポンサーリンク

記事の内容について

null

こんにちはけーちゃんです。
説明するのって難しいですね。

「なんか言ってることおかしくない?」
たぶん、こんなご意見あると思います。

裏付けを取りながら記事を作成していますが、僕の勘違いだったり、そもそも情報源の内容が間違えていたりで、正確でないことが多いと思います。
そんなときは、ご意見もらえたら嬉しいです。

掲載コードについては事前に動作確認をしていますが、貼り付け後に体裁を整えるなどをした結果動作しないものになっていることがあります。
生暖かい視線でスルーするか、ご指摘ください。

ご意見、ご指摘はこちら。
https://note.affi-sapo-sv.com/info.php

 

このサイトは、リンクフリーです。大歓迎です。