Summary of new features of ECMAScript 2020(ES11)

Express Lane:

According to the old rules, first take a look at the new functions of ES2020:

  • Dynamic import (): Import on demand
  • Null value merging operator: the expression is in?? The left operator of is evaluated as undefined or null, and the right operator is returned
  • Optional links: User detects uncertain intermediate nodes
  • BigInt: a new basic data type that represents an integer of arbitrary precision
  • globalThis: Browser: window, worker: self, node: global
  • Promise.allSettled: returns a promise decided after all given promises have been decided or rejected, with an object array. Each object represents the corresponding promise result
  • For in structure: used to standardize the traversal order of for in statements

1. Dynamic import ()

It is used to realize on-demand import, import() is a syntax keyword similar to a function, similar to super(). It receives a string as the module identifier and returns a promise

In the module syntax defined in ES 2015, all module import syntax is statically declared:

import aExport from "./module"
import * as exportName from "./module"
import { export1, export2 as alias2 } from "./module"
import "./module"

Although this set of syntax can meet most import requirements, it can also support a series of important functions such as static analysis and tree jitter. However, it can not meet some needs of dynamic import. For example:

  • You need to selectively load some support libraries according to browser compatibility,
  • Load the code of a module only when it is actually needed, and then
  • Simply want to delay loading some modules to improve the loading experience in a progressive rendering way

And so on, which are common requirements in practical work. Without dynamic import, it will be difficult to realize these requirements. Although we can dynamically import some scripts by creating script tags, this is a browser specific implementation and cannot be directly combined with the existing module syntax. Therefore, it can only be used as an internal implementation mechanism, but cannot be directly exposed to the users of the module.

But dynamic import () solves this problem. It can be used in any platform that supports this syntax, such as webpack, node, or browser environment. Moreover, the format of module identifier is specified by each platform. For example, webpack and node support directly loading node with module name_ Modules, while the browser supports loading remote modules using URLs.

import('lodash').then(_ => {
    // other
})

When the module and other modules it depends on are loaded and executed, promise will enter the fully completed state, and the result value is an object containing all the exported contents of the module: the named export item is placed in the attribute with the same name of the object, while the default export item is placed in the attribute with the name of default. For example, there is the following module utils, and its import method is as follows:

// utils
export default 'hello lxm';
export const x = 11;
export const y = 22;
// Import
import('a').then(module => {
    console.info(module)
})
// result:
{
   default: 'hello lxm'',
   x: 11,
   y: 22,
}

If the module fails to load or execute because the module does not exist or cannot be accessed, promise will enter the rejected state, where you can perform some fallback processing.

2. Null merge operator (?)

You may have encountered that if a variable is empty, you need to assign it a default value. We usually write this:

let num = number || 222

However, there will be a bug in the above code. If the value of realCount is 0, it will be regarded as unable to get its value, and the string 'unable to get' will be obtained. If you want to do this, you can only use ternary operators before:

let num = (number !== undefined) ? number : 222

But now you can use?? Operator. It takes the value on the right of the operator only when the value on the left of the operator is null or undefined:

let num = number ?? 222

Moreover, the operator also supports short circuit characteristics:

const x = a ?? getDefaultValue()
// When 'a' is not 'undefined' or 'null', the 'getDefaultValue' method will not be executed

However, it should be noted that this operator cannot be shared with the AND OR operator, otherwise a syntax exception will be thrown:

a && b ?? "default"    // SyntaxError

The ambiguity of this code is serious. In the understanding of different people, some people may feel that press (A & & B)?? "Default" operation is reasonable, while others think it is only right to run according to a & & (B??? "Default"), so when designing this operator, they simply avoid this situation through syntactic constraints. If you really need to use them in the same expression at the same time, use parentheses to distinguish them:

(a && b) ?? "default"

The main design purpose of this operator is to provide a supplementary operator for the optional chain operator, so it is usually used together with the optional chain operator:

const x = a?.b ?? 0;

The following describes the new optional chain operator (?.) in ES11

3. Optional link

This syntax can greatly simplify our code when we need to try to access properties or methods in an object and are not sure whether the object exists, such as the following:

const el = document.querySelector(".class-a")
const height = el.clientHeight

When we don't know whether there is an element with a class name of class-a in the page, we need to make some judgments before accessing clientHeight to prevent bug s:

const height = el ? el.clientHeight : undefined

Although the above method can be implemented, some people will find it troublesome. Using the "optional chain operator" can simplify the code into the following form:

const height = el?.clientHeight

The following describes common usage scenarios:

Property access

If you need to obtain the attributes in an object, you can use this syntax:

a?.b
a?.[x]

In the above code, if a is undefined or null, the expression will immediately return undefined, otherwise it will return the value of the accessed property. In other words, they are equivalent to the following code:

a == null ? undefined : a.b
a == null ? undefined : a[x]

Method call

You can also use this syntax when trying to call a method:

a?.()

Similarly, if a is undefined or null, undefined will be returned, otherwise the method will be called. However, it should be noted that the operator does not judge whether a is a function type, so if a is a value of other types, the code will still throw an exception at run time.

