Understand the ECMAScript specification

Understand the ECMAScript specification

Overview

The specification file is the official standard of computer language, describing in detail the grammatical rules and implementation methods.

Generally speaking, there is no need to read the specifications unless you want to write a compiler. Because the specifications are very abstract and refined, they lack practical examples, are not easy to understand, and are not very helpful in solving practical application problems. However, if you encounter a difficult grammatical problem and really can't find the answer, you can check the specification file to understand what the language standard says. Specifications are the "last resort" to the problem.

This is necessary for the JavaScript language. Because its usage scenarios are complex, grammatical rules are not uniform, there are many exceptions, and the behavior of various operating environments is inconsistent, leading to endless strange grammatical problems. No grammar book can cover all situations. Checking the specifications can be regarded as the most reliable and authoritative ultimate method for solving grammatical problems.

This chapter introduces how to read and understand ECMAScript 6 specification files.

The specifications of ECMAScript 6 can be found on the official website of ECMA International Standards Organization (www.ecma-international.org/ecma-262/6.0/) Free download and online reading.

This specification file is quite large, with 26 chapters in total, and if it is printed in A4, it has a full 545 pages. Its characteristic is that the regulations are very detailed, and every grammatical behavior and the realization of every function are described in detail and clearly. Basically, the compiler author only needs to translate each step into code. This largely ensures that all ES6 implementations have consistent behavior.

Among the 26 chapters of the ECMAScript 6 specification, Chapters 1 to 3 are an introduction to the file itself, which has little to do with language. Chapter 4 is a description of the overall design of the language, and interested readers can read it. Chapters 5 to 8 are the description of the macro level of the language. Chapter 5 is an introduction to the explanation and writing of specifications, Chapter 6 is an introduction to data types, Chapter 7 is an introduction to abstract operations used in the language, and Chapter 8 is an introduction to how the code runs. Chapter 9 to Chapter 26 introduce the specific syntax.

For general users, except for Chapter 4, other chapters involve some details, so you don't need to read through them, just refer to the relevant chapters when you use them.

the term

The ES6 specification uses some specialized terms. Understanding these terms can help you understand the specifications. This section introduces a few of them.

Abstract operation

The so-called "abstract operations" are some internal methods of the engine, which cannot be called from outside. The specification defines a series of abstract operations, specifies their behavior, and leaves it to various engines to implement them.

For example, the first step of the Boolean(value) algorithm is this.

  1. Let b be ToBoolean(value).

The ToBoolean here is an abstract operation, an algorithm for calculating Boolean values ​​inside the engine.

Many function algorithms use the same steps multiple times, so the ES6 specification extracts them and defines them as "abstract operations" for easy description.

Record and field

The ES6 specification calls the data structure of a key-value map a Record, and each set of key-value pairs is called a field. This means that a Record is composed of multiple fields, and each field contains a key and a value.

[[Notation]]

ES6 specifications use [[Notation]] this notation extensively, such as [[Value]], [[Writable]], [[Get]], [[Set]], etc. . It is used to refer to the key name of the field.

For example, obj is a Record, which has a Prototype property. The ES6 specification does not write obj.Prototype, but instead writes obj.[[Prototype]]. Generally speaking, the attributes using the notation of [[Notation]] are the internal attributes of the object.

All JavaScript functions have an internal attribute [[Call]] to run the function.

F[[Call]](V, argumentsList);

In the above code, F is a function object, [[Call]] is its internal method, F.[[call]]() means to run the function, and V means [[Call] ]The value of this at runtime, and argumentsList is the parameter passed into the function when calling.

Completion Record

Each statement will return a Completion Record, which represents the result of the operation. Each Completion Record has a [[Type]] property, which indicates the type of the running result.

The [[Type]] attribute has five possible values.

-normal -return -throw -break -continue

If the value of [[Type]] is normal, it is called normal completion, which means the operation is normal. Other values ​​are called abrupt completion. Among them, developers only need to pay attention to the situation where [[Type]] is throw, that is, there is an error in operation; the three values ​​of break, continue, and return only appear in specific scenarios, so you don’t need to consider .

Standard flow of abstract operations

The running process of abstract operations is generally as follows.

  1. Let result be AbstractOp().
  2. If result is an abrupt completion, return result.
  3. Set result to result.[[Value]].
  4. return result.

The first step above calls the abstract operation AbstractOp() to get result, which is a Completion Record. In the second step, if result belongs to abrupt completion, return directly. If there is no return here, it means that result belongs to normal completion. The third step is to set the value of result to resultCompletionRecord.[[Value]]. The fourth step is to return to result.

The ES6 specification expresses this standard process in abbreviations.

  1. Let result be AbstractOp().
  2. ReturnIfAbrupt(result).
  3. return result.

The ReturnIfAbrupt(result) in this abbreviation represents the second and third steps above, that is, if an error is reported, an error is returned, otherwise the value is retrieved.

There are even further abbreviations.

  1. Let result be ? AbstractOp().
  2. return result.

The ? in the above process means that AbstractOp() may report an error. Once an error is reported, the error is returned, otherwise the value is retrieved.

