JavaScript/変数
変数 (英: variable) は、オブジェクトやプリミティブに名前をつけ、名前での参照を可能にする機能です。
基本の基本
[編集]変数の宣言
[編集]「変数」は、オブジェクトに名前をつけて参照の手助けをする機能です。 型や値は変数ではなくオブジェクトとプリミティブが保持しています[1]。 静的な言語に慣れた人は、やや戸惑いを感じるかもしれません。
JavaScriptで変数を宣言するには let
, const
または var
キーワードで宣言します。
宣言と代入
[編集]- 初期化なし宣言と代入
let a; // 変数 a を(初期化なしで)宣言 console.log(a); // 未初期化の変数の値は undefined a = 15; // 変数 a に 0 を代入 console.log(a); // 変数 a の値が undefined から 0 に変更 a = "abc"; // 変数 a に 0 を代入 console.log(a); // 変数 a の値が 0 から "abc" に変更
- 実行結果
undefined 15 abc
- ポイント
- 変数を初期化しないで宣言すると、変数の値は undefined になります
- let宣言された変数への代入は何度でも出来ます
- その場合、先に代入されている値と次に代入される値の「型」同じでなくてもいい
- この例で変数 a は undefined → 15 → abc と値が推移しています
- 初期化あり宣言と代入
let a = -1; // 変数 a を -1 で初期化して宣言 console.log(a); // 変数 a の値は初期値の -1 a = 15; // 変数 a に 0 を代入 console.log(a); // 変数 a の値が -1から 0 に変更 let b = a; // 変数 b を a で初期化して宣言 console.log(b); // 変数 b の値は 15 a = "abc"; // 変数 a に 0 を代入 console.log(a); // 変数 a の値が 0 から "abc" に変更 console.log(b); // 変数 b の値は 15 のまま b = a = 43; // 変数 a に 43 を代入し、その値を変数 b に代入 console.log(b); // 変数 b の値は 15 のまま console.log(a); // 変数 a の値が 0 から "abc" に変更
- 実行結果
-1 15 15 abc 15 43 43
- 変数を初期値を明示して宣言すると、変数の値は初期値になります
- 初期値には変数も使えます
- 代入は演算子で右側から評価されます
b = a = 43
はb = (a = 43)
に同じ
構文
[編集]ここで出てきた構文は、3つです。
- letによる変数宣言(初期化なし)
let 変数名
- letによる変数宣言(初期化あり)
let 変数名 = 初期値
- 代入(代入演算子による代入式)
変数名 = 値
初期化と代入は、両方とも =
(等号)を使うので混同しがちですが、
- ❌:
let a = b = 100
- ⭕:
a = b = 100
と重複した初期化は出来ませんが、重複した代入は可能です。
- 様々な初期化と代入
// 2つ以上の宣言を , で区切って一度に出来ます let a, b console.log(a, b) // 初期化は、この様に書きます let x = 3, y = 4 console.log(x, y) // 分割代入風の構文もあります let [i, j] = [ 123, 567 ] console.log(i, j) let p = [1, 2, 3, 4, 5]; let [q, r] = p; console.log(p, q, r)
- 実行結果
undefined undefined 3 4 123 567 [ 1, 2, 3, 4, 5 ] 1 2
let
[編集]let
キーワードで宣言された変数はブロックスコープをもちます。{ }
の中がブロックで、ブロックを出るとlet
で宣言した変数は参照できなくなります。
for文中の式は単文で{ }
は使われていない場合もfor
文のブロックスコープを持ちます。
for
文の条件式でもlet
で宣言された変数はfor
文を出ると参照できなくなります。
'use strict';
for (let i = 0; i < 10; i++) // 波括弧{}でなくてもfor文はブロックスコープ
console.log(i);
console.log(i); // ReferenceError: i is not defined
let
は、ひとつのスコープ内での再宣言は出来ません。
// 二行目でエラーになります
let x = 1; // これは代入ではなく初期化を伴うlet変数宣言
let x = 2; // 再宣言してるので SyntaxError: ここでスクリプトは中止
// x + 3 を計算
console.log(x + 3); // 既にエラーなので「5」は表示されない
let
宣言された変数のインスタンスとの束縛を変えるには、let
変数宣言ではなく代入を行います。
strict モードでも代入が可能です。
つまり、下記のコードは表示が可能です。
'use strict';
let x = 1; // 初期化を伴う let 変数宣言
x = 2; // 代入
// x + 3 を計算
console.log(x + 3); // 5 を表示
当然、(非strictモードという意味の)通常モードでも代入が可能です。
// このコードは可能
let x = 1;
x = 2; // 代入
// x + 3 を計算
console.log(x + 3); // 5 を表示
const
[編集]const
キーワードで変数を宣言すると、その値は初期化の時以外は書き換えられず「定数」として振る舞います。
const E; // SyntaxError: Missing initializer in const declaration
const
で宣言された変数(定数)のスコープは let
で宣言された変数と同じブロックスコープを取ります。
const
で宣言された変数とlet
で宣言された変数との違い
const
で宣言された変数は、初期化が必須const
で宣言された変数は、代入式の左辺に出来ない
const E = 3; console.log(E); // 3 E = 2.7; // TypeError: Assignment to constant variable.
- const の定数的でない例
const obj = { a: 1, b: 2}; for (const prop in obj) console.log(prop); /* a b */
の様に、一度だけしか代入されない変数や for-in文 や for-of文 のループ変数も const で宣言することができ、値がブロックのかなで不変であったり左辺値化ができないことを明示することができます。「定数」と
constと宣言された定数への代入は SyntaxError を throw します。
const x = 12; const y = x * 2; // 初期化には既知の値のみで組み立てられた式が使える x = 1; // SyntaxError: Identifier 'x' has already been declared
const
は定数を宣言しますが、Arrayオブジェクトや Objectオブジェクトのような複合的なオブジェクト(コレクション)の要素の変更は妨げません。
const ary1 = [1,2,3]; const ary2 = ary1; ary2[1] = "abc"; console.log(ary1); // 1,abc,3
- 変数 ary1 を配列インスタンスで初期化
- 変数 ary2 を ary1 で初期化
- 定数な筈の ary2 の要素を書換え(ary2 が参照するインスタンスの要素を書換え)。
- 書き換えたのは ary2 の1番めの要素なのに、ary1 の要素が書き換わっています。これは、2行目でary2 をary1 で初期化しているためで、この時 ary2 は ary1 の別名になっています。
「定数」を「変数」の特殊なものに分類するのは、数学的には違和感を感じる人もいるかもしれないがプログラミングでは便宜上このように分類します。
プログラミングでいう「変数」とは、オブジェクトにつけた名前で、数学の変動する代数とは異なることに気をつけて下さい。
変数名
[編集]JavaScriptの変数名は、関数・プロパティ・ラベルなどとおなじく識別子(しきべつし;Identifier)の規約に従います。 JavaScriptの識別子は、Unicode の文字、$、_、数字(0-9)からなります。識別子は大文字小文字を区別します。 識別子は数字(0-9)からはじまってはいけません。 またキーワード(keyword)を識別子に使うことは出来ません。 変数名も関数名もプロパティなので重複は許されませんが、ラベル名との重複は許されます。
const primeNumber = 57; // OK const prime_number = 57; // OK const prime-number = 57; // SyntaxError、ハイフンは変数名に使えない
外部に公開しない変数は、var f = true; let i = 0; const E = 2.73;
のようにあえて短めにし、一時的な変数であることを伝えようとする傾向がありますが、コーディング規約で定めていない限りリラックスした名付けが行われます。
ただし、日本語の単語をローマ字表記した変数名はスオsh府出来ません。
日本語には、単数複数の区別がないので for (const item in items) {}
の様な名付けがつかえません。
また、月のつもりで tuki と綴ったら ReferenceError: tsuki is not defined と翻字のゆらぎでミスタイプとなる可能性を増やすので推奨しません。
JavaScriptの場合、未宣言の変数への代入はstrictモードでなければエラーにはならず、発見困難なバグの原因となるミススペルを出さない事は特に重要です。
さらに、この例では、tukiがmoonなのかmonthなのかにも曖昧さが生じ、いっそ var 月
の方が良いのかもとさえ思えます。
Unicodeの文字が有効なので
var π = 4 * Math.atan(1);
は正しいプログラムです。
Unicodeの文字の中に「いわゆる全角スペース」は含まれません。
var
[編集]JavaScript の変数はES2015より前は var
しか存在しませんでした。
var で宣言した変数は、後述する巻き上げなどのややトリッキーな挙動を示すのでES2015で const と let が導入されました。
特別な理由があるとき以外は、 const あるいは let で宣言してください。
- var を使ったコード例
var x = 2; console.log(x + 3); // x + 3 を評価した結果の 5 を表示 x = "abc"; console.log(x + 3); // x + 3 を評価した結果の abc3 を表示
未宣言のグローバル変数
[編集]宣言を行う前に変数に代入することはできます。
x = 2; console.log(x + 3); // x + 3 を評価した結果の 5 を表示
- 宣言を行っていない変数に値を代入することも可能で未宣言のグローバル変数を生成します。
ただし、未宣言のグローバル変数の生成はstrictモードでは SyntaxError になることからも判る通り非推奨です。
未宣言のグローバル変数の挙動は予測困難で難解なバグの原因になりえます。
このため、未宣言のグローバル変数はつくらないよう必ず const
, let
または var
で宣言しましょう。
- ミススペルの産物
- プログラマが意図的に未宣言のグローバル変数を作ることはホボあり得えませんが、スペルミスが未宣言のグローバル変数 を作ってしまう主な原因です。
var hensu; /* * 少し長めの行数 * */ hensuu = "abc"; // ミススペルで出来てしまった「未宣言のグローバル変数」
- ローマ字(日本語のラテン文字翻字)には、何種類も流儀があるので識別子をローマ字表記するのは、少なくとも動的言語には向きません(危険です)
- 代入をする時、それまでの値の型と違う型であっても構いません。
var x = 8; console.log(x); // 8 を表示 x = "abc"; // 違う型のオブジェクトを代入 console.log(x); // abc を表示
- JavaScriptは動的な言語なので「変数はオブジェクトに名前を提供し参照の手助け」をしていることを端的に表しています。
変数のスコープ
[編集]変数が参照可能な範囲を変数のスコープといいます。
関数スコープ
[編集]var
で宣言された変数はその関数外では参照ができなくなります。
この様なスコープを関数スコープといいます。
まず、関数の場合のスコープのコード例を示します。
- 関数スコープ
var x = 1; (function() { var x = "a"; console.log(`local x = ${x}`); })(); console.log(`global x = ${x}`);
- 実行結果
local x = a global x = 1
上記のxは関数の中と外では異なる変数になるので、外側のxの値は冒頭のvar x = 1;
のままです。
var
で宣言された変数は関数スコープを持ちますがif
文のブロックのなかも同じ関数なので、下記コードのxはブロックの中と外で同じ変数を指すので、xの値が変わります。
- varで宣言した変数とブロック
var x = 1; if (x === 1) { var x = "a"; console.log(`if block x = ${x}`); } console.log(`global x = ${x}`);
- 実行結果
if block x = a global x = a
これは次のような場合に問題となります。 1 秒間隔で 0, 1, 2 と出力するプログラムを書く場合、下記のようにコードを書いても、「333」となってしまい、失敗します。
- 1 秒間隔で 0, 1, 2 と出力したい
var start = Date.now() for (var i = 0; i < 3; i++) { setTimeout(function() { console.log(`${Date.now() - start} ms: i = ${i}`); }, 1000); }
- 実行結果
1002 ms: i = 3 1007 ms: i = 3 1007 ms: i = 3
- とほぼ同時に表示され i はすべて(ループを抜けた後の値)3 です。
ブロックスコープ
[編集]let で宣言された変数はブロックスコープになります。
- letで宣言した変数とブロック
let x = 1; if (x === 1) { let x = "a"; console.log(`if block x = ${x}`); } console.log(`global x = ${x}`);
- 実行結果
if block x = a global x = 1
ブロックスコープ(2)
[編集]- 1 秒間隔で 0, 1, 2 と出力したい(2)
var start = Date.now() for (let i = 0; i < 3; i++) { setTimeout(function() { console.log(`${Date.now() - start} ms: i = ${i}`); }, 1000); }
- 実行結果
1001 ms: i = 0 1006 ms: i = 1 1006 ms: i = 2
クロージャ
[編集]クロージャとは、関数を束ねて(囲んで)、その周囲の状態(字句環境)を参照する組み合わせのことです。言い換えると、クロージャを使うと、内側の関数から外側の関数のスコープにアクセスできるようになります。JavaScriptでは、クロージャは関数が作成されるたびに、関数作成時に作成されます。
前節の1 秒間隔で 0, 1, 2 と出力したいは、
- 1 秒間隔で 0, 1, 2 と出力したい(改)
var start = Date.now() for (var i = 0; i < 3; i++) { setTimeout((function(x) { return function() { console.log(Date.now() - start,"ms: ", x); } })(i), 1000); }
- 実行結果
1001 ms: 0 1007 ms: 1 1007 ms: 2
- この様に、クロージャのレキシカルスコープを利用すれば、コールバックにパラメータを安定して渡すことができます。
- しかし、このコードも1秒ほど経過した後3行同時に表示する動きは改善されません。
コールバック地獄版
[編集]- 1 秒間隔で 0, 1, 2 と出力したい(改:コールバック地獄版)
var start = Date.now() setTimeout(function () { console.log(Date.now() - start, "ms: Task-1"); setTimeout(function () { console.log(Date.now() - start, "ms: Task-2"); setTimeout(function () { console.log(Date.now() - start, "ms: Task-3"); }, 500) }, 500) }, 500)
- 実行結果
501 ms: Task-1 1007 ms: Task-2 1508 ms: Task-3
- これで当初の「1 秒間隔で 0, 1, 2 と出力するプログラム」は達成できました。
Promiseを使った実装
[編集]Promiseを使うと、非同期処理のコールバック地獄から解放されます。
- Promiseを使った実装
function sleep(ms) { return new Promise((resolve, reject) => { setTimeout(() => { resolve(ms) }, ms) }) } var start = Date.now() for (let i = 0; i < 3; i++) { sleep(500 * i) .then(() => console.log(Date.now() - start, "ms: ", i) ) } console.log("after for loop.")
- 実行結果
after for loop. 5 ms: 0 501 ms: 1 1001 ms: 2
- Promiseオブジェクトを返す sleep() を定義しています。
- sleepを引数で与えられたミリ秒後に、Callable オブジェクト resolve を実行するものです。
- "after for loop." が真っ先に実行されているように、Promiseオブジェクトは平行実行されます。
Promiseを使った実装(rejectを伴う例)
[編集]Promiseは、実行が成功したケースの他、何らかのエラーが起こり失敗したケースのハンドリングンも可能です。
- Promiseを使った実装(rejectを伴う例)
function sleep(ms, data) { return new Promise((resolve, reject) => { setTimeout(() => { if (Math.random()*4.0|0>1) resolve(ms, data) else reject(ms, data) }, ms) }) } var start = Date.now() for (let i = 0; i < 10; i++) { sleep(50 * i, i) .then((ms, data) => console.log(Date.now() - start, "ms: then()", i)) .catch((ms, data) => console.log(Date.now() - start, "ms: catch()", i)) }
- 実行結果
2 ms: catch() 0 51 ms: then() 1 101 ms: then() 2 151 ms: then() 3 201 ms: then() 4 251 ms: then() 5 301 ms: catch() 6 351 ms: then() 7 401 ms: catch() 8 451 ms: catch() 9
- 乱数でresolveとrejectを振り分けています。
ブロックスコープ
[編集]ECMA2015でconst
キーワードとlet
キーワードが導入されました。
{ }
の中がブロックで、ブロックの中でlet
で宣言した変数はブロックを出ると参照できなくなります。
この様なスコープのことをブロックスコープと呼びます。
// 1 秒間隔で 0, 1, 2 と出力したい
// for の次が var i = 0 でなく let i = 0 に変わっている
for (let i = 0; i < 3; i++) { // ES2015なのでアロー関数によるラムダ式を使った
setTimeout(() => console.log(i), 1000);
}
結果
0 1 2
なお、ECMAScript 5以前はlet宣言は無かったので、上述の問題を解決するには次のように即時関数(関数を定義すると同時に実行するためのイディオム)を使って関数スコープを作るしかありませんでした。
for (var i = 0; i < 3; i++) (function(i) {
setTimeout(function() {
console.log(i);
}, 1000);
})(i);
varの巻き上げ
[編集]var
で宣言した変数は、未宣言で代入したあと改めて宣言すると、スコープの先頭にさかのぼって宣言されていたと仮定します(そのため宣言どころか代入前の参照が ReferenceError になりません)と言う挙動を示します。
- ウェブブラウザを開く。
- F12ボタンを押す。
- 画面下部などに出てきたウィンドウのタブ欄にあるconsoleタブをクリックして、console入力に切り替えます。
- そこにコンソール用のコードを貼り付けます。
脚註
[編集]- ^ オブジェクトとプリミティブをあわせてインスタンスということがあります。