Access deep level properties

This operator can also be used in series when accessing the deeper level properties of an object:

a?.b?.[0]?.()?.d

Some people may not bother to judge whether it is really necessary first, so they add this operator to each attribute in the access link. However, as shown in the above code, this code is not readable. Moreover, if an object that should exist does not exist because of some bug s, an exception should be thrown when accessing it, so that the problem can be found in time, rather than being hidden. It is recommended to use optional chain operators only when necessary.

4,BigInt

In ES, all values of type Number are stored in 64 bit floating-point format, so the maximum integer that type Number can effectively represent is 2 ^ 53. With the new BigInt type, you can manipulate integers of any precision.

There are two ways to use: 1. Add suffix n after the number literal; 2. Use its constructor BigInt

const bigInt = 9007199254740993n
const bigInt = BigInt(9007199254740992)

//When the maximum integer limit of Number is exceeded, we can also pass in a string that may be parsed correctly instead
const bigInt = BigInt('9007199254740993')

Like Number, BigInt also supports +, -, * *,% operators:

3n + 2n    // => 5n
3n 2n // => 6n
3n 2n // => 9n
3n % 2n // => 1n

However, because BigInt is a pure integer type and cannot represent decimal places, the result value of BigInt's division operation (/) is still an integer, that is, rounding down:

const bigInt = 3n;
bigInt / 2n; // =>1n instead of 1.5n

Bit operators are also supported, except for the unsigned shift right operator:

1n & 3n    // => 1n
1n | 3n // => 3n
1n ^ 3n // => 2n
~1n // => -2n
1n << 3n // => 8n
1n >> 3n // => 0n

1n >>> 3n // Uncaught TypeError: BigInts have no unsigned right shift, use >> instead

BigInt can be concatenated with strings using the + operator

1n + ' Number'   // => 1 Number
'Number ' + 2n // => Number 2

BigInt is not supported in the following scenarios:

1. BigInt cannot operate with Number, and a type exception will be thrown

1n + 1
// Uncaught TypeError: Cannot mix BigInt and other types, use explicit conversions

2. Some built-in modules, such as Math, do not support BigInt and will also throw exceptions

Math.pow(2n, 64n)
// Uncaught TypeError: Cannot convert a BigInt value to a number

3. BigInt and Number are equal, but not strictly equal, but they can compare sizes

1n  1    // => true
1n = 1 // => false

But they can be compared in size:

1n < 2     // => true
1n < 1 // => false

2n > 1 // => true
2n > 2 // => false

In addition, when converting to Boolean value, like Number, 0n turns to false and other values turn to true:

!!0n       // => false
!!1n // => true

In addition, only the constructor of the other party can be used for conversion between the two:

Number(1n) // => 1
BigInt(1) // => 1n

However, the conversion between the two also has some boundary problems:

// When the precision of BigInt value exceeds the range represented by Number type, the problem of precision loss occurs
Number(9007199254740993n)
// => 9007199254740992

// BigInt throws an exception when there are decimal places in the Number value
BigInt(1.1)
// VM4854:1 Uncaught RangeError: The number 1.1 cannot be converted to a BigInt because it is not an integer

In addition, two array types corresponding to BigInt are also provided in the typed array: BigInt64Array and BigUint64Array:

const array = new BigInt64Array(4);
array[0] // => 0n

array[0] = 2n
array[0] // => 2n

However, because each element is limited to 64 bits, even if an unsigned type is used, it can only represent 2 ^ 64 - 1 at most:

const array = new BigUint64Array(4);
array[0] = 2n 64n
array[0] // => 0n

array[0] = 2n ** 64n - 1n
array[0] // => 18446744073709551615n

5,globalThis

Browser: window, worker: self, node: global

In the browser environment, we can access global objects in many ways. The most commonly used is certainly window, but in addition, there are self and frames, parallel and top used in special scenes.

We usually don't care much about the difference between window and self, but if we use Web Worker, we should understand that window is a global attribute only in the main thread. In the Worker thread, we need to use self instead.

In the node.js environment, we need to use global, such as JSC.js In this more niche environment, you need to use this.

In general development work, you may rarely need to access the global environment, and most of the time you only need to develop based on one environment, so you don't need to deal with this troublesome problem. But for es6-shim For this kind of basic library that needs to support multiple environments, they need to solve this problem.

Earlier, we can easily get the global object through the following code:

const globals = (new Function('return this;'))()

But by Chrome APP content security policy In order to alleviate the problem of cross site scripting attacks, the policy requires that eval and related functions be prohibited. The above code will not be executed normally in the running environment of Chrome APP.

In desperation, libraries like ES6 shim can only Enumerate all possible global attributes:

var getGlobal = function () {
// the only reliable means to get the global object is
// Function('return this')()
// However, this causes CSP violations in Chrome apps.
if (typeof self ! 'undefined') { return self; }
if (typeof window ! 'undefined') { return window; }
if (typeof global ! 'undefined') { return global; }
throw new Error('unable to locate global object');
};
var globals = getGlobal();
if (!globals.Reflect) {
defineProperty(globals, 'Reflect', {}, true);
}

