let
const
const
と不変性const
とループconst
と let
の使い分けglobalThis
[ES2020]const
と let
:一時的なデッドゾーンvar
:巻き上げ(部分的な早期アクティベーション)これらはJavaScriptの主な変数宣言方法です。
ES6以前には、var
もありましたが、いくつかの癖があるため、現代のJavaScriptでは避けるのが最善です。詳しくはSpeaking JavaScriptをご覧ください。
let
let
で宣言された変数は変更可能です。
let i;
= 0;
i = i + 1;
i .equal(i, 1); assert
同時に宣言と代入を行うこともできます。
let i = 0;
const
const
で宣言された変数は不変です。常にすぐに初期化する必要があります。
const i = 0; // must initialize
.throws(
assert=> { i = i + 1 },
()
{name: 'TypeError',
message: 'Assignment to constant variable.',
}; )
const
と不変性JavaScriptでは、const
は、*束縛*(変数名と変数値の関連付け)が不変であることを意味するだけです。値自体は、次の例の obj
のように、変更可能な場合があります。
const obj = { prop: 0 };
// Allowed: changing properties of `obj`
.prop = obj.prop + 1;
obj.equal(obj.prop, 1);
assert
// Not allowed: assigning to `obj`
.throws(
assert=> { obj = {} },
()
{name: 'TypeError',
message: 'Assignment to constant variable.',
}; )
const
とループconst
は、各反復で新しい束縛が作成される for-of
ループで使用できます。
const arr = ['hello', 'world'];
for (const elem of arr) {
console.log(elem);
}// Output:
// 'hello'
// 'world'
ただし、プレーンな for
ループでは、let
を使用する必要があります。
const arr = ['hello', 'world'];
for (let i=0; i<arr.length; i++) {
const elem = arr[i];
console.log(elem);
}
const
と let
の使い分けconst
と let
の使い分けについては、次のルールをお勧めします。
const
は不変の束縛であり、変数がその値を決して変更しないことを示します。こちらを優先してください。let
は変数の値が変化することを示します。const
が使用できない場合にのみ使用してください。 演習:
const
exercises/variables-assignment/const_exrc.mjs
変数の *スコープ* は、プログラム内でアクセスできる領域です。次のコードを考えてみましょう。
// // Scope A. Accessible: x
{ const x = 0;
.equal(x, 0);
assert// Scope B. Accessible: x, y
{ const y = 1;
.equal(x, 0);
assert.equal(y, 1);
assert// Scope C. Accessible: x, y, z
{ const z = 2;
.equal(x, 0);
assert.equal(y, 1);
assert.equal(z, 2);
assert
}
}
}// Outside. Not accessible: x, y, z
.throws(
assert=> console.log(x),
()
{name: 'ReferenceError',
message: 'x is not defined',
}; )
x
の *(直接の)スコープ* です。各変数は、直接のスコープとそのスコープ内にネストされたすべてのスコープでアクセスできます。
const
と let
で宣言された変数は、スコープが常に最も内側のブロックであるため、*ブロックスコープ* と呼ばれます。
同じレベルで同じ変数を2回宣言することはできません。
.throws(
assert=> {
() eval('let x = 1; let x = 2;');
,
}
{name: 'SyntaxError',
message: "Identifier 'x' has already been declared",
; })
なぜ
eval()
を使うのか?
eval()
は、assert.throws()
のコールバックが実行されるまで、解析(したがって SyntaxError
)を遅延させます。これを使用しない場合、このコードが解析された時点で既にエラーが発生し、assert.throws()
は実行されません。
ただし、ブロックをネストし、ブロックの外で使用したのと同じ変数名 x
を使用できます。
const x = 1;
.equal(x, 1);
assert
{const x = 2;
.equal(x, 2);
assert
}.equal(x, 1); assert
ブロック内では、内側の x
がその名前を持つ唯一のアクセス可能な変数です。内側の x
は、外側の x
を *シャドーイング* すると言われます。ブロックを離れると、古い値に再度アクセスできます。
クイズ:基本
クイズアプリ を参照してください。
残りのセクションはすべて高度な内容です。
これら2つの形容詞は、プログラミング言語の現象を説明します。
これらの2つの用語の例を見てみましょう。
変数のスコープは静的な現象です。次のコードを考えてみましょう。
function f() {
const x = 3;
// ···
}
x
は *静的* (または *字句的*)に *スコープ化* されています。つまり、そのスコープは固定されており、実行時に変更されることはありません。
変数のスコープは、静的なツリーを形成します(静的なネストによる)。
関数呼び出しは動的な現象です。次のコードを考えてみましょう。
function g(x) {}
function h(y) {
if (Math.random()) g(y); // (A)
}
A行の関数呼び出しが発生するかどうかは、実行時にのみ決定できます。
関数呼び出しは、動的なツリーを形成します(動的な呼び出しによる)。
JavaScriptの変数のスコープはネストされています。それらはツリーを形成します。
ルートは、*グローバルスコープ* とも呼ばれます。Webブラウザーでは、そのスコープに直接存在する唯一の場所は、スクリプトの最上位レベルです。グローバルスコープの変数は *グローバル変数* と呼ばれ、どこからでもアクセスできます。グローバル変数には2種類あります。
const
、let
、およびクラス宣言を介してのみ作成できます。var
と関数宣言を介して作成されます。globalThis
を介してアクセスできます。これを使用して、グローバルオブジェクト変数を作成、読み取り、削除できます。次のHTMLフラグメントは、globalThis
と2種類のグローバル変数を示しています。
<script>
const declarativeVariable = 'd';
var objectVariable = 'o';
</script>
<script>
// All scripts share the same top-level scope:
console.log(declarativeVariable); // 'd'
console.log(objectVariable); // 'o'
// Not all declarations create properties of the global object:
console.log(globalThis.declarativeVariable); // undefined
console.log(globalThis.objectVariable); // 'o'
</script>
各ECMAScriptモジュールには独自のスコープがあります。したがって、モジュールの最上位レベルに存在する変数はグローバルではありません。図 5 は、さまざまなスコープがどのように関連しているかを示しています。
globalThis
[ES2020]グローバル変数 globalThis
は、グローバルオブジェクトにアクセスするための新しい標準的な方法です。グローバルスコープでは this
と同じ値を持つという事実から、この名前が付けられました。
globalThis
は必ずしもグローバルオブジェクトを直接指すとは限りません
たとえば、ブラウザーでは、間接参照 があります。その間接参照は通常は気づきませんが、存在しており、観測できます。
globalThis
の代替グローバルオブジェクトにアクセスする古い方法は、プラットフォームに依存します。
window
:グローバルオブジェクトを参照する従来の方法です。ただし、Node.jsおよびWebワーカーでは機能しません。self
:Webワーカーおよび一般的なブラウザーで使用できます。ただし、Node.jsではサポートされていません。global
:Node.jsでのみ使用できます。globalThis
のユースケースグローバルオブジェクトは、後方互換性のためにJavaScriptが排除できない誤りであると考えられています。パフォーマンスに悪影響を与え、一般的に混乱を招きます。
ECMAScript 6では、グローバルオブジェクトを回避しやすくするいくつかの機能が導入されました。たとえば、
const
、let
、およびクラス宣言は、グローバルスコープで使用した場合、グローバルオブジェクトプロパティを作成しません。通常、グローバルオブジェクト変数には、globalThis
のプロパティを介してではなく、変数を介してアクセスする方が適切です。前者は、すべてのJavaScriptプラットフォームで常に同じように機能してきました。
Web上のチュートリアルでは、window.globVar
を介してグローバル変数 globVar
にアクセスすることがあります。しかし、接頭辞「window.
」は不要であり、省略することをお勧めします。
window.encodeURIComponent(str); // no
encodeURIComponent(str); // yes
したがって、globalThis
のユースケースは比較的わずかです。たとえば、
これらは宣言の2つの重要な側面です。
表1に、さまざまな宣言がこれらの側面をどのように処理するかをまとめます。
スコープ | アクティベーション | 重複 | グローバルプロパティ | |
---|---|---|---|---|
const |
ブロック | 宣言(TDZ) | ✘ |
✘ |
let |
ブロック | 宣言(TDZ) | ✘ |
✘ |
function |
ブロック(*) | 開始 | ✔ |
✔ |
class |
ブロック | 宣言(TDZ) | ✘ |
✘ |
import |
モジュール | exportと同じ | ✘ |
✘ |
var |
関数 | 開始、部分的に | ✔ |
✔ |
import
については§27.5 “ECMAScriptモジュール”で説明しています。以下のセクションでは、他の構造について詳しく説明します。
const
とlet
:一時的なデッドゾーンJavaScriptの場合、TC39は、宣言の前に直接スコープ内で定数にアクセスした場合に何が起こるかを決定する必要がありました。
{console.log(x); // What happens here?
const x;
}
考えられるいくつかの方法は次のとおりです。
undefined
が返されます。アプローチ1は、このアプローチの先例が言語にないため、却下されました。したがって、JavaScriptプログラマーにとっては直感的ではありません。
アプローチ2は、そうするとx
は定数ではなくなり、宣言の前と後で異なる値を持つことになるため、却下されました。
let
はconst
と同じアプローチ3を使用するため、両方が同様に機能し、それらを簡単に切り替えることができます。
変数のスコープに入ってから宣言を実行するまでの時間は、その変数の一時的なデッドゾーン(TDZ)と呼ばれます。
ReferenceError
が発生します。undefined
のいずれかに設定されます。初期化子がない場合はundefined
になります。次のコードは、一時的なデッドゾーンを示しています。
if (true) { // entering scope of `tmp`, TDZ starts
// `tmp` is uninitialized:
.throws(() => (tmp = 'abc'), ReferenceError);
assert.throws(() => console.log(tmp), ReferenceError);
assert
let tmp; // TDZ ends
.equal(tmp, undefined);
assert }
次の例は、一時的なデッドゾーンが本当に一時的(時間に関連する)であることを示しています。
if (true) { // entering scope of `myVar`, TDZ starts
const func = () => {
console.log(myVar); // executed later
;
}
// We are within the TDZ:
// Accessing `myVar` causes `ReferenceError`
let myVar = 3; // TDZ ends
func(); // OK, called outside TDZ
}
func()
がmyVar
の宣言の前にあり、その変数を使用している場合でも、func()
を呼び出すことができます。ただし、myVar
の一時的なデッドゾーンが終わるまで待つ必要があります。
関数に関する詳細情報
このセクションでは、関数を適切に学習する前に関数を使用しています。うまくいけば、すべて理解できるでしょう。理解できない場合は、§25「呼び出し可能な値」を参照してください。
関数宣言は、スコープ内のどこにあるかに関係なく、そのスコープに入るときに常に実行されます。これにより、関数が宣言される前に関数foo()
を呼び出すことができます。
.equal(foo(), 123); // OK
assertfunction foo() { return 123; }
foo()
の早期アクティベーションは、前のコードが次と同等であることを意味します。
function foo() { return 123; }
.equal(foo(), 123); assert
const
またはlet
を使用して関数を宣言した場合、それは早期にアクティベートされません。次の例では、宣言後にのみbar()
を使用できます。
.throws(
assert=> bar(), // before declaration
() ReferenceError);
const bar = () => { return 123; };
.equal(bar(), 123); // after declaration assert
関数g()
が早期にアクティベートされない場合でも、次のルールに従えば、前の関数f()
(同じスコープ内)から呼び出すことができます。f()
は、g()
の宣言後に呼び出す必要があります。
const f = () => g();
const g = () => 123;
// We call f() after g() was declared:
.equal(f(), 123); assert
モジュールの関数は通常、その本体全体が実行された後に呼び出されます。したがって、モジュールでは、関数の順序を心配する必要はほとんどありません。
最後に、早期アクティベーションが前述のルールを自動的に維持する方法に注目してください。スコープに入るとき、すべての関数宣言は、呼び出しが行われる前に最初に実行されます。
早期アクティベーションに依存して、関数を宣言前に呼び出す場合は、早期にアクティベートされていないデータにアクセスしないように注意する必要があります。
funcDecl();
const MY_STR = 'abc';
function funcDecl() {
.throws(
assert=> MY_STR,
() ReferenceError);
}
MY_STR
の宣言後にfuncDecl()
を呼び出すと、問題は解消されます。
早期アクティベーションには落とし穴があり、それを使用しなくてもその利点のほとんどを得ることができることがわかりました。したがって、早期アクティベーションは避けるのが賢明です。しかし、私はこれについて強くは感じておらず、前述のように、構文が好きなので、関数宣言をよく使用します。
クラス宣言は、いくつかの点で関数宣言に似ていますが、クラス宣言は早期にアクティベートされません。
.throws(
assert=> new MyClass(),
() ReferenceError);
class MyClass {}
.equal(new MyClass() instanceof MyClass, true); assert
それはなぜですか?次のクラス宣言を考えてみましょう。
class MyClass extends Object {}
extends
のオペランドは式です。したがって、次のようなことができます。
const identity = x => x;
class MyClass extends identity(Object) {}
このような式の評価は、それが言及された場所で行う必要があります。そうでないと混乱します。それが、クラス宣言が早期にアクティベートされない理由です。
var
:ホイスティング(部分的な早期アクティベーション)var
は、const
とlet
(現在推奨)よりも古い変数を宣言する方法です。次のvar
宣言を検討してください。
var x = 123;
この宣言には2つの部分があります。
var x
:var
で宣言された変数のスコープは、ほとんどの他の宣言のように最も内側の周囲のブロックではなく、最も内側の周囲の関数です。このような変数は、スコープの開始時にすでにアクティブであり、undefined
で初期化されています。x = 123
:代入は常にその場で実行されます。次のコードは、var
の効果を示しています。
function f() {
// Partial early activation:
.equal(x, undefined);
assertif (true) {
var x = 123;
// The assignment is executed in place:
.equal(x, 123);
assert
}// Scope is function, not block:
.equal(x, 123);
assert }
クロージャを探索する前に、束縛変数と自由変数について学習する必要があります。
スコープごとに、言及される変数のセットがあります。これらの変数の中で、次のものを区別します。
次のコードを検討してください。
function func(x) {
const y = 123;
console.log(z);
}
func()
の本体では、x
とy
は束縛変数です。z
は自由変数です。
では、クロージャとは何ですか?
クロージャとは、関数と、その「出生地」に存在する変数への接続を組み合わせたものです。
この接続を維持することのポイントは何ですか?それは、関数の自由変数の値を提供します。たとえば、次のようになります。
function funcFactory(value) {
return () => {
return value;
;
}
}
const func = funcFactory('abc');
.equal(func(), 'abc'); // (A) assert
funcFactory
は、func
に割り当てられたクロージャを返します。func
は出生地の変数への接続を持っているため、(スコープから「エスケープ」した場合でも)A行で呼び出されたときに自由変数value
にアクセスできます。
JavaScriptのすべての関数はクロージャである
JavaScriptでは、静的スコープがクロージャを介してサポートされています。したがって、すべての関数がクロージャです。
次の関数はインクリメンタ(私が今作った名前)を返します。インクリメンタとは、内部に数値を格納する関数です。呼び出されると、その数値に引数を加算して更新し、新しい値を返します。
function createInc(startValue) {
return (step) => { // (A)
+= step;
startValue return startValue;
;
}
}const inc = createInc(5);
.equal(inc(2), 7); assert
A行で作成された関数が、自由変数startValue
に内部数値を保持していることがわかります。今回は、出生スコープから読み取るだけでなく、関数呼び出しをまたいで変更および持続するデータを保存するために使用します。
ローカル変数を介して、出生スコープにさらに多くのストレージスロットを作成できます。
function createInc(startValue) {
let index = -1;
return (step) => {
+= step;
startValue ++;
indexreturn [index, startValue];
;
}
}const inc = createInc(5);
.deepEqual(inc(2), [0, 7]);
assert.deepEqual(inc(2), [1, 9]);
assert.deepEqual(inc(2), [2, 11]); assert
クロージャは何に役立ちますか?
まず、それらは単に静的スコープの実装です。そのため、コールバックのコンテキストデータを提供します。
関数が関数呼び出しをまたいで持続する状態を格納するために使用することもできます。createInc()
はその例です。
また、(リテラルまたはクラスを介して生成された)オブジェクトにプライベートデータを提供できます。その仕組みの詳細については、Exploring ES6で説明されています。
クイズ:上級者向け
クイズアプリ を参照してください。