質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

89.63%

JavaScript 変数の挙動を詳しく知りたい

解決済

回答 5

投稿

  • 評価
  • クリップ 3
  • VIEW 617

Nura

score 1

JavaScriptの基礎を勉強しているのですが、プログラミングの初歩である変数で壁にぶつかりました。

2冊ほど入門書を読み、他有料サービスを使って学習をしてましたが、変数の応用的なことがわからず、途方に暮れております。

先に進めない理由が変数に囚われていることが原因かと思い、今回は、熟練者の方々に知恵をお借りしたいと思い質問いたしました。

早速、下記の簡単なコードに触れていきたいと思います。

'use strict';

let sum = 0;

  for (let i = 0; i < 100; i++) {
    sum += i;
  }

console.log(sum);

上記のコードは、変数をブロック外で宣言さえすれば、ブロック内で再代入した値は、ブロック外に持ち出して利用することができると認識しています。

しかし、なぜそうなるのかが説明できません。
説明するには、どのような知識が必要でしょうか。

次にストップウォッチの一部のコードについて触れていきたいと思います。

'use strict';

 // 値を取得
  const start = document.getElementById('start');
 
 let startTime;

   function countUp() {
     console.log(Date.now() - startTime);
 
     setTimeout( () => {
       countUp();
     }, 10);
  }

   start.addEventListener('click', () => {
   startTime = Date.now();
 
     countUp();
   });

あらかじめブロック外で変数startTimeを宣言しておいて、クリックイベント内で変数startTimeに新しく値を再代入する。

その後、countUp関数で再代入した値を参照していますが、なぜ関数内で値が参照できるのか理解ができません。

値を参照する際は、ブロック内で再代入された値が優先されているのでしょうか?

また、ブロック外で変数の宣言のみをすることに何か意味などあるのでしょうか?

基礎に関する質問で申し訳ないのですが、ご教授の程よろしくお願い致します

  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 過去に投稿した質問と同じ内容の質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

質問への追記・修正、ベストアンサー選択の依頼

  • maisumakun

    2020/02/14 11:06

    > 値を参照する際は、ブロック内で再代入された値が優先されているのでしょうか?

    「何に対して」優先される、ということを主張したいのでしょうか。その対比されるべきものが自分にはわかりません。

    キャンセル

  • otn

    2020/02/15 00:13

    何が疑問なのか不明です。言語仕様は理解されているように見えます。
    ・実装を知りたい
    ・言語設計者が何故そのような言語仕様に決めたのか知りたい
    とかでしょうか?あるいはちがう?

    キャンセル

回答 5

checkベストアンサー

+5

値を参照する際は、ブロック内で再代入された値が優先されているのでしょうか?

着目すべきは「値」ではありません。

  • 変数は宣言するとメモリ上に値を格納できる場所を確保するものです。
    代入は「メモリ上に確保した場所」を参照して、値を入れ替えています。

コンピュータのハードウェアレベルでメモリと値の関係をざっくり説くと

  1. メモリの指定アドレスに対して値を格納する(代入/書き換え/削除)。
  2. メモリの指定アドレスに対して格納された値を参照する。

といった動作があります。マシン語(機械語)/アセンブリ言語では、名前の付いた変数はなく、メモリ番地(アドレス)を指定して値を扱います。

JavaScriptなど、(人が理解しやすい)高級言語では、メモリ番地を指定することなく処理がかけるように変数という仕様がありますので、再代入された値ではなく、代入先のメモリ番地を参照しています。

JavaScriptのスコープの概念は ブロックの内側にあるものを優先して利用しますが、次のように解釈するのが正しいと思います。

let a = 10; // =メモリ上に新しい場所を確保(1)
let b = 20; // =メモリ上に新しい場所を確保(2)

(function(){
  let a = 5; // =メモリ上に新しい場所を確保(3)
             // 同じ名前の変数を宣言しているが、メモリ番地は別
  b -= 10; // ブロック内にないので、外で宣言された
           // b が指すメモリ番地に変更を加える
  console.log( a, b ); // 5, 10
})();

console.log( a, b ); // 10, 10

ブロック外で変数の宣言のみをすることに何か意味などあるのでしょうか?

