JavaScriptの基礎を勉強しているのですが、プログラミングの初歩である変数で壁にぶつかりました。
2冊ほど入門書を読み、他有料サービスを使って学習をしてましたが、変数の応用的なことがわからず、途方に暮れております。
先に進めない理由が変数に囚われていることが原因かと思い、今回は、熟練者の方々に知恵をお借りしたいと思い質問いたしました。
早速、下記の簡単なコードに触れていきたいと思います。
JavaScript
1'use strict'; 2 3let sum = 0; 4 5 for (let i = 0; i < 100; i++) { 6 sum += i; 7 } 8 9console.log(sum);
上記のコードは、変数をブロック外で宣言さえすれば、ブロック内で再代入した値は、ブロック外に持ち出して利用することができると認識しています。
しかし、なぜそうなるのかが説明できません。
説明するには、どのような知識が必要でしょうか。
次にストップウォッチの一部のコードについて触れていきたいと思います。
JavaScript
1'use strict'; 2 3 // 値を取得 4 const start = document.getElementById('start'); 5 6 let startTime; 7 8 function countUp() { 9 console.log(Date.now() - startTime); 10 11 setTimeout( () => { 12 countUp(); 13 }, 10); 14 } 15 16 start.addEventListener('click', () => { 17 startTime = Date.now(); 18 19 countUp(); 20 }); 21```あらかじめブロック外で変数startTimeを宣言しておいて、クリックイベント内で変数startTimeに新しく値を再代入する。 22 23その後、countUp関数で再代入した値を参照していますが、なぜ関数内で値が参照できるのか理解ができません。 24 25**値を参照する際は、ブロック内で再代入された値が優先されているのでしょうか?** 26 27また、**ブロック外で変数の宣言のみをすることに何か意味などあるのでしょうか?** 28 29基礎に関する質問で申し訳ないのですが、ご教授の程よろしくお願い致します
気になる質問をクリップする
クリップした質問は、後からいつでもMYページで確認できます。
またクリップした質問に回答があった際、通知やメールを受け取ることができます。
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
2020/02/14 15:13
回答5件
0
ベストアンサー
値を参照する際は、ブロック内で再代入された値が優先されているのでしょうか?
着目すべきは「値」ではありません。
- 変数は宣言するとメモリ上に値を格納できる場所を確保するものです。
代入は「メモリ上に確保した場所」を参照して、値を入れ替えています。
コンピュータのハードウェアレベルでメモリと値の関係をざっくり説くと
- メモリの指定アドレスに対して値を格納する(代入/書き換え/削除)。
- メモリの指定アドレスに対して格納された値を参照する。
といった動作があります。マシン語(機械語)/アセンブリ言語では、名前の付いた変数はなく、メモリ番地(アドレス)を指定して値を扱います。
JavaScriptなど、(人が理解しやすい)高級言語では、メモリ番地を指定することなく処理がかけるように変数という仕様がありますので、再代入された値ではなく、代入先のメモリ番地を参照しています。
JavaScriptのスコープの概念は ブロックの内側にあるものを優先して利用しますが、次のように解釈するのが正しいと思います。
javascript
1let a = 10; // =メモリ上に新しい場所を確保(1) 2let b = 20; // =メモリ上に新しい場所を確保(2) 3 4(function(){ 5 let a = 5; // =メモリ上に新しい場所を確保(3) 6 // 同じ名前の変数を宣言しているが、メモリ番地は別 7 b -= 10; // ブロック内にないので、外で宣言された 8 // b が指すメモリ番地に変更を加える 9 console.log( a, b ); // 5, 10 10})(); 11 12console.log( a, b ); // 10, 10 13
ブロック外で変数の宣言のみをすることに何か意味などあるのでしょうか?
上述した解釈で、ご質問にも示される setTimeout の第一引数(コールバック関数) ストップウォッチのコード を眺めてください。
関数内で宣言すると、カウントできません。
次のようなコードになります。
javascript
1const start = document.getElementById('start'); 2// 1)メモリ番地を(とりあえず)確保 3let startTime; 4function countUp() { 5 6 // 3)以降繰り返し. startTime のメモリを参照し格納済みの値を読む 7 console.log(Date.now() - startTime); 8 9 setTimeout( () => { 10 countUp(); // 再帰呼び出し 11 }, 10); 12} 13start.addEventListener('click', () => { 14 // 2)startTime のメモリに Date値を格納 15 startTime = Date.now(); 16 countUp(); 17});
JavaScriptは、様々な処理を行うために非同期で実行する処理も多く、結果を受け取る手段として、ブロックの外で宣言する変数が必要になります。
投稿2020/02/13 21:55
編集2020/02/13 22:21総合スコア5434
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
退会済みユーザー
2020/02/15 21:00 編集
0
全ての挙動は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文のブロックとかは通常のレキシカル環境です。それぞれの環境はグローバル環境が一番外側にあって、ネストされた構成になっています。(今回、モジュール環境については説明しません。)
JavaScript
1// グローバル環境 ===> 2"use strict"; 3let x = 1; 4const f = function() { 5 // 関数 f の関数環境 ---> 6 let y = 2; 7 if (true) { 8 // if 文ブロックのレキシカル環境 ~~~> 9 let z = 3; 10 console.log(x, y, z); // => 1 2 3 11 // <~~~ if 文ブロックのレキシカル環境 12 } 13 // <--- 関数 f の関数環境 14}; 15const g = function() { 16 // 関数 g の関数環境 ---> 17 let y = 42; 18 f(); 19 console.log(y) // => 42 20 // <--- 関数 g の関数環境 21}; 22g(); 23// <=== グローバル環境
それぞれのレキシカル環境は環境レコードを持ちます。この環境レコードの見える範囲、すなわちスコープがその環境そのものになります。そのため、それぞれの環境の範囲をグローバルスコープ、関数スコープ、ブロックスコープなどと呼ぶこともあります(これらのスコープは仕様書に定められた正式な名称ではありません)。
ここで重要なのは各レキシカル環境はネストされた構造になっていると言うことです。各レキシカル環境は自分の外側(outer)のレキシカル環境を把握しており、一番外側がグローバル環境になります。この関係を覚えておいてください。
さて、変数は環境レコードに束縛されているという名前付きの値と言うことでした。これはどういうことかというと、ある環境で変数が宣言されると、その宣言が書かれたレキシカル環境の環境レコードにその変数名で値が束縛されます。ただし、var
を使った場合と関数宣言の場合は特殊で、関数環境、モジュール環境、グローバル環境のうち一番近い環境の環境レコードに束縛されます。値の初期状態では、宣言の方法によって異なり、指定された値になる、undefined
になる、または、未初期化(uninitialized)という特別な状態になるかのいずれかです。この束縛処理はレキシカル環境の評価の開始時に、存在する全ての変数に対して行われます。※
※ 関数宣言を除き、変数に指定された初期値が代入されるタイミングは宣言文が来たときであり、それまでは開始時の変数作成時に入れられた値が使われます。var
で宣言されたは最初にundefined
が入るため、宣言文の前に変数にアクセスしてもエラーになりません。let
とconst
で宣言された変数は未初期化(uninitialized)という特別な状態になるため、宣言文の前に変数にアクセスしようとするReferenceError
例外が発生します。いずれの場合も、変数自体は宣言文が来る前に存在していることに注意してください。この動作はC等の他言語にはあまり見られない、ECMAScript特有のものです。
では、今度はアクセスがどうなるかを見てみましょう。あるレキシカル環境で変数にアクセスしたとき(変数への代入含むが、宣言は含まない)、そのレキシカル環境での環境レコードに変数名で索漠された値があるかどうかを見に行きます。もし、環境レコードに変数名の値があれば、その値を取得する(代入であれば指定の値を代入する)ことになります。逆に、変数名が環境レコードになかった場合、今度はそのレキシカル環境の外側(oute)のレキシカル環境の環境レコードを見に行きます。もし、そこにあればその値の取得(または指定の値の代入)、なければまた外側、と内から外へ順番に辿っていきます。一番外側のグローバル環境の環境レコード※にもなければ、ReferenceError
になります。
※ グローバル環境の環境レコードをグローバル環境レコード(global environment record)と呼びますが、これはグローバルオブジェクト(global object)と深い関係を持っています。ただ、その説明をするとかなり長くなるので、今回は割愛します。
この動作、何かに似ているとは思いませんか?そう、プロトタイプチェーン(prototype chain)です。先程、レキシカル環境の範囲をスコープと呼ぶことがあると言いましたが、入れ子になったスコープがチェーンとして繋がって、順番に見に行くと言うことから、この動作をスコープチェーンと呼ぶこともあります(スコープチェーンは仕様書に定められた正式な名称ではありません)。
さぁ、ここまで読んだならもうわかりましたね。ブロック内だろうが、関数内だろうが、もし、そこに宣言されていない場合は、ブロックや関数の外側に範囲を拡げて変数を探しに行くと言うことです。難しいことをたくさん言ったような気がしますが、単純に言えば、たったそれだけです。注意して欲しいのは、もし宣言が見つかったなら、つまり、環境レコードに変数名が付いた値が存在したら、そこで探索は終了してしまうと言うことです。そして、もう一つ、この探索は外側には向かうが、内側には向かわないと言うことです。内側に同じ名前の変数があっても外側には影響しないと言うことです。
難しく言っているだけでさっぱりだな…と思ったかも知れません。ですが、実は上の話はクロージャーに通じる話なのです。上の動作を維持するには内側の環境レコードは外側の環境レコードを常に見に行けるようにしておかなくてはなりません。関数は関数環境を構成し、関数が評価されるときには環境レコードを作る必要があります。その時、関数の外側、つまりは、関数が定義されたところの環境レコードがなければ、外側に見に行くと言うことができません。そのためには、関数は自分が定義されたときの環境レコードを持ち続ける必要があります。それがクロージャーの正体なのです。※
※ 環境レコードもGCの対象です。どこからも参照されていない環境レコードは削除されます。もちろん、その中にあった変数もです。しかし、上のように関数によって参照され続ける関数レコードは、その関数がなくならない限り、削除されることはありません。これもまた、オブジェクトと似たような関係になっています。
うまく説明できたとは思ってはいません。詳しい動作を話せば切りがないですし、私もまだ理解できていない部分もあります。本当の詳細な動作を知りたい場合は最初にあげた仕様書を読んでくださいとしか言えない実力不足の私を許してください。
より詳しい方がもっとより良い説明をしてくださると助かります。また、間違い等がありましたら、ご指摘をお願いします。
投稿2020/02/14 13:32
編集2020/02/14 22:42総合スコア21737
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
退会済みユーザー
2020/02/15 20:54
0
※これからする説明は分かりやすさを優先しているため、実際の仕様や実装と異なる部分があると思いますので、ご了解ください。あと、ホイスティングは悪い文明です。
JavaScript では、定義されたスコープに変数が閉じ込められ、外側に影響することはできません。
そして、変数を参照する際に、スコープを内側から順番に探してきます。
js
1{ // A 2 let x = 0; 3 { // B 4 let x = 1; 5 console.log( x ); // (1) 6 } 7}
上記のコード、(1)で変数x
を参照していますが、ここでJavaScriptは変数x
を探しに行きます。
まず、参照しているスコープ、Bの中で探したところ、let x = 1;
という定義文が見つかりました。
なので、(1)の変数x
は、スコープBのlet x = 1;
で定義されている変数だ、とJavaScriptは判断します。
js
1{ // A 2 let x = 0; 3 { // B 4 console.log( x ); // (2) 5 } 6}
上記のコード、(2)で変数x
を参照していますが、やはりここでJavaScriptは変数x
を探しに行きます。
まず、参照しているスコープ、Bの中で探したところ、定義文が見つかりませんでした。
そこで、その外側のスコープ、Aの中で探したところ、let x = 0;
という定義文が見つかりました。
なので、(2)の変数x
は、スコープAのlet x = 0;
で定義されている変数だ、とJavaScriptは判断します。
ついてこれていますか、大丈夫ですか。
js
1{ // A 2 let x = 0; 3 { // B 4 x = x + 1; // (3) 5 console.log(x); 6 { // C 7 let x = 10; 8 console.log(x); 9 { // D 10 x = x + 1; // (4) 11 console.log(x); 12 } 13 x = x + 1; // (5) 14 console.log(x); 15 } 16 x = x + 1; // (6) 17 console.log(x); 18 } 19 x = x + 1; // (7) 20 console.log(x); 21}
上記のコードについて。
(3)で変数x
を参照していますが、やはりここでJavaScriptは変数x
を探しに行きます。
まず、参照しているスコープ、Bの中で探したところ、定義文が見つかりませんでした。
そこで、その外側のスコープ、Aの中で探したところ、定義文が見つかりました。
スコープC、Dの定義は閉じ込められているので、外側から参照できません。
なので、(3)の変数x
は、スコープAで定義されている変数だ、とJavaScriptは判断し、その値0
に1
を足します。
つまり、ここでの変数x
は1
です。
(4)で変数x
を参照していますが、やはりここでJavaScriptは変数x
を探しに行きます。
まず、参照しているスコープ、Dの中で探したところ、定義文が見つかりませんでした。
そこで、その外側のスコープ、Cの中で探したところ、定義文が見つかりました。
なので、(4)の変数x
は、スコープCで定義されている変数だ、とJavaScriptは判断し、その値10
に1
を足します。
つまり、ここでの変数x
は11
です。
(5)で変数x
を参照していますが、やはりここでJavaScriptは変数x
を探しに行きます。
まず、参照しているスコープ、Cの中で探したところ、定義文が見つかりました。
スコープDの定義は閉じ込められているので、外側から参照できません。
なので、(5)の変数x
は、スコープCで定義されている変数だ、とJavaScriptは判断し、その値11
に1
を足します。
つまり、ここでの変数x
は12
です。
(6)で変数x
を参照していますが、やはりここでJavaScriptは変数x
を探しに行きます。
まず、参照しているスコープ、Bの中で探したところ、定義文が見つかりませんでした。
そこで、その外側のスコープ、Aの中で探したところ、定義文が見つかりました。
スコープC、Dの定義は閉じ込められているので、外側から参照できません。
なので、(6)の変数x
は、スコープAで定義されている変数だ、とJavaScriptは判断し、その値1
に1
を足します。
つまり、ここでの変数x
は2
です。
(7)で変数x
を参照していますが、やはりここでJavaScriptは変数x
を探しに行きます。
まず、参照しているスコープ、Aの中で探したところ、定義文が見つかりました。
スコープB、C、Dの定義は閉じ込められているので、外側から参照できません。
なので、(7)の変数x
は、スコープAで定義されている変数だ、とJavaScriptは判断し、その値2
に1
を足します。
つまり、ここでの変数x
は3
です。
以上、ご理解いただけましたか?
投稿2020/02/14 02:04
編集2020/02/14 07:16総合スコア36898
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
退会済みユーザー
2020/02/15 20:55 編集
0
簡単に言うと、変数はメモリ上に箱を確保した状態です。
読み書きの際、その変数に何か操作しているのではなく
その変数が持つ箱のアドレス先に対して行っている。
こう考えたら理解できますか?
なので色々なブロック内で、その変数を読み書きしていますが
これは変数ではなく、そのアドレス先にアクセスしてるイメージです。
投稿2020/02/14 01:45
総合スコア333
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
退会済みユーザー
2020/02/15 19:17
0
とりあえず変数のスコープと生存期間、中でも静的スコープあたりについて調べていただくとして、
なぜそうなるのかが説明できません。
なぜ関数内で値が参照できるのか理解ができません。
わりと「そのように言語仕様が作られているから」としか言いようがありません。
そこは見えた方が便利なのでそう作られました。
ただ、もっと単純なもの、具体的にはC言語の関数内のスコープについては、関数の呼び出し時に変数を確保しようとすると(コール)スタックに置くのが楽で、その結果関数が終わった時点で変数が破壊され(他の用途に再利用を許すようになり)他から使えなくなるという挙動に自然になります。(スタックについては適宜調べてください)
そのあたりの自然な挙動から、可視性を制限することがプログラムを書きやすくすることに気づき、敢えて可視性を制限する様々なスコープ関連の言語仕様が生まれたのかなあと思っています。
投稿2020/02/14 01:17
総合スコア3047
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
退会済みユーザー
2020/02/15 20:57 編集
あなたの回答
tips
太字
斜体
打ち消し線
見出し
引用テキストの挿入
コードの挿入
リンクの挿入
リストの挿入
番号リストの挿入
表の挿入
水平線の挿入
プレビュー
質問の解決につながる回答をしましょう。 サンプルコードなど、より具体的な説明があると質問者の理解の助けになります。 また、読む側のことを考えた、分かりやすい文章を心がけましょう。