letconstconst と不変性const とループconst と let の使い分けglobalThis [ES2020]const と let:一時的なデッドゾーンvar:巻き上げ(部分的な早期アクティベーション)これらはJavaScriptの主な変数宣言方法です。
ES6以前には、varもありましたが、いくつかの癖があるため、現代のJavaScriptでは避けるのが最善です。詳しくはSpeaking JavaScriptをご覧ください。
letlet で宣言された変数は変更可能です。
let i;
i = 0;
i = i + 1;
assert.equal(i, 1);同時に宣言と代入を行うこともできます。
let i = 0;constconst で宣言された変数は不変です。常にすぐに初期化する必要があります。
const i = 0; // must initialize
assert.throws(
() => { i = i + 1 },
{
name: 'TypeError',
message: 'Assignment to constant variable.',
}
);const と不変性JavaScriptでは、const は、*束縛*(変数名と変数値の関連付け)が不変であることを意味するだけです。値自体は、次の例の obj のように、変更可能な場合があります。
const obj = { prop: 0 };
// Allowed: changing properties of `obj`
obj.prop = obj.prop + 1;
assert.equal(obj.prop, 1);
// Not allowed: assigning to `obj`
assert.throws(
() => { 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;
assert.equal(x, 0);
{ // Scope B. Accessible: x, y
const y = 1;
assert.equal(x, 0);
assert.equal(y, 1);
{ // Scope C. Accessible: x, y, z
const z = 2;
assert.equal(x, 0);
assert.equal(y, 1);
assert.equal(z, 2);
}
}
}
// Outside. Not accessible: x, y, z
assert.throws(
() => console.log(x),
{
name: 'ReferenceError',
message: 'x is not defined',
}
);x の *(直接の)スコープ* です。各変数は、直接のスコープとそのスコープ内にネストされたすべてのスコープでアクセスできます。
const と let で宣言された変数は、スコープが常に最も内側のブロックであるため、*ブロックスコープ* と呼ばれます。
同じレベルで同じ変数を2回宣言することはできません。
assert.throws(
() => {
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;
assert.equal(x, 1);
{
const x = 2;
assert.equal(x, 2);
}
assert.equal(x, 1);ブロック内では、内側の 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:
assert.throws(() => (tmp = 'abc'), ReferenceError);
assert.throws(() => console.log(tmp), ReferenceError);
let tmp; // TDZ ends
assert.equal(tmp, undefined);
}次の例は、一時的なデッドゾーンが本当に一時的(時間に関連する)であることを示しています。
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()を呼び出すことができます。
assert.equal(foo(), 123); // OK
function foo() { return 123; }foo()の早期アクティベーションは、前のコードが次と同等であることを意味します。
function foo() { return 123; }
assert.equal(foo(), 123);constまたはletを使用して関数を宣言した場合、それは早期にアクティベートされません。次の例では、宣言後にのみbar()を使用できます。
assert.throws(
() => bar(), // before declaration
ReferenceError);
const bar = () => { return 123; };
assert.equal(bar(), 123); // after declaration 関数g()が早期にアクティベートされない場合でも、次のルールに従えば、前の関数f()(同じスコープ内)から呼び出すことができます。f()は、g()の宣言後に呼び出す必要があります。
const f = () => g();
const g = () => 123;
// We call f() after g() was declared:
assert.equal(f(), 123);モジュールの関数は通常、その本体全体が実行された後に呼び出されます。したがって、モジュールでは、関数の順序を心配する必要はほとんどありません。
最後に、早期アクティベーションが前述のルールを自動的に維持する方法に注目してください。スコープに入るとき、すべての関数宣言は、呼び出しが行われる前に最初に実行されます。
早期アクティベーションに依存して、関数を宣言前に呼び出す場合は、早期にアクティベートされていないデータにアクセスしないように注意する必要があります。
funcDecl();
const MY_STR = 'abc';
function funcDecl() {
assert.throws(
() => MY_STR,
ReferenceError);
}MY_STRの宣言後にfuncDecl()を呼び出すと、問題は解消されます。
早期アクティベーションには落とし穴があり、それを使用しなくてもその利点のほとんどを得ることができることがわかりました。したがって、早期アクティベーションは避けるのが賢明です。しかし、私はこれについて強くは感じておらず、前述のように、構文が好きなので、関数宣言をよく使用します。
クラス宣言は、いくつかの点で関数宣言に似ていますが、クラス宣言は早期にアクティベートされません。
assert.throws(
() => new MyClass(),
ReferenceError);
class MyClass {}
assert.equal(new MyClass() instanceof MyClass, true);それはなぜですか?次のクラス宣言を考えてみましょう。
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:
assert.equal(x, undefined);
if (true) {
var x = 123;
// The assignment is executed in place:
assert.equal(x, 123);
}
// Scope is function, not block:
assert.equal(x, 123);
}クロージャを探索する前に、束縛変数と自由変数について学習する必要があります。
スコープごとに、言及される変数のセットがあります。これらの変数の中で、次のものを区別します。
次のコードを検討してください。
function func(x) {
const y = 123;
console.log(z);
}func()の本体では、xとyは束縛変数です。zは自由変数です。
では、クロージャとは何ですか?
クロージャとは、関数と、その「出生地」に存在する変数への接続を組み合わせたものです。
この接続を維持することのポイントは何ですか?それは、関数の自由変数の値を提供します。たとえば、次のようになります。
function funcFactory(value) {
return () => {
return value;
};
}
const func = funcFactory('abc');
assert.equal(func(), 'abc'); // (A)funcFactoryは、funcに割り当てられたクロージャを返します。funcは出生地の変数への接続を持っているため、(スコープから「エスケープ」した場合でも)A行で呼び出されたときに自由変数valueにアクセスできます。
JavaScriptのすべての関数はクロージャである
JavaScriptでは、静的スコープがクロージャを介してサポートされています。したがって、すべての関数がクロージャです。
次の関数はインクリメンタ(私が今作った名前)を返します。インクリメンタとは、内部に数値を格納する関数です。呼び出されると、その数値に引数を加算して更新し、新しい値を返します。
function createInc(startValue) {
return (step) => { // (A)
startValue += step;
return startValue;
};
}
const inc = createInc(5);
assert.equal(inc(2), 7);A行で作成された関数が、自由変数startValueに内部数値を保持していることがわかります。今回は、出生スコープから読み取るだけでなく、関数呼び出しをまたいで変更および持続するデータを保存するために使用します。
ローカル変数を介して、出生スコープにさらに多くのストレージスロットを作成できます。
function createInc(startValue) {
let index = -1;
return (step) => {
startValue += step;
index++;
return [index, startValue];
};
}
const inc = createInc(5);
assert.deepEqual(inc(2), [0, 7]);
assert.deepEqual(inc(2), [1, 9]);
assert.deepEqual(inc(2), [2, 11]);クロージャは何に役立ちますか?
まず、それらは単に静的スコープの実装です。そのため、コールバックのコンテキストデータを提供します。
関数が関数呼び出しをまたいで持続する状態を格納するために使用することもできます。createInc()はその例です。
また、(リテラルまたはクラスを介して生成された)オブジェクトにプライベートデータを提供できます。その仕組みの詳細については、Exploring ES6で説明されています。
クイズ:上級者向け
クイズアプリ を参照してください。