JavaScriptの変数・スコープ・スコープチェーンのまとめ

はじめに

JavaScriptの勉強中にややこしいところがあったのでメモ.
スコープチェーンとか実行コンテキストとかプロトタイプ周りについて言及します.

変数について

変数の種類

変数は,以前まではvarで宣言していましたが,ES6からはletconstでも宣言できるようになったんですね.

種類 詳細
var 関数スコープ
let ブロックスコープ
const 再代入不可

var

関数スコープ

関数スコープを持つ変数を宣言します.

var global = "I am global"

function f(){
  var local = "I am local";
  console.log(local); // I am local
}

f();

console.log(global); // I am global
console.log(local); // undefined
変数の巻き上げ

変数宣言が関数の途中で行われていた場合,その宣言は先頭に巻き上げられ,undefinedが代入されます.

function f(){
  // 変数の巻き上げが行われる
  // var local = undefined;

  console.log(local); // undefined
  
  var local = "I am local";
  
  console.log(local); // I am local
}

f();

これは,JavaScriptエンジンが,コードの実行に先立って,割り当て抜きの変数宣言を行うためです.

つまり,

  1. 変数localを発見したが,中身は割り当てない(undefined)
  2. --- コードの実行開始 ---
  3. console.log(local)を実行するが,localはundefinedのまま
  4. localに"I am local"が代入される
  5. console.log(local)を実行し,"I am local"が出力される

という順にJavaScriptエンジンが実行するため,上記のような出力がなされるというわけです.

スコープチェーン

呼び出された関数は,それぞれが実行コンテキストという情報を持つようになります.

実行コンテキストは,

  • 自身の関数内で定義されたローカル変数
  • 自身の関数内で定義された関数内関数

の情報を全て保持しています.

ある関数は,自身の実行コンテキスト内の情報と,自身を呼び出した元の関数の実行コンテキスト内の情報にアクセスすることができます.
一方で,自身が呼び出した関数の実行コンテキスト内の情報には,アクセスすることができません.

つまり,自分より浅い階層の関数が持つ実行コンテキストは参照できますが,自分より深い階層の持つ関数が持つ実行コンテキストは参照できないということになります.

f:id:Szarny:20170930143715p:plain:w300

var global = "I am global"

function outer(){
  var outer_local = "I am outer local";
  
  function inner(){
    var inner_local = "I am inner local";
    
    console.log(inner_local); // I am inner local
    console.log(outer_local); // I am outer local
    console.log(global); // I am global
  }
  
  inner();
  
  console.log(inner_local); // error
  console.log(outer_local); // I am outer local
  console.log(global); // I am global
}

outer();

console.log(inner_local); // error
console.log(outer_local); // error
console.log(global); // I am global

また,同名のプロパティが参照可能な複数の実行コンテキストに存在した際には,より近い実行コンテキストを参照します.
f:id:Szarny:20170930145035p:plain:w300

var val = 2;

function outer(){
  var val = 1;
  
  function inner(){
    console.log(val); // 1
  }
  
  inner();
}

outer();

let

letの特徴

letは,他のプログラミング言語で一般的にみられるようなブロックスコープを持ちます.
また,再宣言を許しません.

そのため,以下のような違いが出ます.

function var_func(){
    var a = 1;
    var b = 2;
    
    if(a < b){
        var message = "b is bigger!";
    }
    
    console.log(message); // b is bigger!
}

var_func();
function let_func(){
    let a = 1;
    let b = 2;
    
    if(a < b){
        let message = "b is bigger!";
    }
    
    console.log(message); // error
}

let_func();

const

constの特徴

constはletの再代入不可バージョンです.

var i = 1;
console.log(i); // 1

var i = 2;
console.log(i); // 2

const j = 1;
console.log(j); // 1

j = 2; // Uncaught TypeError: Assignment to constant variable.

但し,再代入不可というのは「その変数そのものに,新しいモノを代入するのはダメだ」という意味なので,オブジェクトの特定のプロパティの値を変更したり,配列の中身を入れ替えたりするといったことは可能です.
もちろん,オブジェクトの新規再代入は許されません.

const human = {
    age: 20,
    name: "hoge"
}

console.log(human) // {age: 20, name: "hoge"}

// これはセーフ
human.age = 100 
console.log(human) //{age: 100, name: "hoge"}

// これはアウト
human = {
    age: 99,
    name: "huga"
} // Uncaught TypeError: Assignment to constant variable.