上述した解釈で、ご質問にも示される setTimeout の第一引数(コールバック関数) ストップウォッチのコード を眺めてください。

関数内で宣言すると、カウントできません。
次のようなコードになります。

const start = document.getElementById('start');
// 1)メモリ番地を(とりあえず)確保
let startTime;
function countUp() {

  // 3)以降繰り返し. startTime のメモリを参照し格納済みの値を読む
  console.log(Date.now() - startTime);

  setTimeout( () => {
    countUp(); // 再帰呼び出し
  }, 10);
}
start.addEventListener('click', () => {
  // 2)startTime のメモリに Date値を格納
  startTime = Date.now();
  countUp();
});


JavaScriptは、様々な処理を行うために非同期で実行する処理も多く、結果を受け取る手段として、ブロックの外で宣言する変数が必要になります。

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2020/02/16 04:13 編集

    返信が遅くなり、誠に申し訳ございません。

    抽象的な質問に対し、丁寧にご説明いただき誠にありがとうございます!
    今回、メモリやアドレスという存在について知れたことにより、変数の根本的な認識を変えることできた気がします。

    このもやを非同期処理の勉強の前にどうしても晴らしたかったので、とても助かりました。

    本当にありがとうございます。

    キャンセル

+4

※これからする説明は分かりやすさを優先しているため、実際の仕様や実装と異なる部分があると思いますので、ご了解ください。あと、ホイスティングは悪い文明です。


JavaScript では、定義されたスコープに変数が閉じ込められ、外側に影響することはできません。
そして、変数を参照する際に、スコープを内側から順番に探してきます。
 

{ // A
  let x = 0;
  { // B
    let x = 1;
    console.log( x ); // (1)
  }
}


上記のコード、(1)で変数xを参照していますが、ここでJavaScriptは変数xを探しに行きます。
まず、参照しているスコープ、Bの中で探したところ、let x = 1;という定義文が見つかりました。
なので、(1)の変数xは、スコープBのlet x = 1;で定義されている変数だ、とJavaScriptは判断します。
 

{ // A
  let x = 0;
  { // B
    console.log( x ); // (2)
  }
}


上記のコード、(2)で変数xを参照していますが、やはりここでJavaScriptは変数xを探しに行きます。
まず、参照しているスコープ、Bの中で探したところ、定義文が見つかりませんでした。
そこで、その外側のスコープ、Aの中で探したところ、let x = 0;という定義文が見つかりました。
なので、(2)の変数xは、スコープAのlet x = 0;で定義されている変数だ、とJavaScriptは判断します。
 
ついてこれていますか、大丈夫ですか。

 

{ // A
  let x = 0; 
  { // B
    x = x + 1; // (3)
    console.log(x);
    { // C
      let x = 10;
      console.log(x);
      { // D
        x = x + 1;  // (4)
        console.log(x);
      }
      x = x + 1;  // (5)
      console.log(x);
    }
    x = x + 1;  // (6)
    console.log(x);
  }
  x = x + 1;  // (7)
  console.log(x);
}


上記のコードについて。

(3)で変数xを参照していますが、やはりここでJavaScriptは変数xを探しに行きます。
まず、参照しているスコープ、Bの中で探したところ、定義文が見つかりませんでした。
そこで、その外側のスコープ、Aの中で探したところ、定義文が見つかりました。
スコープC、Dの定義は閉じ込められているので、外側から参照できません。
なので、(3)の変数xは、スコープAで定義されている変数だ、とJavaScriptは判断し、その値01を足します。
つまり、ここでの変数x1です。

(4)で変数xを参照していますが、やはりここでJavaScriptは変数xを探しに行きます。
まず、参照しているスコープ、Dの中で探したところ、定義文が見つかりませんでした。
そこで、その外側のスコープ、Cの中で探したところ、定義文が見つかりました。
なので、(4)の変数xは、スコープCで定義されている変数だ、とJavaScriptは判断し、その値101を足します。
つまり、ここでの変数x11です。