In addition to ?, the ES 6 specification also uses another shorthand symbol !.

  1. Let result be ! AbstractOp().
  2. return result.

The ! in the above process means that AbstractOp() will not report an error, and the return must be normal completion, and the value can always be retrieved.

Equality operator

Here are some examples to introduce how to use this specification.

The equality operator (==) is a very headache operator. Its grammatical behavior is changeable and unintuitive. This section will take a look at how the specifications dictate its behavior.

Please look at the following expression, what is its value.

0 == null;

If you are not sure about the answer, or want to know how the language is handled internally, you can check the specifications, [Section 7.2.12](http://www.ecma-international.org/ecma-262/6.0/#sec-abstract- equality-comparison) is a description of the equality operator (==).

The description of each grammatical behavior in the specification is divided into two parts: first the overall behavior description, and then the implementation algorithm details. The overall description of the equality operator is only one sentence.

"The comparison x == y, where x and y are values, produces true or false."

The meaning of the above sentence is that the equality operator is used to compare two values ​​and returns true or false.

Here are the algorithm details.

  1. ReturnIfAbrupt(x).
  2. ReturnIfAbrupt(y).
  3. If Type(x) is the same as Type(y), then
  4. Return the result of performing Strict Equality Comparison x === y.
  5. If x is null and y is undefined, return true.
  6. If x is undefined and y is null, return true.
  7. If Type(x) is Number and Type(y) is String, return the result of the comparison x == ToNumber(y).
  8. If Type(x) is String and Type(y) is Number, return the result of the comparison ToNumber(x) == y.
  9. If Type(x) is Boolean, return the result of the comparison ToNumber(x) == y.
  10. If Type(y) is Boolean, return the result of the comparison x == ToNumber(y).
  11. If Type(x) is either String, Number, or Symbol and Type(y) is Object, then return the result of the comparison x == ToPrimitive(y).
  12. If Type(x) is Object and Type(y) is either String, Number, or Symbol, then return the result of the comparison ToPrimitive(x) == y.
  13. Return false.

The above algorithm has a total of 12 steps. The translation is as follows.

  1. If x is not a normal value (for example, an error is thrown), interrupt execution.
  2. If y is not a normal value, interrupt execution.
  3. If Type(x) is the same as Type(y), perform the strict equality operation x === y.
  4. If x is null and y is undefined, return true.
  5. If x is undefined and y is null, return true.
  6. If Type(x) is a number and Type(y) is a string, return the result of x == ToNumber(y).
  7. If Type(x) is a string and Type(y) is a number, return the result of ToNumber(x) == y.
  8. If Type(x) is a boolean value, return the result of ToNumber(x) == y.
  9. If Type(y) is a boolean value, return the result of x == ToNumber(y).
  10. If Type(x) is a string or numeric value or a Symbol value and Type(y) is an object, return the result of x == ToPrimitive(y).
  11. If Type(x) is an object and Type(y) is a string or numeric value or a Symbol value, return the result of ToPrimitive(x) == y.
  12. Return false.

Since the type of 0 is numeric, the type of null is Null (this is the specification [Section 4.3.13](http://www.ecma-international.org/ecma-262/6.0/#sec-terms- and-definitions-null-type) is the result of the internal Type operation and has nothing to do with the typeof operator). Therefore, the first 11 steps above can not get the result, and you can get false until the 12th step.

0 == null; // false

Array space

Let's look at another example.

const a1 = [undefined, undefined, undefined];
const a2 = [, , ,];

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

a1[0]; // undefined
a2[0]; // undefined

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

In the above code, the members of the array a1 are three undefined, and the members of the array a2 are three empty positions. These two arrays are very similar, the length is 3, and the members at each position are read out as undefined.

However, they actually have major differences.

0 in a1; // true
0 in a2; // 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); // [,, ,]

The above code lists four operations in total, and the results of the arrays a1 and a2 are different. The first three operations (the in operator, the hasOwnProperty method of the array, and the Object.keys method) all show that the array a2 cannot get the property name. The last operation (the map method of the array) shows that the array a2 has not been traversed.

Why is the behavior of a1 and a2 members inconsistent? The members of the array are undefined or empty. What is the difference?

The [12.2.5 section "Initialization of Arrays"] of the specification] (http://www.ecma-international.org/ecma-262/6.0/#sec-array-initializer) gives the answer.

"Array elements may be elided at the beginning, middle or end of the element list. Whenever a comma in the element list is not preceded by an AssignmentExpression (ie, a comma at the beginning or after another comma), the missing array element contributes to the length of the Array and increases the index of subsequent elements. Elided array elements are not defined. If an element is elided at the end of an array, that element does not contribute to the length of the Array."

The translation is as follows.

"Array members can be omitted. As long as there is no expression before the comma, the length property of the array will be increased by 1, and the position index of the subsequent members will be increased accordingly. The omitted members will not be defined. If the omitted members Is the last member of the array, it will not increase the length property of the array."

The above specifications make it very clear that the space of the array will be reflected in the length property, which means that the space has its own position, but the value of this position is undefined, that is, this value does not exist. If you must read it, the result will be undefined (because undefined does not exist in the JavaScript language).

This explains why the in operator, the hasOwnProperty method of the array, and the Object.keys method all fail to get the empty property name. Because this attribute name does not exist at all, the specification does not say that the attribute name (position index) should be assigned to the space, only that the position index of the next element should be increased by 1.

As for why the map method of the array skips the space, please see the next section.

Array's map method

The [22.1.3.15 section] of the specification (http://www.ecma-international.org/ecma-262/6.0/#sec-array.prototype.map) defines the map method of arrays. This section first describes the behavior of the map method in general, and there is no mention of array vacancies.

The following algorithm description is like this.

  1. Let O be ToObject(this value).
  2. ReturnIfAbrupt(O).
  3. Let len be ToLength(Get(O, "length")).
  4. ReturnIfAbrupt(len).
  5. If IsCallable(callbackfn) is false, throw a TypeError exception.
  6. If thisArg was supplied, let T be thisArg; else let T be undefined.
  7. Let A be ArraySpeciesCreate(O, len).
  8. ReturnIfAbrupt(A).
  9. Let k be 0.
  10. Repeat, while k <len
  11. Let Pk be ToString(k).
  12. Let kPresent be HasProperty(O, Pk).
  13. ReturnIfAbrupt(kPresent).
  14. If kPresent is true, then
  15. Let kValue be Get(O, Pk).
  16. ReturnIfAbrupt(kValue).
  17. Let mappedValue be Call(callbackfn, T, «kValue, k, O»).
  18. ReturnIfAbrupt(mappedValue).
  19. Let status be CreateDataPropertyOrThrow (A, Pk, mappedValue).
  20. ReturnIfAbrupt(status).
  21. Increase k by 1.
  22. Return A.

The translation is as follows.

  1. Get the this object of the current array
  2. Return if an error is reported
  3. Find the length property of the current array
  4. Return if an error is reported
  5. If the parameter callbackfn of the map method is not executable, an error will be reported
  6. If this is specified in the parameters of the map method, let T be equal to this parameter, otherwise T is undefined
  7. Generate a new array A, consistent with the length property of the current array
  8. Return if an error is reported
  9. Set k equal to 0
  10. As long as k is less than the length property of the current array, repeat the following steps
  11. Set Pk equal to ToString(k), that is, convert K to a string
  12. Set kPresent equal to HasProperty(O, Pk), that is, whether the current array has specified properties
  13. Return if an error is reported
  14. If kPresent is equal to true, proceed to the following steps
  15. Set kValue equal to Get(O, Pk), and retrieve the specified attribute of the current array
  16. Return if an error is reported
  17. Set mappedValue equal to Call(callbackfn, T, «kValue, k, O»), that is, execute the callback function
  18. Return if an error is reported
  19. Set status equal to CreateDataPropertyOrThrow (A, Pk, mappedValue), that is, put the value of the callback function into the specified position of the A array
  20. Return if an error is reported
  21. Increase k by 1
  22. Back to A

If you look carefully at the above algorithm, you can find that when dealing with an array with all vacancies, there is no problem with the previous steps. When entering step 2 of step 10, kPresent will report an error because the attribute name corresponding to the space does not exist for the array, so it will return and the subsequent steps will not be performed.

const arr = [, , ,];
arr.map((n) => {
  console.log(n);
  return 1;
}); // [,, ,]

In the above code, arr is an array of empty spaces. When the map method traverses the members and finds that there are empty spaces, it skips directly without entering the callback function. Therefore, the console.log statement in the callback function will not be executed at all, and the entire map method returns a new array with all vacancies.

[Implementation] (https://github.com/v8/v8/blob/44c44521ae11859478b42004f57ea93df52526ee/src/js/array.js#L1347) of the V8 engine's map method is as follows, you can see that it is completely consistent with the specification algorithm description .

function ArrayMap(f, receiver) {
  CHECK_OBJECT_COERCIBLE(this, "Array.prototype.map");

  // Pull out the length so that modifications to the length in the
  // loop will not affect the looping and side effects are visible.
  var array = TO_OBJECT(this);
  var length = TO_LENGTH_OR_UINT32(array.length);
  return InnerArrayMap(f, receiver, array, length);
}

function InnerArrayMap(f, receiver, array, length) {
  if (!IS_CALLABLE(f)) throw MakeTypeError(kCalledNonCallable, f);

  var accumulator = new InternalArray(length);
  var is_array = IS_ARRAY(array);
  var stepping = DEBUG_IS_STEPPING(f);
  for (var i = 0; i < length; i++) {
    if (HAS_INDEX(array, i, is_array)) {
      var element = array[i];
      // Prepare break slots for debugger step in.
      if (stepping) %DebugPrepareStepInIfStepping(f);
      accumulator[i] = %_Call(f, receiver, element, i, array);
    }
  }
  var result = new GlobalArray();
  %MoveArrayContents(accumulator, result);
  return result;
}