This kind of problem is really encountered, and it is very troublesome to deal with it every time. That's why there is global this in this proposal.

Through globalThis, we can finally use a standard method to get global objects without caring about the running environment of the code. This is a great convenience feature for libraries like es6-shim:

if (!globalThis.Reflect) {
defineProperty(globalThis, 'Reflect', {}, true);
}

In addition, there are some details about globalThis, such as to meet Secure ECMAScript The requirements of globalThis are writable. In the browser page, the outer window Under the influence of features, globalThis actually points to WindowProxy, rather than the real global object in the current page (the object cannot be accessed directly).

6,Promise.allSettled

stay Promise provides a set of combination methods (such as the most commonly used ones) Promise.all), which receive multiple promise objects and return a new promise representing the combination result. According to the result state of the incoming promise, the combined promise will be switched to different states.

So far, there are four such methods. There are only logical differences between the four methods, and they all have their own applicable scenarios:

  • Promise.all returns a combined promise. When all promises are switched to the full state, the promise is switched to the full state; However, if any one of the projects is switched to the rejected state, the project will be switched to the rejected state immediately;
  • Promise.race returns a combined promise. When any one of the promises is switched to the fully or rejected state, the promise will be switched to the same state immediately;
  • Promise.allSettled returns a combined promise. When all promises are switched to the full or rejected state, the promise will be switched to the full state;
  • Promise.any returns a combined promise. When any one of the promises is switched to the full state, the promise will be switched to the full state immediately, but the promise will be switched to the rejected state only when all the promises are switched to the rejected state. (ECMAScript2021 )

Promise.allSettled usage:

Pass in an array, put any number of promise objects in it, and accept a new promise representing the combination result.

It should be noted that the combined promise will wait for all the incoming promises. When they are all switched to the state (whether in the fully qualified state or the rejected state), the combined promise will switch to the fully qualified state and give the result information of all the promises:

async function a() {
const promiseA = fetch('/api/a') // => rejected, <Error: a>
const promiseB = fetch('/api/B') // => fulfilled, "b"

const results = await Promise.allSettled([ promiseA, promiseB])
results.length // => 3
results[0] // => { status: "rejected", reason: <Error: a> }
results[1] // => { status: "fulfilled", value: "b" }
}

Because the result value is an array, you can easily filter out any result information you are interested in:

// Get the result information of all completed states
results.filter( result => result.status = "fulfilled" )
// Get the result information of all rejected states
results.filter( result => result.status = "rejected" )
// Get the result information of the first rejected state
results.find( result => result.status = "rejected" )

The usage scenarios are as follows:

1. Sometimes, during the initialization process of a page, you need to load multiple copies of initialization data or perform some other initialization operations, and you usually want to wait until these initialization operations are completed before executing the subsequent process:

async function init() {
setInited(false)
setInitError(undefined)
const results = await Promise.allSettled([
loadDetail(),
loadRecommentListFirstPage(),
initSDK(),
])
const errors = results
.filter( result => result.status = "rejected" )
.map( rejectedResult => rejectedResult.reason )
if (errors.length) {
setInitError(errors[0])
$logs.error(errors)
}
setInited(true)}

2. For another example, if we have a custom global message center, we can also do some Asynchronous Support Based on allSettled. For example, after the login pop-up layer is opened and the user successfully logs in, a login event is broadcast to the page. Usually, after listening to the event in other parts of the page, we need to request new data from the server. At this time, we may need to close the login pop-up layer after all the data are updated:

async function login() {
// goto login ...

const results = messageCenter.login.emit()
const promiseResults = results.filter(isPromise)
if (promiseResults.length) {
await Promise.allSettled(promiseResults)
}
closeLoginModal()
closeLoading()}

7. For in structure

For specification Traversal order of for in statements

In the previous es specification, the order of for in statements during traversal is hardly specified, but the implementation of each es engine tends to be consistent in most cases, and it will be different only in some boundary cases. It is difficult for us to require all engines to be completely consistent. The main reason is that for in is the most complex of all traversal APIs in ES. Coupled with the omission of the specification, major browsers have many unique implementation logic when implementing the API. It is difficult for the maintainers of each engine to be willing to review this part of the code.

Therefore, the author of the specification has done a lot of work to test the traversal logic of for in in many existing ES engines. And sort out the consistent parts between them, and then add this part to ES specification among.

In addition, the specification also provides a sample code for each engine to use when implementing for in logic. You can take a look at:

function* EnumerateObjectProperties(obj) {
const visited = new Set();
for (const key of Reflect.ownKeys(obj)) {
if (typeof key = "symbol") continue;
const desc = Reflect.getOwnPropertyDescriptor(obj, key);
if (desc) {
visited.add(key);
if (desc.enumerable) yield key;
}
}
const proto = Reflect.getPrototypeOf(obj);
if (proto === null) return;
for (const protoKey of EnumerateObjectProperties(proto)) {
if (!visited.has(protoKey)) yield protoKey; }}

Reference:

Tags: Javascript ECMAScript

Posted on Tue, 09 Nov 2021 17:41:58 -0500 by djloc286