(5)で変数xを参照していますが、やはりここでJavaScriptは変数xを探しに行きます。
まず、参照しているスコープ、Cの中で探したところ、定義文が見つかりました。
スコープDの定義は閉じ込められているので、外側から参照できません。
なので、(5)の変数xは、スコープCで定義されている変数だ、とJavaScriptは判断し、その値111を足します。
つまり、ここでの変数x12です。

(6)で変数xを参照していますが、やはりここでJavaScriptは変数xを探しに行きます。
まず、参照しているスコープ、Bの中で探したところ、定義文が見つかりませんでした。
そこで、その外側のスコープ、Aの中で探したところ、定義文が見つかりました。
スコープC、Dの定義は閉じ込められているので、外側から参照できません。
なので、(6)の変数xは、スコープAで定義されている変数だ、とJavaScriptは判断し、その値11を足します。
つまり、ここでの変数x2です。

(7)で変数xを参照していますが、やはりここでJavaScriptは変数xを探しに行きます。
まず、参照しているスコープ、Aの中で探したところ、定義文が見つかりました。
スコープB、C、Dの定義は閉じ込められているので、外側から参照できません。
なので、(7)の変数xは、スコープAで定義されている変数だ、とJavaScriptは判断し、その値21を足します。
つまり、ここでの変数x3です。
 
以上、ご理解いただけましたか?

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2020/02/16 04:50 編集

    返信が遅くなり、誠に申し訳ございません。

    わかりやすく問題形式で記載いただいたコードは、初学者の私レベルにはとても理解しやすかったです!

    特に(4)以降の問題は、初学者には難しいなと感じたので、参考になりました。

    ご回答いただきありがとうございます。

    キャンセル

+4

全ての挙動はStandard ECMA-262※に詳しく書いてあります。

※ JavaScriptはECMAScriptと言う言語を基にDOM等のブラウザ固有のAPIを追加した言語です。ECMAScriptでは文法とブラウザという環境に依存しない組込オブジェクトを定めています。つまり、文法に関してはECMAScriptそのものと同一と言うことです。

以上です。


では、さすがになんなんなので、説明していきたいと思います。たぶん、非常にわかりにくい説明になっていると思います。ECMAScriptの仕様はかなり複雑ですが、視点を変えれば非常に単純でもあります。もしかしたら、最初は読んでも理解できないかも知れません。それでも、ECMAScriptの真髄が少しでも伝われば幸いです。

なお、古い文法であるwith文とか多くの場合で非推奨とされるevalを使った場合とかはもっと複雑怪奇になる(というか、私の力では説明し切れない)ので除外します。この二つを使うことはないと信じています。また、厳格モード("use strict";)は有効であることを前提とします。モジュールはちらっと話ができたらいいなと思っていましたが、私が混乱しそうなので、まずはモジュールは無しという前提とします。仕様はECMAScript 2015以降に準拠とし、仕様書自体はECMAScript 2019を参考にしています。ECMAScript 5以前では動作は同じでも解釈に異なる点があることにご注意ください。


まず始めに、知っておかなくてはならない用語があります。それはレキシカル環境(lexical environment)と環境レコード(environment record)です。変数とは、宣言された場所に基づくレキシカル環境の環境レコードに変数名で束縛された(bound)値です。この値はプリミティブ値(primitive value)またはオブジェクト(object)です。なんか似たようなものを見たことがありませんか?そう、通常のオブジェクトにおけるプロパティもプロパティ名で束縛された値でした。全く一緒というわけではありませんが、環境レコードをオブジェクトとみなした場合、変数として束縛されたものは環境レコードのプロパティのようなものと捉えることができます。

ここまではついてきていますか?よし、いいでしょう。よくわからないレキシカル環境というものが出てきました。まずは、これを読み解く必要があります。レキシカル環境とは文法上区別された一定の範囲のことです。トップレベルのスクリプト全体、モジュール、関数定義、ブロック文等があります。特別なものとして、トップレベルに使われるグローバル環境(global environment)、モジュールで使われるモジュール環境(module environment)、関数で使われる関数環境(function environment)があり、ちょっとだけ特別な動作をします。if文のブロックとかは通常のレキシカル環境です。それぞれの環境はグローバル環境が一番外側にあって、ネストされた構成になっています。(今回、モジュール環境については説明しません。)

