letコマンドとconstコマンド

let コマンド

基本的な使い方

ES6 では、変数を宣言するための新しい let コマンドが追加されています。その使用法は var と似ていますが、宣言された変数は let コマンドが配置されているコード ブロック内でのみ有効です。

{
  a = 10 とします。
  変数 b = 1;
}

a // 参照エラー: a が定義されていません。
b // 1

上記のコードは、コード ブロック内でそれぞれ letvar を使用して 2 つの変数を宣言します。次に、これら 2 つの変数はコード ブロックの外で呼び出されます。その結果、「let」で宣言された変数はエラーを報告し、「var」で宣言された変数は正しい値を返します。これは、「let」で宣言された変数が、それが配置されているコード ブロック内でのみ有効であることを示しています。

for ループのカウンタは、let コマンドの使用に非常に適しています。

for (i = 0; i < 10; i++) {
  // ...
}

コンソール.ログ(i);
// 参照エラー: i が定義されていません

上記のコードでは、カウンター「i」は「for」ループの本体内でのみ有効であり、ループの外で参照されるとエラーが報告されます。

次のコードで var を使用すると、最終出力は 10 になります。

var a = [];
for (var i = 0; i < 10; i++) {
  a[i] = 関数 () {
    コンソール.ログ(i);
  };
}
a[6](); // 10

上記のコードでは、変数 ivar コマンドによって宣言されており、グローバル スコープで有効であるため、変数 i はグローバルに 1 つだけ存在します。ループするたびに変数 i の値が変化し、関数内の console.log(i) はループ内の配列 a に割り当てられ、内部の i はグローバル i を指します。つまり、配列 'a' のメンバー内のすべての 'i' は同じ 'i' を指し、実行時に 'i' 値の最後のラウンドである 10 が出力されます。

「let」を使用した場合、宣言された変数はブロックレベルのスコープ内でのみ有効で、最終出力は 6 になります。

var a = [];
for (i = 0; i < 10; i++) {
  a[i] = 関数 () {
    コンソール.ログ(i);
  };
}
a[6](); // 6

上記のコードでは、変数 ilet によって宣言されています。現在の i はこのサイクルでのみ有効であるため、各サイクルの i は実際には新しい変数であるため、最終出力は 6 になります。 。変数「i」がループの各サイクルで再宣言される場合、どのようにして前のサイクルの値を知り、このサイクルの値を計算するのかと疑問に思うかもしれません。これは、JavaScript エンジンが内部的に前のループの値を記憶しており、このラウンドの変数 i を初期化するときに、前のループに基づいて計算が実行されるためです。

また、for ループにはもう 1 つの特殊な機能があります。それは、ループ変数を設定する部分が親スコープであり、ループ本体の内部が別の子スコープであることです。

for (i = 0; i < 3; i++) {
  私 = 'abc' とします。
  コンソール.ログ(i);
}
//ABC
//ABC
//ABC

上記のコードは正しく実行され、「abc」が 3 回出力されます。これは、関数内の変数 i とループ変数 i が同じスコープ内になく、別のスコープを持つことを示します (同じスコープ内で let を使用して同じ変数を繰り返し宣言することはできません)。

変動プロモーションはありません

var コマンドは、「変数プロモーション」現象を引き起こします。つまり、変数は宣言される前に使用でき、値は 未定義 になります。この現象は少し奇妙です。一般的な論理によれば、変数は宣言文の後に使用する必要があります。

この現象を修正するには、let コマンドの文法的な動作を変更します。宣言した変数は宣言後に使用する必要があります。変更しないとエラーが報告されます。

//var の場合
console.log(foo); // 出力は未定義です
var foo = 2;

// letの場合
console.log(bar); // エラーを報告する ReferenceError
バー = 2 とします。

