letconstletとconstによるブロックスコープconstによる不変変数の作成constは値を不変にしませんconstvarで宣言された変数のライフサイクルletで宣言された変数のライフサイクルtypeofはReferenceErrorをスローしますletとconstforループfor-ofループとfor-inループconst対let対varES6は、変数を宣言する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 次のループでは、ループの先頭で変数を宣言できます。
forfor-infor-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も可。