// グローバル環境 ===>
"use strict";
let x = 1;
const f = function() {
  // 関数 f の関数環境 --->
  let y = 2;
  if (true) {
    // if 文ブロックのレキシカル環境 ~~~>
    let z = 3;
    console.log(x, y, z); // => 1 2 3
    // <~~~ if 文ブロックのレキシカル環境
  }
  // <--- 関数 f の関数環境
};
const g = function() {
  // 関数 g の関数環境 --->
  let y = 42;
  f();
  console.log(y) // => 42
  // <--- 関数 g の関数環境
};
g();
// <=== グローバル環境

それぞれのレキシカル環境は環境レコードを持ちます。この環境レコードの見える範囲、すなわちスコープがその環境そのものになります。そのため、それぞれの環境の範囲をグローバルスコープ、関数スコープ、ブロックスコープなどと呼ぶこともあります(これらのスコープは仕様書に定められた正式な名称ではありません)。

ここで重要なのは各レキシカル環境はネストされた構造になっていると言うことです。各レキシカル環境は自分の外側(outer)のレキシカル環境を把握しており、一番外側がグローバル環境になります。この関係を覚えておいてください。

さて、変数は環境レコードに束縛されているという名前付きの値と言うことでした。これはどういうことかというと、ある環境で変数が宣言されると、その宣言が書かれたレキシカル環境の環境レコードにその変数名で値が束縛されます。ただし、varを使った場合と関数宣言の場合は特殊で、関数環境、モジュール環境、グローバル環境のうち一番近い環境の環境レコードに束縛されます。値の初期状態では、宣言の方法によって異なり、指定された値になる、undefinedになる、または、未初期化(uninitialized)という特別な状態になるかのいずれかです。この束縛処理はレキシカル環境の評価の開始時に、存在する全ての変数に対して行われます。※

※ 関数宣言を除き、変数に指定された初期値が代入されるタイミングは宣言文が来たときであり、それまでは開始時の変数作成時に入れられた値が使われます。varで宣言されたは最初にundefinedが入るため、宣言文の前に変数にアクセスしてもエラーになりません。letconstで宣言された変数は未初期化(uninitialized)という特別な状態になるため、宣言文の前に変数にアクセスしようとするReferenceError例外が発生します。いずれの場合も、変数自体は宣言文が来る前に存在していることに注意してください。この動作はC等の他言語にはあまり見られない、ECMAScript特有のものです。

では、今度はアクセスがどうなるかを見てみましょう。あるレキシカル環境で変数にアクセスしたとき(変数への代入含むが、宣言は含まない)、そのレキシカル環境での環境レコードに変数名で索漠された値があるかどうかを見に行きます。もし、環境レコードに変数名の値があれば、その値を取得する(代入であれば指定の値を代入する)ことになります。逆に、変数名が環境レコードになかった場合、今度はそのレキシカル環境の外側(oute)のレキシカル環境の環境レコードを見に行きます。もし、そこにあればその値の取得(または指定の値の代入)、なければまた外側、と内から外へ順番に辿っていきます。一番外側のグローバル環境の環境レコード※にもなければ、ReferenceError になります。

※ グローバル環境の環境レコードをグローバル環境レコード(global environment record)と呼びますが、これはグローバルオブジェクト(global object)と深い関係を持っています。ただ、その説明をするとかなり長くなるので、今回は割愛します。

この動作、何かに似ているとは思いませんか?そう、プロトタイプチェーン(prototype chain)です。先程、レキシカル環境の範囲をスコープと呼ぶことがあると言いましたが、入れ子になったスコープがチェーンとして繋がって、順番に見に行くと言うことから、この動作をスコープチェーンと呼ぶこともあります(スコープチェーンは仕様書に定められた正式な名称ではありません)。

さぁ、ここまで読んだならもうわかりましたね。ブロック内だろうが、関数内だろうが、もし、そこに宣言されていない場合は、ブロックや関数の外側に範囲を拡げて変数を探しに行くと言うことです。難しいことをたくさん言ったような気がしますが、単純に言えば、たったそれだけです。注意して欲しいのは、もし宣言が見つかったなら、つまり、環境レコードに変数名が付いた値が存在したら、そこで探索は終了してしまうと言うことです。そして、もう一つ、この探索は外側には向かうが、内側には向かわないと言うことです。内側に同じ名前の変数があっても外側には影響しないと言うことです。