上記のコードでは、変数 foovar コマンドで宣言されており、変数の昇格が行われます。つまり、スクリプトの実行が開始されると、変数 foo はすでに存在しますが、値が存在しないため、「未定義」になります。 と出力されます。変数 barletコマンドで宣言されており、変数の昇格は行われません。これは、変数bar` が宣言される前には存在しないことを意味し、それが使用されるとエラーがスローされます。

一時的なデッドゾーン

「let」コマンドがブロックレベルのスコープに存在する限り、それによって宣言された変数はこの領域に「バインド」され、外部の影響を受けなくなります。

var tmp = 123;

if (true) {
  tmp = 'abc' // 参照エラー;
  tmp にしましょう。
}

上記のコードでは、グローバル変数 tmp がありますが、 let はブロックレベルのスコープでローカル変数 tmp を宣言しており、後者がこのブロックレベルのスコープをバインドすることになるため、 let が宣言する前に、変数への tmp 割り当てはエラーを報告します。

ES6 では、ブロック内に「let」コマンドと「const」コマンドがある場合、このブロック内でこれらのコマンドによって宣言された変数は最初から閉じられたスコープを形成することを明確に規定しています。宣言前にこれらの変数を使用すると、エラーが発生します。

つまり、コード ブロック内では、「let」コマンドを使用して宣言されるまで変数は使用できません。文法的には、これは「一時的なデッド ゾーン」(TDZ) と呼ばれます。

if (true) {
  //TDZ開始
  tmp = 'abc' // 参照エラー;
  console.log(tmp); // 参照エラー

  let tmp; // TDZ が終了します
  console.log(tmp); // 未定義

  tmp = 123;
  コンソール.log(tmp); // 123
}

上記のコードでは、let コマンドが変数 tmp を宣言する前に、変数 tmp の「デッド ゾーン」に属しています。

「一時的なデッドゾーン」は、typeof が 100% 安全な操作ではなくなったことも意味します。

typeof x; // 参照エラー
x にしましょう。

上記のコードでは、変数「x」は「let」コマンドを使用して宣言されているため、宣言される前は「x」の「デッドゾーン」に属しており、変数が使用されている限りエラーが発生します。報告した。したがって、typeof は実行時に ReferenceError をスローします。

比較のために、変数がまったく宣言されていない場合、typeof を使用してもエラーは発生しません。

typeof undeclared_variable // "未定義"

上記のコードでは、undeclared_variable は存在しない変数名であり、結果は「未定義」になります。したがって、「let」が存在する前は、「typeof」演算子は 100% 安全であり、エラーを報告することはありませんでした。これはもはや真実ではありません。この設計は、誰もが良いプログラミング習慣を身につけられるようにするためのものです。変数は宣言後に使用する必要があります。そうしないと、エラーが報告されます。

一部の「デッド ゾーン」は隠されており、見つけるのが簡単ではありません。

関数バー(x = y, y = 2) {
  [x, y] を返します。
}

bar(); // エラーを報告します

上記のコードで、bar 関数の呼び出し時にエラーが報告される理由は (一部の実装ではエラーが報告されない場合があります)、パラメータ x のデフォルト値が別のパラメータ y と等しいためです。 「現時点では宣言されていないが、それは「死んだ」地区である」。 y のデフォルト値が x である場合、この時点で x が宣言されているため、エラーは報告されません。

関数バー(x = 2, y = x) {
  [x, y] を返します。
}
bar(); // [2, 2]

さらに、次のコードでもエラーが報告されますが、これは var の動作とは異なります。

// エラーを報告しません
var x = x;

// エラーを報告する
x = x とします。
// 参照エラー: x が定義されていません

上記のコードによって報告されるエラーも、一時的なデッド ゾーンが原因で発生します。 let を使用して変数を宣言する場合、宣言が完了する前に変数が使用されている限り、エラーが報告されます。上の行は、変数 x の宣言ステートメントが実行される前に、x の値が取得され、「x が定義されていません」というエラーが発生します。

ES6 では、一時的なデッド ゾーンや「let」および「const」ステートメントでは変数のプロモーションが発生しないことを規定しています。これは主に実行時エラーを減らし、変数が宣言される前に使用されて予期しない動作が発生するのを防ぐためです。このような間違いは ES5 ではよくあることですが、この規定が整備されたことで、それらを避けるのは簡単です。

つまり、一時的なデッド ゾーンの本質は、変数が現在のスコープに入るとすぐに、使用される変数はすでに存在しますが、その変数はコード行までしか取得および使用できないということです。変数を宣言するものが表示されます。

重複した宣言は許可されません

「let」では、同じスコープ内で同じ変数を繰り返し宣言することはできません。

// エラーを報告する
関数 func() {
  a = 10 とします。
  変数 a = 1;
}

// エラーを報告する
関数 func() {
  a = 10 とします。
  a = 1 とします。
}

したがって、関数内でパラメータを再宣言することはできません。

関数 func(arg) {
  引数を付けてみましょう。
}
func() // エラーを報告する

関数 func(arg) {
  {
    引数を付けてみましょう。
  }
}
func() // エラーは報告されませんでした

ブロックレベルのスコープ

ブロックレベルのスコープが必要なのはなぜですか?

ES5 にはグローバル スコープと関数スコープのみがあり、ブロックレベルのスコープがないため、多くの無理なシナリオが発生します。

最初のシナリオでは、内部変数が外部変数を上書きする可能性があります。

var tmp = 新しい日付();

関数 f() {
  コンソール.ログ(tmp);
  if (偽) {
    var tmp = 'ハローワールド';
  }
}

f(); // 未定義

上記のコードの本来の目的は、外側の tmp 変数が if コード ブロックの外側で使用され、内側の tmp 変数が内部で使用されることです。ただし、関数 f の実行後、出力結果は 未定義 になります。これは、変数がプロモートされ、内部の tmp 変数が外部の tmp 変数を上書きするためです。

2 番目のシナリオでは、カウントに使用されるループ変数がグローバル変数として漏洩します。

vars = 'こんにちは';

for (var i = 0; i < s.length; i++) {
  console.log(s[i]);
}

コンソール.log(i); // 5

上記のコードでは、変数 i はループの制御にのみ使用されていますが、ループ終了後も変数 i は消えることなく、グローバル変数にリークされます。

ES6 のブロックレベルのスコープ

「let」は実際にブロックレベルのスコープを JavaScript に追加します。

関数 f1() {
  n = 5 とします。
  if (true) {
    n = 10 とします。
  }
  コンソール.log(n); // 5
}

上記の関数には 2 つのコード ブロックがあり、どちらも変数 n を宣言し、実行後に 5 を出力します。これは、外側のコード ブロックが内側のコード ブロックの影響を受けないことを意味します。 var を使用して変数 n を 2 回定義すると、最終的な出力値は 10 になります。

ES6 では、ブロックレベルのスコープを任意にネストできます。

{{{{
  {let insane = 'Hello World'}
  console.log(insane); // エラーレポート
}}}};

上記のコードは 5 レベルのブロックレベル スコープを使用しており、各レベルは個別のスコープです。第 4 レベルのスコープは、第 5 レベルのスコープの内部変数を読み取ることができません。

内側のスコープでは、外側のスコープと同じ名前の変数を定義できます。

{{{{
  let insane = 'Hello World';
  {let insane = 'Hello World'}
}}}};

実際、ブロックレベルのスコープの出現により、広く使用されている匿名即時実行関数式 (匿名 IIFE) が不要になりました。

//IIFEの書き方
(関数 () {
  var tmp = ...;
  ...
}());

//ブロックレベルのスコープ記述方法
{
  tmp = ...; とします。
  ...
}

ブロックレベルのスコープと関数の宣言

関数はブロックレベルのスコープで宣言できますか?これはかなりややこしい質問です。

ES5 では、関数はトップレベル スコープと関数スコープでのみ宣言でき、ブロックレベル スコープでは宣言できないと規定されています。

// 状況 1
if (true) {
  関数 f() {}
}

// ケース 2
試す {
  関数 f() {}
} キャッチ(e) {
  // ...
}

ES5 によれば、上記 2 つの関数宣言は不正です。

ただし、ブラウザは古いコードとの互換性を維持するために、この要件に準拠していないため、上記の 2 つの状況はエラーを報告せずに実際に実行できます。

ES6 ではブロック レベルのスコープが導入され、ブロック レベルのスコープ内で関数を明示的に宣言できるようになりました。 ES6 では、ブロックレベルのスコープでは関数宣言ステートメントが「let」のように動作し、ブロックレベルのスコープ外では参照できないと規定しています。

function f() { console.log('私は外にいる!'); }

(関数 () {
  if (偽) {
    // 関数 f の宣言を 1 回繰り返す
    function f() { console.log('私は中にいます!') }
  }

  f();
}());

上記のコードを ES5 で実行すると、if 内で宣言された関数 f が関数先頭に昇格するため、「I am inside!」となります。実際に実行するコードは以下のとおりです。

// ES5環境
function f() { console.log('私は外にいる!'); }

(関数 () {
  function f() { console.log('私は中にいます!') }
  if (偽) {
  }
  f();
}());

ES6 は理論的には完全に異なります。「私は外にいる!」となります。ブロックレベルのスコープで宣言された関数は let に似ているため、スコープの外では効果がありません。ただし、実際に上記のコードを ES6 ブラウザで実行すると、エラーが報告されます。これはなぜですか?

// ブラウザのES6環境
function f() { console.log('私は外にいる!'); }

(関数 () {
  if (偽) {
    // 関数 f の宣言を 1 回繰り返す
    function f() { console.log('私は中にいます!') }
  }

  f();
}());
// キャッチされない TypeError: f は関数ではありません

上記のコードは ES6 ブラウザでエラーを報告します。

ブロックレベルのスコープで宣言された関数の処理規則が変更されると、古いコードに大きな影響を与えることは明らかです。結果として生じる非互換性の問題を軽減するために、ES6 は 付録 B に含まれています。 web-legacy-compatibility-semantics) は、ブラウザ実装が上記の規制に準拠する必要はなく、独自の 動作 を持つことを規定しています。 -es6 のブロック レベル関数)。

  • 関数をブロック スコープで宣言できるようにします。
  • 関数宣言は var と同様です。つまり、グローバル スコープまたは関数スコープの先頭に昇格されます。
  • 同時に、関数宣言も、それが配置されているブロックレベルのスコープの先頭に昇格されます。

上記の 3 つのルールは ES6 ブラウザ実装にのみ有効であり、他の環境での実装は依然として「let」として扱われる必要がないことに注意してください。

これら 3 つのルールに従って、ブラウザの ES6 環境では、ブロックレベルのスコープで宣言された関数は、「var」で宣言された変数のように動作します。上記の例で実際に動作するコードは以下の通りです。

// ブラウザのES6環境
function f() { console.log('私は外にいる!'); }
(関数 () {
  var f = 未定義;
  if (偽) {
    function f() { console.log('私は中にいます!') }
  }

  f();
}());
// キャッチされない TypeError: f は関数ではありません

環境によって動作が大きく異なることを考慮すると、ブロックスコープでの関数宣言は避けるべきです。本当に必要な場合は、関数宣言文ではなく関数式として記述する必要があります。

// ブロックレベルのスコープ内では関数宣言ステートメントを使用しないことをお勧めします。
{
  a = '秘密' とします。
  関数 f() {
    を返します。
  }
}

// ブロックレベルのスコープ内では、関数式が最初に使用されます
{
  a = '秘密' とします。
  let f = function () {
    を返します。
  };
}

さらに、もう一つ注意が必要な点があります。 ES6 のブロックレベルのスコープには中括弧が必要です。中括弧がない場合、JavaScript エンジンはブロックレベルのスコープが存在しないと認識します。

//最初の書き方はエラーを報告することです
(真の場合) x = 1 とします。

// 2 番目の書き方では、エラーは報告されません
if (true) {
  x = 1 とします。
}

上記のコードでは、最初の書き方には中括弧が含まれていないため、ブロック レベルのスコープがなく、「let」は現在のスコープの最上位レベルでのみ使用できるため、エラーが報告されます。 2 番目の書き方では中括弧を使用するため、ブロックレベルのスコープが確立されます。

関数宣言についても同様です。厳密モードでは、関数は現在のスコープの最上位でのみ宣言できます。

// エラーを報告しません
'厳密を使用';
if (true) {
  関数 f() {}
}

// エラーを報告する
'厳密を使用';
もし(真)
  関数 f() {}

const コマンド

基本的な使い方

const は読み取り専用の定数を宣言します。定数の値は、一度宣言すると変更できません。

定数PI = 3.1415;
PI // 3.1415

PI = 3;
// TypeError: 定数変数への代入。

上記のコードは、定数の値を変更するとエラーが発生することを示しています。

const で宣言された変数は値を変更してはなりません。つまり、const 変数を宣言したら、すぐに初期化する必要があり、後で値を割り当てたままにすることはできません。

const foo;
// SyntaxError: const 宣言にイニシャライザがありません

上記のコードは、「const」について、値を割り当てずに宣言しただけの場合、エラーが報告されることを示しています。

const のスコープは let コマンドのスコープと同じです。これは、それが宣言されているブロックレベルのスコープ内でのみ有効です。

if (true) {
  定数MAX = 5;
}

MAX // キャッチされない参照エラー: MAX が定義されていません

const コマンドによって宣言された定数はプロモートされず、一時的なデッド ゾーンもあり、宣言された位置の後でのみ使用できます。

if (true) {
  console.log(MAX); // 参照エラー
  定数MAX = 5;
}

上記のコードは定数 MAX が宣言される前に呼び出され、エラーが報告されます。

const で宣言した定数は let のように繰り返し宣言することはできません。

var message = "こんにちは!";
年齢 = 25 としましょう。

// 次の 2 行はエラーを報告します
const message = "さようなら!";
定数年齢 = 30;

エッセンス

const が実際に保証しているのは、変数の値が変更できないということではなく、変数が指すメモリ アドレスに格納されているデータが変更できないということです。単純なタイプのデータ (数値、文字列、ブール値) の場合、値は変数が指すメモリ アドレスに格納されるため、定数と同等です。しかし、複合型のデータ (主にオブジェクトと配列) の場合、変数が指すメモリ アドレスは実際のデータへのポインタを格納するだけであり、このポインタが固定されていること (つまり、常に別のデータを指すこと) のみを保証できます。固定アドレス)、それが指すデータ構造が可変であるかどうかについては、完全に制御不能です。したがって、オブジェクトを定数として宣言するときは細心の注意を払う必要があります。

const foo = {};

// foo に属性を追加すると成功する可能性があります
foo.prop = 123;
foo.prop // 123

// foo が別のオブジェクトを指すとエラーが報告されます
foo = {}; // TypeError: "foo" は読み取り専用です

上記のコードでは、定数「foo」にオブジェクトを指すアドレスが格納されています。不変なのはこのアドレスのみです。つまり、foo で別のアドレスを指すことはできませんが、オブジェクト自体は変更可能であるため、新しいプロパティを追加することはできます。

別の例を示します。

定数 a = [];
a.push('Hello'); // 実行可能ファイル
a.length = 0; // 実行可能ファイル
a = ['Dave'] // エラーレポート

上記のコードでは、定数 'a' は配列です。配列自体は書き込み可能ですが、別の配列が 'a' に代入されるとエラーが報告されます。

本当にオブジェクトをフリーズしたい場合は、Object.freeze メソッドを使用する必要があります。

const foo = Object.freeze({});

// 通常モードでは、次の行は効果がありません。
// 厳密モードでは、この行はエラーを報告します
foo.prop = 123;

上記のコードでは、定数「foo」がフリーズされたオブジェクトを指しているため、新しいプロパティの追加は機能せず、厳密モードではエラーが報告されます。

オブジェクト自体をフリーズするだけでなく、オブジェクトのプロパティもフリーズする必要があります。以下はオブジェクトを完全にフリーズする関数です。

var constantize = (obj) => {
  オブジェクト.フリーズ(obj);
  Object.keys(obj).forEach( (key, i) => {
    if ( typeof obj[key] === 'オブジェクト' ) {
      constantize( obj[key] );
    }
  });
};

ES6 で変数を宣言する 6 つの方法

ES5 には、変数を宣言する方法が 2 つしかありません。「var」コマンドと「function」コマンドです。 ES6 では、「let」コマンドと「const」コマンドの追加に加えて、後の章で変数を宣言する他の 2 つの方法、「import」コマンドと「class」コマンドについても説明します。したがって、ES6 には変数を宣言する方法が合計 6 つあります。

最上位オブジェクトのプロパティ

最上位オブジェクトとは、ブラウザ環境の「ウィンドウ」オブジェクトと Node の「グローバル」オブジェクトを指します。 ES5 では、トップレベル オブジェクトのプロパティはグローバル変数と同等です。

window.a = 1;
// 1

a = 2;
window.a // 2

上記のコードでは、トップレベル オブジェクトの属性の割り当てとグローバル変数の割り当ては同じものです。

最上位オブジェクトのプロパティはグローバル変数にリンクされていますが、これは JavaScript 言語の最大の設計上の失敗の 1 つであると考えられています。この設計にはいくつかの大きな問題があります。まず、宣言されていない変数のエラーをコンパイル時に報告することは不可能です。これは、グローバル変数がトップレベルのオブジェクトの属性によって作成される可能性があり、属性の作成が動的であるためです。 ); 第 2 に、プログラマは無意識のうちにグローバル変数を簡単に作成してしまう可能性があります (入力ミスなど)。最後に、トップレベルのオブジェクトのプロパティはどこでも読み書きできるため、モジュール型プログラミングには非常に不利です。一方、「window」オブジェクトは実体的な意味を持ち、ブラウザのウィンドウオブジェクトを指しますが、これも不適当です。

これを変更するために、ES6 では、互換性を維持するために、「var」コマンドと「function」コマンドによって宣言されたグローバル変数は引き続きトップレベル オブジェクトの属性であることを規定しています。それは、「let」コマンドと「const」コマンド、「class」コマンドによって宣言されたグローバル変数はトップレベルオブジェクトの属性ではないことを規定しています。言い換えれば、ES6 以降、グローバル変数はトップレベル オブジェクトのプロパティから徐々に切り離されることになります。

変数 a = 1;
// Node の REPL 環境にいる場合は、global.a として記述できます。
// または、一般的なメソッドを使用して this.a として記述します。
window.a // 1

b = 1 とします。
window.b // 未定義

上記のコードでは、グローバル変数 avar コマンドによって宣言されているため、これはトップレベル オブジェクトのプロパティです。グローバル変数 blet コマンドによって宣言されているため、最上位オブジェクトのプロパティではないため、「unknown」が返されます。

グローバルこのオブジェクト

JavaScript 言語にはグローバル環境 (つまり、グローバル スコープ) を提供するトップレベルのオブジェクトがあり、すべてのコードはこの環境で実行されます。ただし、トップレベルのオブジェクトは実装間で均一ではありません。

  • ブラウザでは最上位オブジェクトは window ですが、Node と Web Worker には window がありません。
  • ブラウザや Web ワーカーでは、self もトップレベルのオブジェクトを指しますが、Node には self がありません。
  • Node では、トップレベルのオブジェクトは global ですが、他の環境ではサポートされていません。

さまざまな環境で同じコード部分でトップレベルのオブジェクトを取得するために、現在一般的に this キーワードが使用されていますが、これには制限があります。

  • グローバル環境では、「this」はトップレベルのオブジェクトを返します。ただし、Node.js モジュールの「this」は現在のモジュールを返し、ES6 モジュールの「this」は「unknown」を返します。
  • 関数内の this。関数がオブジェクトのメソッドとしてではなく、単に関数として実行される場合、this はトップレベル オブジェクトを指します。ただし、厳密モードでは、この時点で「this」は「未定義」を返します。
  • 厳密モードか通常モードかに関係なく、new Function('return this')() は常にグローバル オブジェクトを返します。ただし、ブラウザが CSP (コンテンツ セキュリティ ポリシー、コンテンツ セキュリティ ポリシー) を使用している場合、メソッド eval および new Function は使用できない場合があります。

要約すると、あらゆる状況でトップレベルのオブジェクトを取得する方法を見つけるのは困難です。ここでは、ほとんど機能しない 2 つの方法を紹介します。

//方法1
(ウィンドウの種類 !== '未定義'
   窓
   : (プロセスの種類 === 'オブジェクト' &&
      typeof require === '関数' &&
      グローバルのタイプ === 'オブジェクト')
     ?グローバル
     : これ);

//方法2
var getGlobal = function () {
  if (typeof self !== '未定義') { return self }
  if (ウィンドウの種類 !== '未定義') { ウィンドウを返す }
  if (typeof global !== '未定義') { return global }
  throw new Error('グローバル オブジェクトが見つかりません');
};

ES2020 言語標準レベルでは、「globalThis」がトップレベルのオブジェクトとして導入されます。言い換えれば、「globalThis」はどの環境にも存在し、そこからグローバル環境の「this」を指すトップレベルのオブジェクトを取得できます。

shim ライブラリ global-this はこの提案をシミュレートし、すべての環境で globalThis を取得できます。


作者: wangdoc

アドレス: https://wangdoc.com/

ライセンス: クリエイティブ・コモンズ 3.0