ECMAScript 仕様を理解する

概要

仕様書は、構文規則と実装方法を詳細に規定したコンピューター言語の公式標準です。

一般に、コンパイラを作成する場合を除き、仕様を読む必要はありません。仕様は非常に抽象的かつ簡潔であり、例が不足しているため、理解するのが容易ではなく、実際のアプリケーションの問題を解決するのにはあまり役に立ちません。ただし、文法上の難しい質問に遭遇し、本当に答えが見つからない場合は、仕様書をチェックして言語標準に記載されている内容を確認できます。仕様は問題を解決するための「最後の手段」です。

これは JavaScript 言語に必要です。使用シナリオは複雑で、文法規則は統一されておらず、多くの例外があり、さまざまな動作環境の動作に一貫性がないため、すべての状況をカバーできる奇妙な文法問題が無限に発生します。仕様を確認することは、文法問題を解決するための最も信頼でき、権威のある究極の方法です。

この章では、ECMAScript 6 仕様ファイルの読み方を紹介します。

ECMAScript 6 の仕様は、ECMA 国際標準化機構の公式 Web サイト ([www.ecma-international.org/ecma-262/6.0/](http://www.ecma-international.org/ecma- 262/6.0/ )) 無料でダウンロードしてオンラインで読むことができます。

この仕様書は全 26 章からなるかなり大部なもので、A4 に印刷すると 545 ページにもなります。非常に詳細な記述が特徴で、あらゆる文法動作やあらゆる機能の実装が詳細かつ明確に記述されています。基本的に、コンパイラの作成者は各ステップをコードに変換するだけで済みます。これにより、すべての ES6 実装にわたって一貫した動作がほぼ保証されます。

ECMAScript 6 仕様の 26 章のうち、第 1 章から第 3 章まではファイル自体の紹介であり、言語とはほとんど関係がありません。第 4 章は言語の全体的な設計について説明しており、興味のある読者は読むことができます。第 5 章から第 8 章までは言語のマクロレベルの説明です。第 5 章では仕様の命名法と記述方法の紹介、第 6 章ではデータ型の紹介、第 7 章では言語内で使用される抽象操作の紹介、第 8 章ではコードの実行方法を紹介します。第 9 章から第 26 章では、特定の構文を紹介します。

一般のユーザーにとっては、第 4 章を除いて、他のすべての章は特定の側面に詳細に関係しているため、使用する際に関連する章を参照する限り、すべてを読む必要はありません。

用語

ES6 仕様では、いくつかの専門用語が使用されています。これらの用語を理解すると、仕様を理解するのに役立ちます。このセクションではそのいくつかを紹介します。

抽象操作

いわゆる「抽象操作」はエンジンの一部の内部メソッドであり、外部から呼び出すことはできません。この仕様では、一連の抽象操作を定義し、その動作を規定し、さまざまなエンジンに実装を委ねています。

たとえば、「Boolean(value)」アルゴリズムの最初のステップは次のとおりです。

  1. b を ToBoolean(value) とします。

ここでの「ToBoolean」は抽象演算であり、エンジン内部でブール値を計算するためのアルゴリズムです。

多くの関数のアルゴリズムでは同じステップが複数回使用されるため、ES6 仕様では説明を容易にするためにそれらを抽出し、「抽象操作」として定義しています。

レコードとフィールド

ES6 仕様では、キーと値のマップのデータ構造をレコードと呼び、キーと値のペアの各グループをフィールドと呼びます。つまり、レコードは複数のフィールドで構成され、各フィールドにはキーと値が含まれます。

[[表記]]

ES6 仕様では、[[Value]]、[[Writable]]、[[Get]]、[[Set]] など、多くの [[Notation]] 表記法が使用されています。 。フィールドのキー名を参照するために使用されます。

たとえば、obj はレコードであり、Prototype プロパティがあります。 ES6 仕様では obj.Prototype ではなく、 obj.[[Prototype]] と記述されています。一般に、表記法 [[Notation]] を使用するプロパティはオブジェクトの内部プロパティです。

すべての JavaScript 関数には、関数の実行に使用される内部プロパティ [[Call]] があります。

F.[[Call]](V, argumentList)

上記のコードでは、F は関数オブジェクト、[[Call]] はその内部メソッド、F.[[call]]() は関数の実行を意味し、V は [[Call] を意味します。 ] ]実行時の this の値、argumentsList は、呼び出し時に関数に渡されるパラメーターです。

完了記録

各ステートメントは、実行結果を示す完了レコードを返します。各完了レコードには、実行結果のタイプを示す [[Type]] 属性があります。

[[Type]] 属性には 5 つの可能な値があります。

  • 普通 -戻る
  • 投げる
  • 壊す -続く

[[Type]] の値が normal の場合、正常完了といい、正常に動作していることを示します。他の値は突然完了と呼ばれます。このうち、開発者が注意する必要があるのは、[[Type]] が throw である場合、つまり操作エラーが発生する場合は break、 continue、 の 3 つの値です。 「return」は特定のシナリオでのみ使用されるため、考慮する必要はありません。

抽象操作の標準プロセス

抽象演算の実行プロセスは一般に次のとおりです。

  1. resultをAbstractOp()とします。
  2. result が突然完了した場合、result を返します。
  3. result を result.[[Value]] に設定します。
  4. 結果を返します。

上記の最初のステップでは、抽象操作 AbstractOp() を呼び出し、完了レコードである result を取得します。 2 番目のステップでは、「結果」が突然の完了に属する場合は、直接戻ります。ここでリターンがなければresultは正常終了に属することを意味します。 3 番目のステップは、result の値を resultCompletionRecord.[[Value]] に設定することです。 4 番目のステップは、「結果」を返すことです。

ES6 仕様では、この標準プロセスを短縮形で表現しています。

  1. resultをAbstractOp()とします。
  2. ReturnIfAbrupt(結果)。
  3. 結果を返します。

この略語の「ReturnIfAbrupt(result)」は上記の 2 番目と 3 番目のステップを表します。つまり、エラーが報告された場合はエラーが返され、それ以外の場合は値が取得されます。

さらに短縮形式もあります。

  1. 「result」を「AbstractOp()」とします。
  2. 結果を返します。

上記のプロセスの「?」は、「AbstractOp()」がエラーを報告する可能性があることを意味します。エラーが報告されるとエラーが返され、それ以外の場合は値が取得されます。

「?」に加えて、ES 6 仕様では別の短縮記号「!」も使用されます。

  1. 「result」を「AbstractOp()」とします。
  2. 結果を返します。

上記のプロセスの ! は、AbstractOp() がエラーを報告せず、正常終了を返さなければならず、常に値を取り出せることを意味します。

##等価演算子

以下に、この仕様の使用方法の例をいくつか示します。

等価演算子 (==) は非常に面倒な演算子であり、その文法的な動作は可変で直感的ではありません。このセクションでは、仕様でその動作がどのように規定されているかを見ていきます。

次の式を見て、その値が何であるかを尋ねてください。

0 == null

答えがわからない場合、または言語が内部的にどのように処理するかを知りたい場合は、仕様 セクション 7.2.12 を確認してください。 /#sec-abstract-equality-comparison) は、等価演算子 (==) の説明です。

仕様内の各構文動作の説明は 2 つの部分に分かれています。最初に全体的な動作の説明、次に実装アルゴリズムの詳細です。等価演算子の全体的な説明は 1 文だけです。

「比較 x == y (x と y は値) は、true または false を生成します。」

上記の文が意味するのは、等価演算子は 2 つの値を比較し、「true」または「false」を返すために使用されるということです。

以下にアルゴリズムの詳細を示します。

1.ReturnIfAbrupt(x)。 1.ReturnIfAbrupt(y)。

  1. Type(x) が Type(y) と同じである場合、
  2. 厳密等価比較 x === y を実行した結果を返します。
  3. x が null で、y が 未定義 の場合、true を返します。
  4. x が unknown で、y が null の場合、true を返します。
  5. Type(x) が Number で、Type(y) が String の場合、 比較 x == ToNumber(y) の結果を返します。
  6. Type(x) が String で、Type(y) が Number の場合、 比較 ToNumber(x) == y の結果を返します。
  7. Type(x) が Boolean の場合、ToNumber(x) == y の比較結果を返します。
  8. Type(y) が Boolean の場合、x == ToNumber(y) の比較結果を返します。
  9. Type(x) が String、Number、Symbol のいずれかで、Type(y) が Object の場合、 比較 x == ToPrimitive(y) の結果を返します。
  10. Type(x) が Object で、Type(y) が String、Number、または Symbol の場合、 比較 ToPrimitive(x) == y の結果を返します。
  11. false を返します。

上記のアルゴリズムは合計 12 ステップあり、翻訳すると次のようになります。

  1. x が正常な値ではない場合 (エラーが発生した場合など)、実行を中断します。
  2. y が通常の値ではない場合、実行を中断します。
  3. Type(x) が Type(y) と同じ場合、厳密等価演算 x === y を実行します。
  4. x が null で、y が 未定義 の場合、true を返します。
  5. x が unknown で、y が null の場合、true を返します。
  6. Type(x) が数値、Type(y) が文字列の場合、x == ToNumber(y) の結果を返します。
  7. Type(x) が文字列であり、Type(y) が数値の場合、ToNumber(x) == y の結果を返します。
  8. Type(x) がブール値の場合、ToNumber(x) == y の結果を返します。
  9. Type(y) がブール値の場合、x == ToNumber(y) の結果を返します。
  10. Type(x) が文字列、数値、または Symbol 値で、Type(y) がオブジェクトの場合、x == ToPrimitive(y) の結果を返します。
  11. Type(x) がオブジェクトで、Type(y) が文字列、数値、または Symbol 値の場合、ToPrimitive(x) == y の結果を返します。
  12. false を返します。

0 の型は数値であるため、null の型は Null になります (これは仕様 セクション 4.3.13 -terms-and-definitions-null-type) は内部 Type 演算の結果であり、typeof 演算子とは何の関係もありません。したがって、上記の最初の 11 ステップでは結果は得られず、ステップ 12 まで「false」は得られません。

0 == null // false

配列内の空の位置

別の例を見てみましょう。

const a1 = [未定義、未定義、未定義];
const a2 = [, , ,];

a1.length // 3
a2.length // 3

a1[0] // 未定義
a2[0] // 未定義

a1[0] === a2[0] // true

上記のコードでは、配列 'a1' のメンバーは 3 つの '未定義' で、配列 'a2' のメンバーは 3 つの空きです。 2 つの配列は非常に似ており、どちらも長さは 3 で、各位置のメンバーは「未定義」として読み取られます。

ただし、実際には大きな違いがあります。

a1 に 0 // true
a2 に 0 // false

a1.hasOwnProperty(0) // true
a2.hasOwnProperty(0) // false

Object.keys(a1) // ["0", "1", "2"]
Object.keys(a2) // []

a1.map(n => 1) // [1, 1, 1]
a2.map(n => 1) // [, , ,]

上記のコードは合計 4 つの操作をリストしており、配列 a1 と a2 の結果は異なります。最初の 3 つの操作 (「in」演算子、配列の「hasOwnProperty」メソッド、「Object.keys」メソッド) はすべて、配列「a2」がプロパティ名を取得できないことを示しています。最後の操作 (配列の map メソッド) は、配列 a2 が走査されていないことを示しています。

「a1」と「a2」のメンバーが一貫性のない動作をするのはなぜですか?配列のメンバーが「未定義」または空の場合の違いは何でしょうか?

答えは、仕様の [セクション 12.2.5「配列の初期化」] (http://www.ecma-international.org/ecma-262/6.0/#sec-array-initializer) に記載されています。

"配列要素は、要素リストの先頭、中間、または末尾で省略できます。要素リスト内のコンマの前に AssignmentExpression がない場合 (つまり、先頭にコンマがあるか、別のコンマの後にカンマがある場合)、欠落している配列要素は省略されます。配列の長さに影響し、後続の要素のインデックスが増加します。要素が配列の最後で削除された場合、その要素は配列の長さに影響しません。」

訳は以下の通りです。

"配列メンバーは省略できます。カンマの前に式がない限り、配列の length プロパティは 1 ずつ増加し、それに応じて後続のメンバーの位置インデックスも増加します。省略されたメンバーは省略されません。省略されたメンバーが配列の最後のメンバーである場合、配列の length プロパティは増加しません。

上記の仕様は、配列内のギャップが「length」属性に反映されることを明確にしています。つまり、ギャップには独自の位置がありますが、この位置の値は未定義、つまり値が存在しません。 。読み取る必要がある場合、結果は「未定義」になります (「未定義」は JavaScript 言語に存在しないことを意味するため)。

これは、「in」演算子、配列の「hasOwnProperty」メソッド、および「Object.keys」メソッドが空のプロパティ名を取得できない理由を説明しています。この属性名はまったく存在しないため、仕様では空の位置に属性名(位置インデックス)を割り当てるとは書かれておらず、次の要素の位置インデックスを 1 加算するだけです。

配列の map メソッドが空の位置をスキップする理由については、次のセクションを参照してください。

配列のマップメソッド

仕様 (http://www.ecma-international.org/ecma-262/6.0/#sec-array.prototype.map) の [セクション 22.1.3.15] では、配列の map メソッドを定義しています。このセクションではまず、一般的な map メソッドの動作について説明しますが、配列のギャップについては触れません。

以下のアルゴリズムの説明は次のとおりです。

  1. OをToObject(この値)とします。 1.「ReturnIfAbrupt(O)」。
  2. len を ToLength(Get(O, "length")) とします。
  3. ReturnIfAbrupt(len)。
  4. IsCallable(callbackfn) が false の場合、TypeError 例外をスローします。
  5. thisArg が指定された場合は、T を thisArg にし、それ以外の場合は T を unknown にします。
  6. A を ArraySpeciesCreate(O, len) とします。 1.「ReturnIfAbrupt(A)」。
  7. k を 0 とします。
  8. k < len の間、繰り返します。
  9. Pk を ToString(k) とします。
  10. kPresentをHasProperty(O, Pk)とします。
  11. ReturnIfAbrupt(kPresent)。
  12. kPresent が true の場合、
  13. kValueをGet(O, Pk)とします。 1.「ReturnIfAbrupt(kValue)」。
  14. mappedValue を Call(callbackfn, T, «kValue, k, O») とします。
  15. 「ReturnIfAbrupt(mappedValue)」。
  16. status を CreateDataPropertyOrThrow (A, Pk,mappedValue) とします。
  17. 「ReturnIfAbrupt(ステータス)」。
  18. k を 1 ずつ増やします。
  19. A を返します。

訳は以下の通りです。

  1. 現在の配列の this オブジェクトを取得します
  2. エラーが報告された場合に返す
  3. 現在の配列の length 属性を見つける
  4. エラーが報告された場合に返す
  5. マップメソッドのパラメータ callbackfn が実行可能でない場合、エラーが報告されます
  6. マップメソッドの引数に this を指定した場合、T を引数と等しくします。それ以外の場合、T は未定義になります。
  7. 現在の配列の length プロパティと一致する新しい配列 A を生成します。
  8. エラーが報告された場合に返す
  9. k を 0 に設定します。
  10. k が現在の配列の length プロパティより小さい限り、次の手順を繰り返します。
  11. Pk を ToString(k) に等しく設定します。つまり、K を文字列に変換します。
  12. kPresentをHasProperty(O, Pk)と等しく設定します。つまり、現在の配列に指定されたプロパティがあるかどうかを調べます。
  13. エラーが報告された場合に返す
  14. kPresentがtrueに等しい場合は、次の手順に進みます。
  15. kValue を Get(O, Pk) に設定し、現在の配列の指定された属性を取得します
  16. エラーが報告された場合に返す
  17. mappedValue を Call(callbackfn, T, «kValue, k, O») に設定します。つまり、コールバック関数を実行します。
  18. エラーが報告された場合に返す
  19. status を CreateDataPropertyOrThrow (A, Pk, mappedValue) に等しく設定します。つまり、コールバック関数の値を A 配列の指定された位置に置きます。
  20. エラーが報告された場合に返す
  21. k が 1 ずつ増加します
  22. A を返す

上記のアルゴリズムを注意深く見てみると、空白でいっぱいの配列を処理する場合、前の手順では問題がないことがわかります。ステップ 10 でステップ 2 に入ると、ギャップに対応する属性名が配列に存在しないため、kPresent はエラーを報告し、戻り、以降のステップは実行されません。

const arr = [, , ,];
arr.map(n => {
  コンソール.log(n);
  1を返します。
}) // [, , ,]

上記のコードでは、「arr」はすべて空のビットを含む配列です。「map」メソッドがメンバーを走査して空のビットがあることが判明すると、それを直接スキップし、コールバック関数に入りません。したがって、コールバック関数内の console.log ステートメントはまったく実行されず、map メソッド全体がすべて空のビットを含む新しい配列を返します。

V8エンジンによるmapメソッドの実装は以下の通りであることがわかります。仕様のアルゴリズムの説明と完全に一致しています。

関数 ArrayMap(f, レシーバ) {
  CHECK_OBJECT_COERCIBLE(これ、「Array.prototype.map」);

  // 長さを引き出して、長さを変更できるようにします。
  // ループはループには影響しませんが、副作用は発生します。
  var 配列 = TO_OBJECT(this);
  var length = TO_LENGTH_OR_UINT32(array.length);
  return InnerArrayMap(f, レシーバ, 配列, 長さ);
}

function InnerArrayMap(f, レシーバー, 配列, 長さ) {
  if (!IS_CALLABLE(f)) throw MakeTypeError(kCalledNonCallable, f);

  var アキュムレータ = 新しい InternalArray(長さ);
  var is_array = IS_ARRAY(配列);
  var ステッピング = DEBUG_IS_STEPPING(f);
  for (var i = 0; i < length; i++) {
    if (HAS_INDEX(配列, i, is_array)) {
      var 要素 = 配列[i];
      // デバッガーのステップイン用にブレーク スロットを準備します。
      if (ステッピング) %DebugPrepareStepInIfStepping(f);
      アキュムレータ[i] = %_Call(f, レシーバ, 要素, i, 配列);
    }
  }
  var result = 新しい GlobalArray();
  %MoveArrayContents(アキュムレータ, 結果);
  結果を返します。
}

作者: wangdoc

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

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