難しく言っているだけでさっぱりだな…と思ったかも知れません。ですが、実は上の話はクロージャーに通じる話なのです。上の動作を維持するには内側の環境レコードは外側の環境レコードを常に見に行けるようにしておかなくてはなりません。関数は関数環境を構成し、関数が評価されるときには環境レコードを作る必要があります。その時、関数の外側、つまりは、関数が定義されたところの環境レコードがなければ、外側に見に行くと言うことができません。そのためには、関数は自分が定義されたときの環境レコードを持ち続ける必要があります。それがクロージャーの正体なのです。※

※ 環境レコードもGCの対象です。どこからも参照されていない環境レコードは削除されます。もちろん、その中にあった変数もです。しかし、上のように関数によって参照され続ける関数レコードは、その関数がなくならない限り、削除されることはありません。これもまた、オブジェクトと似たような関係になっています。


うまく説明できたとは思ってはいません。詳しい動作を話せば切りがないですし、私もまだ理解できていない部分もあります。本当の詳細な動作を知りたい場合は最初にあげた仕様書を読んでくださいとしか言えない実力不足の私を許してください。

より詳しい方がもっとより良い説明をしてくださると助かります。また、間違い等がありましたら、ご指摘をお願いします。

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2020/02/16 05:54

    返信が遅くなり、誠に申し訳ございません。

    私の「なぜ」のような思考停止質問に対しても、とても詳しく説明していただいたおかげで、静的スコープや参照する際の具体的なルートなどが頭の中で整理できた気がします。

    クロージャーについても通ずる部分があるということは、必ず理解しておくべき問題だったんですね。

    基礎が理解できていないのを再確認することができてかなりスッキリしました。

    これで、やっと次に進めます。
    本当にありがとうございました!!!

    キャンセル

0

とりあえず変数のスコープと生存期間、中でも静的スコープあたりについて調べていただくとして、

なぜそうなるのかが説明できません。
なぜ関数内で値が参照できるのか理解ができません。

わりと「そのように言語仕様が作られているから」としか言いようがありません。
そこは見えた方が便利なのでそう作られました。

ただ、もっと単純なもの、具体的にはC言語の関数内のスコープについては、関数の呼び出し時に変数を確保しようとすると(コール)スタックに置くのが楽で、その結果関数が終わった時点で変数が破壊され(他の用途に再利用を許すようになり)他から使えなくなるという挙動に自然になります。(スタックについては適宜調べてください)
そのあたりの自然な挙動から、可視性を制限することがプログラムを書きやすくすることに気づき、敢えて可視性を制限する様々なスコープ関連の言語仕様が生まれたのかなあと思っています。

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2020/02/16 04:15 編集

    返信が遅くなり、誠に申し訳ございません。

    変数のスコープ・静的スコープは、詳しく調べておりませんでした。
    私自身スコープは、「変数の有効範囲なんだなぁ」くらいの認識でしたので、お教えいただいた情報を辿り、今頭の中にある「なぜ」を解決していきたいと思います。

    ご回答いただきありがとうございます。

    キャンセル

-1

簡単に言うと、変数はメモリ上に箱を確保した状態です。
読み書きの際、その変数に何か操作しているのではなく
その変数が持つ箱のアドレス先に対して行っている。
こう考えたら理解できますか?

なので色々なブロック内で、その変数を読み書きしていますが
これは変数ではなく、そのアドレス先にアクセスしてるイメージです。

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2020/02/16 04:17

    返信が遅くなり、誠に申し訳ございません。

    変数を単に値を入れる箱と思いすぎて覚えた気になっていました。

    変数は宣言した時点で、メモリ上に保管され変数自体は番号(アドレス)に変換されている?
    ということは、
    宣言時:メモリ上→変数(番号が付く)→値
    アクセス時:メモリ上→アドレス(変数)→値

    このような考え方であっていますでしょうか。

    キャンセル

15分調べてもわからないことは、teratailで質問しよう!

  • ただいまの回答率 89.63%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる

同じタグがついた質問を見る