JavaScript/変数

出典: フリー教科書『ウィキブックス(Wikibooks)』
< JavaScriptJavaScript/var から転送)
ナビゲーションに移動 検索に移動

変数 (英: variable) は、オブジェクトやプリミティブに名前をつけ、名前での参照を可能にする機能です。

変数の宣言[編集]

「変数」は、オブジェクトに名前をつけて参照の手助けをする機能です。 型や値は変数ではなくオブジェクトとプリミティブが保持しています[1]。 静的な言語に慣れた人は、やや戸惑いを感じるかもしれません。

JavaScriptで変数を宣言するには let, const または var キーワードで宣言します。

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 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(ary); // 1,abc,3
  1. 変数 ary1 に配列インスタンスで初期化
  2. 変数 ary2 に ary1 を初期化したインスタンスで初期化
  3. 定数な筈のaryの要素を書き換え(ary1を初期化したインスタンスの要素の書き換え)。
  4. 書き換えたのは ary2 の1番めの要素なのに、ary1の要素が書き換わっている。これは、2行目でary2をary1で初期化しているためで、この時 ary2 は ary1 の別名になっています。
JavaScriptにおける「定数」とは値の変更を許さない変数のこと

「定数」を「変数」の特殊なものに分類するのは、数学的には違和感を感じる人もいるかもしれないがプログラミングでは便宜上このように分類します。

プログラミングでいう「変数」とは、データに値を名前をつけるものという意味で、数学でいう値の変動する代数とはプログラミング「変数」は意味が異なることに気をつけて下さい。


変数名[編集]

JavaScriptの変数名は、関数・プロパティ・ラベルなどとおなじく識別子(しきべつし;Identifier)の規約に従います。 JavaScriptの識別子は、Unicode の文字、$、_、数字(0-9)からなります。識別子は大文字小文字を区別します。 識別子は数字(0-9)からはじまってはいけません。 また予約語(よやくご;keyword)を識別子に使うことは出来ません。 変数名は関数名との重複は許されませんがラベル名との重複は許されます。

const primeNumber = 57; // OK
const prime_number = 57; // OK
const prime-number = 57; // SyntaxError、ハイフンは変数名に使えない

変数名にはJavaと同様、慣習的にvar variableNamesLikeThisのようなキャメルケースが用いられます。 外部に公開しない変数はあえて短めにし、一時的な変数であることを伝えようとする傾向がありますが、コーディング規約で定めていない限りリラックスした名付けが行われます。 ただし日本語の単語をローマ字表記した変数名は単数複数の区別がないので for (let item in items)の様な名付けが行えなかったり、のつもりで tuki と綴ったら ReferenceError: tsuki is not defined と翻字のゆらぎでミスタイプとなる可能性を増やすので推奨しません(この例では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 x;
console.log(x); // undefined を表示(ReferenceErrorになるのは未宣言の場合で、この場合はならない)
y = 100; // 未宣言変数への代入
console.log(y); // 100 を表示、結果的に代入が宣言のように振る舞う。
z = 200; // グローバル変数への代入の実体は、グローバルオブジェクトのプロパティへの代入
window.z = 200; // これとおなじ

既に var または let で作成された変数の値を書き換えるのならキーワード無しでも可能ですが、その場合は宣言ではなく代入になります。

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 にならない)と言う挙動を示します。

console画面の開き方と使い方
  1. ウェブブラウザを開く。
  2. F12ボタンを押す。
  3. 画面下部などに出てきたウィンドウのタブ欄にあるconsoleタブをクリックして、console入力に切り替える。
  4. そこにコンソール用のコードを貼り付ける。


脚注[編集]

  1. ^ オブジェクトとプリミティブをあわせてインスタンスということがあります。

参考文献[編集]