let
const
let
とconst
によるブロックスコープconst
による不変変数の作成const
は値を不変にしませんconst
var
で宣言された変数のライフサイクルlet
で宣言された変数のライフサイクルtypeof
はReferenceError
をスローしますlet
とconst
for
ループfor-of
ループとfor-in
ループconst
対let
対var
ES6は、変数を宣言する2つの新しい方法を提供します:let
とconst
です。これらは主にES5の変数宣言方法であるvar
に取って代わるものです。
let
let
はvar
と似ていますが、宣言された変数はブロックスコープを持ち、現在のブロック内でのみ存在します。var
は関数スコープです。
以下のコードでは、let
で宣言された変数tmp
は、A行から始まるブロック内でのみ存在することがわかります。
function
order
(
x
,
y
)
{
if
(
x
>
y
)
{
// (A)
let
tmp
=
x
;
x
=
y
;
y
=
tmp
;
}
console
.
log
(
tmp
===
x
);
// ReferenceError: tmp is not defined
return
[
x
,
y
];
}
const
const
はlet
と似ていますが、宣言する変数はすぐに初期化され、その後は値を変更できません。
const
foo
;
// SyntaxError: missing = in const declaration
const
bar
=
123
;
bar
=
456
;
// TypeError: `bar` is read-only
for-of
はループの反復ごとに1つのバインディング(変数の記憶領域)を作成するため、ループ変数をconst
で宣言しても問題ありません。
for
(
const
x
of
[
'a'
,
'b'
])
{
console
.
log
(
x
);
}
// Output:
// a
// b
次の表は、ES6で変数を宣言できる6つの方法の概要を示しています(kangaxによる表を参考にしています)。
ホイスティング | スコープ | グローバルプロパティを作成します | |
---|---|---|---|
var |
宣言 | 関数 | はい |
let |
一時的デッドゾーン | ブロック | いいえ |
const |
一時的デッドゾーン | ブロック | いいえ |
関数 |
完全 | ブロック | はい |
クラス |
いいえ | ブロック | いいえ |
import |
完全 | モジュールグローバル | いいえ |
let
とconst
によるブロックスコープ let
とconst
の両方とも、ブロックスコープを持つ変数を作成します。これらは、それらを囲む最も内側のブロック内でのみ存在します。次のコードは、const
で宣言された変数tmp
がif
文のブロック内でのみ存在することを示しています。
function
func
()
{
if
(
true
)
{
const
tmp
=
123
;
}
console
.
log
(
tmp
);
// ReferenceError: tmp is not defined
}
対照的に、var
で宣言された変数は関数スコープです。
function
func
()
{
if
(
true
)
{
var
tmp
=
123
;
}
console
.
log
(
tmp
);
// 123
}
ブロックスコープは、関数内で変数をシャドウイングできることを意味します。
function
func
()
{
const
foo
=
5
;
if
(
···
)
{
const
foo
=
10
;
// shadows outer `foo`
console
.
log
(
foo
);
// 10
}
console
.
log
(
foo
);
// 5
}
const
による不変変数の作成 let
によって作成された変数は変更可能です。
let
foo
=
'abc'
;
foo
=
'def'
;
console
.
log
(
foo
);
// def
定数、const
によって作成された変数は不変です。異なる値を割り当てることはできません。
const
foo
=
'abc'
;
foo
=
'def'
;
// TypeError
const
は値を不変にしません const
は、変数が常に同じ値を持つことを意味するだけであり、値自体が不変である、または不変になるという意味ではありません。たとえば、obj
は定数ですが、それが指す値は変更可能です。プロパティを追加できます。
const
obj
=
{};
obj
.
prop
=
123
;
console
.
log
(
obj
.
prop
);
// 123
しかし、obj
に異なる値を割り当てることはできません。
obj
=
{};
// TypeError
obj
の値を不変にするには、自分で対処する必要があります。たとえば、凍結することによって。
const
obj
=
Object
.
freeze
({});
obj
.
prop
=
123
;
// TypeError
Object.freeze()
は浅い凍結です Object.freeze()
は浅い凍結であることに注意してください。引数のプロパティのみを凍結し、そのプロパティに格納されているオブジェクトは凍結しません。たとえば、オブジェクトobj
は凍結されています。
>
const
obj
=
Object
.
freeze
(
{
foo
:
{
}
}
);
>
obj
.
bar
=
123
TypeError
:
Can
't add property bar, object is not extensible
> obj.foo = {}
TypeError: Cannot assign to read only property '
foo
'
of
#
<
Object
>
しかし、オブジェクトobj.foo
は凍結されていません。
> obj.foo.qux = 'abc';
> obj.foo.qux
'abc'
const
const
変数が作成された後は変更できません。しかし、それはスコープを再入力して、新しい値で最初からやり直せないという意味ではありません。たとえば、ループを使用すると。
function
logArgs
(...
args
)
{
for
(
const
[
index
,
elem
]
of
args
.
entries
())
{
// (A)
const
message
=
index
+
'. '
+
elem
;
// (B)
console
.
log
(
message
);
}
}
logArgs
(
'Hello'
,
'everyone'
);
// Output:
// 0. Hello
// 1. everyone
このコードにはA行とB行に2つのconst
宣言があります。そして、各ループの反復で、それらの定数は異なる値を持ちます。
let
またはconst
で宣言された変数には、いわゆる一時的デッドゾーン(TDZ)があります。スコープに入ると、実行が宣言に到達するまでアクセス(取得または設定)できません。var
で宣言された変数(TDZがない)とlet
で宣言された変数(TDZがある)のライフサイクルを比較してみましょう。
var
で宣言された変数のライフサイクル var
変数には一時的デッドゾーンがありません。そのライフサイクルは次の手順で構成されます。
var
変数のスコープ(それを囲む関数)に入ると、そのための記憶領域(バインディング)が作成されます。変数は、undefined
に設定することですぐに初期化されます。undefined
のままです。let
で宣言された変数のライフサイクル let
で宣言された変数には一時的デッドゾーンがあり、そのライフサイクルは次のようになります。
let
変数のスコープ(それを囲むブロック)に入ると、そのための記憶領域(バインディング)が作成されます。変数は初期化されません。ReferenceError
が発生します。undefined
に設定されます。const
変数はlet
変数と同様に動作しますが、イニシャライザ(つまり、すぐに値に設定する)が必要であり、変更できません。
TDZ内では、変数を取得または設定すると例外がスローされます。
let
tmp
=
true
;
if
(
true
)
{
// enter new scope, TDZ starts
// Uninitialized binding for `tmp` is created
console
.
log
(
tmp
);
// ReferenceError
let
tmp
;
// TDZ ends, `tmp` is initialized with `undefined`
console
.
log
(
tmp
);
// undefined
tmp
=
123
;
console
.
log
(
tmp
);
// 123
}
console
.
log
(
tmp
);
// true
イニシャライザがある場合、TDZはイニシャライザが評価され、結果が変数に割り当てられた後に終了します。
let
foo
=
console
.
log
(
foo
);
// ReferenceError
次のコードは、デッドゾーンが実際に時間的(時間に基づく)であり、空間的(位置に基づく)ではないことを示しています。
if
(
true
)
{
// enter new scope, TDZ starts
const
func
=
function
()
{
console
.
log
(
myVar
);
// OK!
};
// Here we are within the TDZ and
// accessing `myVar` would cause a `ReferenceError`
let
myVar
=
3
;
// TDZ ends
func
();
// called outside TDZ
}
typeof
はTDZ内の変数に対してReferenceError
をスローします typeof
を使用して一時的デッドゾーン内の変数にアクセスすると、例外が発生します。
if
(
true
)
{
console
.
log
(
typeof
foo
);
// ReferenceError (TDZ)
console
.
log
(
typeof
aVariableThatDoesntExist
);
// 'undefined'
let
foo
;
}
なぜ?その理由は次のとおりです。foo
は宣言されていませんが、初期化されていません。その存在を認識する必要がありますが、認識していません。したがって、警告されることは望ましいように思われます。
さらに、この種のチェックは、グローバル変数を条件付きで作成する場合にのみ役立ちます。これは、通常のプログラムでは行う必要のないことです。
条件付きで変数を作成する場合、2つのオプションがあります。
オプション1 - typeof
とvar
if
(
typeof
someGlobal
===
'undefined'
)
{
var
someGlobal
=
{
···
};
}
このオプションはグローバルスコープでのみ機能します(したがって、ES6モジュール内では機能しません)。
オプション2 - window
if
(
!
(
'someGlobal'
in
window
))
{
window
.
someGlobal
=
{
···
};
}
const
とlet
に一時的デッドゾーンがある理由はいくつかあります。
const
について: const
を正しく動作させるのは困難です。 Allen Wirfs-Brock氏の言葉 を引用します。「TDZ…はconst
に合理的なセマンティクスを提供します。このトピックについてはかなりの技術的な議論があり、TDZが最良の解決策として浮上しました。」let
も一時的デッドゾーンを持つため、let
とconst
の切り替えによって予期しない方法で動作が変化することはありません。undefined
の場合、その値はそのガードによって保証された値と矛盾する可能性があります。このセクションの情報源
let
とconst
次のループでは、ループの先頭で変数を宣言できます。
for
for-in
for-of
変数を宣言するには、var
、let
、またはconst
のいずれかを使用できます。それぞれに異なる効果があり、次に説明します。
for
ループ for
ループの先頭でvar
を使用して変数を宣言すると、その変数に対して単一のバインディング(記憶領域)が作成されます。
const
arr
=
[];
for
(
var
i
=
0
;
i
<
3
;
i
++
)
{
arr
.
push
(()
=>
i
);
}
arr
.
map
(
x
=>
x
());
// [3,3,3]
3つのアロー関数の本体内のすべてのi
は同じバインディングを参照するため、すべて同じ値を返します。
let
を使用して変数を宣言すると、ループの各反復ごとに新しいバインディングが作成されます。
const
arr
=
[];
for
(
let
i
=
0
;
i
<
3
;
i
++
)
{
arr
.
push
(()
=>
i
);
}
arr
.
map
(
x
=>
x
());
// [0,1,2]
今回は、各i
は特定の反復のバインディングを参照し、その時点で現在の値を保持します。そのため、各アロー関数は異なる値を返します。
const
はvar
のように動作しますが、const
で宣言された変数の初期値を変更することはできません。
// TypeError: Assignment to constant variable
// (due to i++)
for (const i=0; i<3; i++) {
console.log(i);
}
各反復ごとに新しいバインディングを取得するのは最初は奇妙に見えるかもしれませんが、後のセクションで説明するように、ループ変数を参照する関数をループを使用して作成する際に非常に役立ちます。
for-of
ループとfor-in
ループ for-of
ループでは、var
は単一のバインディングを作成します。
const
arr
=
[];
for
(
var
i
of
[
0
,
1
,
2
])
{
arr
.
push
(()
=>
i
);
}
arr
.
map
(
x
=>
x
());
// [2,2,2]
const
は反復ごとに1つの不変のバインディングを作成します。
const
arr
=
[];
for
(
const
i
of
[
0
,
1
,
2
])
{
arr
.
push
(()
=>
i
);
}
arr
.
map
(
x
=>
x
());
// [0,1,2]
let
も反復ごとに1つのバインディングを作成しますが、作成されるバインディングは変更可能です。
for-in
ループはfor-of
ループと同様に動作します。
以下は、3つのリンクを表示するHTMLページです。
<!
doctype
html
>
<
html
>
<
head
>
<
meta
charset
=
"UTF-8"
>
<
/head>
<
body
>
<
div
id
=
"content"
><
/div>
<
script
>
const
entries
=
[
[
'yes'
,
'ja'
],
[
'no'
,
'nein'
],
[
'perhaps'
,
'vielleicht'
],
];
const
content
=
document
.
getElementById
(
'content'
);
for
(
const
[
source
,
target
]
of
entries
)
{
// (A)
content
.
insertAdjacentHTML
(
'beforeend'
,
`<div><a id="
${
source
}
" href="">
${
source
}
</a></div>`
);
document
.
getElementById
(
source
).
addEventListener
(
'click'
,
(
event
)
=>
{
event
.
preventDefault
();
alert
(
target
);
// (B)
});
}
<
/script>
<
/body>
<
/html>
表示される内容は変数target
(B行)によって異なります。A行でconst
ではなくvar
を使用していた場合、ループ全体に対して単一のバインディングが存在し、target
は後で'vielleicht'
という値になります。そのため、どのリンクをクリックしても、常に'vielleicht'
という翻訳が表示されます。
ありがたいことに、const
を使用すると、ループの反復ごとに1つのバインディングが取得され、翻訳が正しく表示されます。
パラメータと同じ名前の変数をlet
で宣言すると、静的(ロード時)エラーが発生します。
function
func
(
arg
)
{
let
arg
;
// static error: duplicate declaration of `arg`
}
ブロック内で同じことを行うと、パラメータがシャドウされます。
function
func
(
arg
)
{
{
let
arg
;
// shadows parameter `arg`
}
}
これとは対照的に、パラメータと同じ名前の変数をvar
で宣言しても何も起こらず、同じスコープ内でvar
変数を再宣言しても何も起こりません。
function
func
(
arg
)
{
var
arg
;
// does nothing
}
function
func
(
arg
)
{
{
// We are still in same `var` scope as `arg`
var
arg
;
// does nothing
}
}
パラメータにデフォルト値がある場合、それらはlet
文のシーケンスとして扱われ、一時的デッドゾーンの影響を受けます。
// OK: `y` accesses `x` after it has been declared
function
foo
(
x
=
1
,
y
=
x
)
{
return
[
x
,
y
];
}
foo
();
// [1,1]
// Exception: `x` tries to access `y` within TDZ
function
bar
(
x
=
y
,
y
=
2
)
{
return
[
x
,
y
];
}
bar
();
// ReferenceError
パラメータのデフォルト値のスコープは本体のスコープとは別に存在します(前者は後者を囲みます)。つまり、パラメータのデフォルト値「内側」で定義されたメソッドまたは関数は、本体のローカル変数を見ません。
const
foo
=
'outer'
;
function
bar
(
func
=
x
=>
foo
)
{
const
foo
=
'inner'
;
console
.
log
(
func
());
// outer
}
bar
();
JavaScriptのグローバルオブジェクト(ウェブブラウザではwindow
、Node.jsではglobal
)は、特にパフォーマンスに関して、機能というよりはバグに近いものです。そのため、ES6が区別を導入するのは理にかなっています。
var
宣言let
宣言const
宣言モジュールの本体はグローバルスコープで実行されず、スクリプトのみが実行されることに注意してください。したがって、さまざまな変数の環境は次のチェーンを形成します。
関数宣言は…
let
のようにブロックスコープです。var
のように(グローバルスコープにある間は)グローバルオブジェクトにプロパティを作成します。次のコードは、関数宣言のホイスティングを示しています。
{
// Enter a new scope
console
.
log
(
foo
());
// OK, due to hoisting
function
foo
()
{
return
'hello'
;
}
}
クラス宣言は…
クラスがホイスティングされないことは驚くべきことかもしれません。なぜなら、内部的には関数を作成するからです。この動作の理由は、それらのextends
句の値が式によって定義され、それらの式は適切なタイミングで実行される必要があるためです。
{
// Enter a new scope
const
identity
=
x
=>
x
;
// Here we are in the temporal dead zone of `MyClass`
const
inst
=
new
MyClass
();
// ReferenceError
// Note the expression in the `extends` clause
class
MyClass
extends
identity
(
Object
)
{
}
}
const
対let
対var
let
またはconst
を常に使用するようにお勧めします。
const
を優先してください。変数が値をまったく変更しない場合はいつでも使用できます。言い換えれば、変数は代入の左辺または++
または--
のオペランドであってはなりません。const
変数が参照するオブジェクトを変更することは許可されます。
const
foo
=
{};
foo
.
prop
=
123
;
// OK
for-of
ループでもconst
を使用できます。ループの反復ごとに1つの(不変の)バインディングが作成されるためです。
for
(
const
x
of
[
'a'
,
'b'
])
{
console
.
log
(
x
);
}
// Output:
// a
// b
for-of
ループの本体内では、x
を変更できません。
let
を使用します。
let
counter
=
0
;
// initial value
counter
++
;
// change
let
obj
=
{};
// initial value
obj
=
{
foo
:
123
};
// change
var
は避けてください。これらのルールに従うと、var
はレガシーコードにのみ表示され、注意深いリファクタリングが必要であることを示すシグナルとなります。
var
はlet
とconst
にはない1つのことを行います。これを使用して宣言された変数はグローバルオブジェクトのプロパティになります。ただし、これは一般的に良いことではありません。window
(ブラウザ)またはglobal
(Node.js)に代入することで、同じ効果を得ることができます。
前述のスタイルルールに対する代替手段として、完全に不変なもの(プリミティブ値と凍結オブジェクト)に対してのみconst
を使用するという方法があります。すると、2つのアプローチがあります。
const
を優先する: const
は不変のバインディングを示します。let
を優先する: const
は不変の値を示します。1を推奨しますが、2も可。