Latest proposal

This chapter introduces some promising new proposals that have not yet entered the standard.

do expression

In essence, the block-level scope is a statement that encapsulates multiple operations and has no return value.

{
  let t = f();
  t = t * t + 1;
}

In the above code, the block-level scope encapsulates the two statements. However, outside the block-level scope, there is no way to get the value of t, because the block-level scope does not return a value unless t is a global variable.

There is now a proposal that enables block-level scope to become expressions, which means that values ​​can be returned. The way is before block-level scope Add do to make it a do expression, and then it will return the value of the last executed expression inside.

let x = do {
  let t = f();
  t * t + 1;
};

In the above code, the variable x will get the return value of the entire block-level scope (t * t + 1).

The logic of the do expression is very simple: what is encapsulated will be returned.

// equivalent to <expression>
do {<expression>;}

// equivalent to <statement>
do {<statement>}

The advantage of the do expression is that it can encapsulate multiple statements and make the program more modular, just like Lego bricks.

let x = do {
  if (foo()) {
    f();
  } else if (bar()) {
    g();
  } else {
    h();
  }
};

The essence of the above code is to call different functions according to the execution result of the function foo, and assign the return result to the variable x. Use the do expression to express the intention of this operation very concisely and clearly. Moreover, the do block-level scope provides a separate scope, and internal operations can be isolated from the global scope.

It is worth mentioning that the do expression is very useful in JSX syntax.

return (
  <nav>
    <Home />
    {do {
      if (loggedIn) {
        <LogoutButton />;
      } else {
        <LoginButton />;
      }
    }}
  </nav>
);

In the above code, if you don't use the do expression, you can only use the ternary judgment operator (?:). In that case, once the judgment logic is complicated, the code becomes very difficult to read.

throw expression

JavaScript syntax stipulates that throw is a command used to throw errors and cannot be used in expressions.

// report an error
console.log(throw new Error());

In the above code, the parameter of console.log must be an expression. If it is a throw statement, an error will be reported.

There is now a proposal that allows throw to be used in expressions.

// The default value of the parameter
function save(filename = throw new TypeError("Argument required")) {}

// The return value of the arrow function
lint(ast, {
  with: () => throw new Error("avoid using'with' statements."),
});

// conditional expression
function getEncoder(encoding) {
  const encoder =
    encoding === "utf8"
      ? new UTF8Encoder()
      : encoding === "utf16le"
      ? new UTF16Encoder(false)
      : encoding === "utf16be"
      ? new UTF16Encoder(true)
      : throw new Error("Unsupported encoding");
}

// logical expression
class Product {
  get id() {
    return this._id;
  }
  set id(value) {
    this._id = value || throw new Error("Invalid value");
  }
}

In the above code, throw appears in the expression.

Syntactically, the throw in the throw expression is no longer a command, but an operator. In order to avoid confusion with the throw command, it is stipulated that throw appears at the beginning of the line and is always interpreted as a throw statement, not a throw expression.

Partial execution of functions

Syntax

Multi-parameter functions sometimes need to bind one or more of the parameters, and then return a new function.

function add(x, y) {
  return x + y;
}
function add7(x) {
  return x + 7;
}

In the above code, the add7 function is actually a special version of the add function. By binding a parameter to 7, you can get add7 from add.

// bind method
const add7 = add.bind(null, 7);

// Arrow function
const add7 = (x) => add(x, 7);

The above two writings are somewhat redundant. Among them, the limitation of the bind method is more obvious. It must provide this, and can only bind parameters one by one from front to back, and cannot bind only non-head parameters.

There is now a proposal to make it easier to bind parameters and return a new function. This is called partial application of the function.

const add = (x, y) => x + y;
const addOne = add(1, ?);

const maxGreaterThanZero = Math.max(0, ...);

According to the new proposal, ? is a placeholder for a single parameter, and ... is a placeholder for multiple parameters. The following forms are all part of the execution of the function.

f(x, ?)
f(x, ...)
f(?, x)
f(..., x)
f(?, x, ?)
f(..., x, ...)

? and ... can only appear in function calls, and a new function will be returned.

const g = f(?, 1, ...);
// Equivalent to
const g = (x, ...y) => f(x, 1, ...y);

Part of the execution of a function can also be used for object methods.

let obj = {
  f(x, y) {
    return x + y;
  },
};

const g = obj.f(?, 3);
g(1); // 4

be careful

Part of the execution of the function has some special attention.

(1) Part of the execution of the function is based on the original function. If the original function changes, the new function generated by the partial execution will immediately reflect this change.

let f = (x, y) => x + y;

const g = f(?, 3);
g(1); // 4

// Replace function f
f = (x, y) => x * y;

g(1); // 3

In the above code, after the part that defines the function is executed, replacing the original function will immediately affect the new function.

(2) If the value provided in advance is an expression, then this expression will not be evaluated at the time of definition, but at each call.

let a = 3;
const f = (x, y) => x + y;

const g = f(?, a);
g(1); // 4

// change the value of a
a = 10;
g(1); // 11

In the above code, the pre-provided parameter is the variable a, so every time the function g is called, the value of a will be evaluated.

(3) If the new function has more parameters than the number of placeholders, the extra parameters will be ignored.

const f = (x, ...y) => [x, ...y];
const g = f(?, 1);
g(2, 3, 4); // [2, 1]

In the above code, the function g has only one placeholder, which means that it can only accept one parameter, and the extra parameters will be ignored.

Written as follows, there is no problem with extra parameters.

const f = (x, ...y) => [x, ...y];
const g = f(?, 1, ...);
g(2, 3, 4); // [2, 1, 3, 4];

(4) ... will only be collected once. If multiple ...s are used in part of the function, then the value of each ... will be the same.

const f = (...x) => x;
const g = f(..., 9, ...);
g(1, 2, 3); // [1, 2, 3, 9, 1, 2, 3]

In the above code, g defines two ... placeholders, and their values ​​are the same when they are actually executed.

Pipeline operator

The Unix operating system has a pipeline mechanism (pipeline) that can pass the value of the previous operation to the next operation. This mechanism is very useful, allowing simple operations to be combined into complex operations. Many languages ​​have pipeline implementations, and now there is a proposal to let JavaScript also have a pipeline mechanism.

The JavaScript pipeline is an operator, written as |>. It has an expression on the left and a function on the right. The pipeline operator transfers the value of the expression on the left to the function on the right for evaluation.

x |> f;
// Equivalent to
f(x);

The biggest advantage of the pipeline operator is that you can write nested functions as chained expressions from left to right.

function doubleSay(str) {
  return str + ", " + str;
}

function capitalize(str) {
  return str[0].toUpperCase() + str.substring(1);
}

function exclaim(str) {
  return str + "!";
}

Above are three simple functions. If you want to nest execution, the traditional way of writing and the way of writing pipeline are as follows.

// Traditional way of writing
exclaim(capitalize(doubleSay("hello")));
// "Hello, hello!"

// How to write the pipeline
"hello" |> doubleSay |> capitalize |> exclaim;
// "Hello, hello!"

The pipeline operator can only pass one value, which means that the function on its right must be a one-parameter function. If it is a multi-parameter function, it must be curried and changed to a single-parameter version.

function double(x) {
  return x + x;
}
function add(x, y) {
  return x + y;
}

let person = { score: 25 };
person.score |> double |> ((_) => add(7, _));
// 57

In the above code, the add function requires two parameters. However, the pipeline operator can only pass in one value, so you need to provide another parameter in advance and change it to the single-parameter arrow function _ => add(7, _). The underscore in this function has no special meaning and can be replaced by other symbols. The underscore is only used because it can visually indicate that it is a placeholder.

The pipeline operator also applies to the await function.

x |> (await f);
// Equivalent to
await f(x);

const userAge = userId |> (await fetchUserById) |> getAgeFromUser;
// Equivalent to
const userAge = getAgeFromUser(await fetchUserById(userId));

Numerical separator

In European and American languages, longer values ​​allow a separator (usually a comma) to be added every three digits to increase the readability of the value. For example, 1000 can be written as 1,000.

There is now a proposal that allows JavaScript values ​​to use underscore (_) as a separator.

let budget = 1_000_000_000_000;
budget === 10 ** 12; // true

JavaScript's numeric separator does not specify the number of digits for the interval, that is, you can add a separator every three digits, or you can add one every digit, every two digits, and every four digits.

123_00 === 12_300; // true

12345_00 === 123_4500; // true
12345_00 === 1_234_500; // true

Decimal and scientific notation can also use numeric separators.

// decimal
0.000_001;
// Scientific notation
1e10_000;

There are several points to note when using numeric separators.

-It cannot be at the leading or trailing of the value. -Two or more separators cannot be connected together. -There can be no separator before and after the decimal point. -In scientific notation, there can be no separator before and after the e or E that represents the exponent.

The following writing will report an error.

// All errors
3_.141
3._141
1_e12
1e_12
123__456
_1464301
1464301_

In addition to decimal, other hexadecimal values ​​can also use separators.

// Binary
0b1010_0001_1000_0101;
// Hexadecimal
0xa0_b0_c0;

Note that the separator cannot be followed by the prefix 0b, 0B, 0o, 0O, 0x, 0X.

// report an error
0_b111111000
0b_111111000

The following three functions to convert character strings into numeric values ​​do not support numeric separators. The main reason is that the designer of the proposal believes that the numeric separator is mainly for the convenience of writing numeric values ​​during encoding, not for processing externally input data.

-Number() -parseInt() -parseFloat()

Number("123_456"); // NaN
parseInt("123_456"); // 123

Math.signbit()

Math.sign() is used to determine the sign of a value, but if the parameter is -0, it will return -0.

Math.sign(-0); // -0

This leads to the fact that Math.sign() is not very useful for judging the sign bit. JavaScript internally uses 64-bit floating-point numbers (international standard IEEE 754) to represent values. IEEE 754 stipulates that the first bit is the sign bit, 0 means positive number, and 1 means negative number. So there are two types of zeros, +0 is the zero value when the sign bit is 0, and -0 is the zero value when the sign bit is 1. In actual programming, it is very troublesome to determine whether a value is +0 or -0, because they are equal.

+0 === -0; // true

Currently, there is a proposal that introduces the Math.signbit() method to determine whether the sign bit of a number is set.

Math.signbit(2); //false
Math.signbit(-2); //true
Math.signbit(0); //false
Math.signbit(-0); //true

As you can see, the method correctly returns the sign bit of -0 is set.

The algorithm of this method is as follows.

-If the parameter is NaN, return false -If the parameter is -0, return true -If the parameter is negative, return true -Return false in other cases

Double colon operator

Arrow functions can be bound to the this object, greatly reducing the way of explicitly binding the this object (call, apply, bind). However, arrow functions are not suitable for all occasions, so there is now a proposal that proposes the "function bind" operator, Used to replace call, apply, and bind calls.

The function binding operator is two colons side by side (::). The left side of the double colon is an object, and the right side is a function. This operator will automatically bind the object on the left as the context (ie the this object) to the function on the right.

foo::bar;
// Equivalent to
bar.bind(foo);

foo::bar(...arguments);
// Equivalent to
bar.apply(foo, arguments);

const hasOwnProperty = Object.prototype.hasOwnProperty;
function hasOwn(obj, key) {
  return obj::hasOwnProperty(key);
}

If the left side of the double colon is empty and the right side is a method of an object, it is equivalent to binding the method to the object.

var method = obj::obj.foo;
// Equivalent to
var method = ::obj.foo;

let log = ::console.log;
// Equivalent to
var log = console.log.bind(console);

If the result of the double colon operator is still an object, chain writing can be used.

import { map, takeWhile, forEach } from "iterlib";

getPlayers()
  ::map((x) => x.character())
  ::takeWhile((x) => x.strength > 100)
  ::forEach((x) => console.log(x));

Realm API

Realm API provides a sandbox function (sandbox), allowing code to be isolated and preventing those isolated codes from getting global objects.

In the past, <iframe> was often used as a sandbox.

const globalOne = window;
let iframe = document.createElement("iframe");
document.body.appendChild(iframe);
const globalTwo = iframe.contentWindow;

In the above code, the global object of <iframe> is independent (iframe.contentWindow). Realm API can replace this function.

const globalOne = window;
const globalTwo = new Realm().global;

In the above code, Realm API provides a global object new Realm().global separately.

The Realm API provides a Realm() constructor to generate a Realm object. The object's global property points to a new top-level object, which is similar to the original top-level object.

const globalOne = window;
const globalTwo = new Realm().global;

globalOne.evaluate("1 + 2"); // 3
globalTwo.evaluate("1 + 2"); // 3

In the above code, the evaluate() method of the top-level object generated by Realm can run the code.

The following code can prove that the Realm top-level object and the original top-level object are two objects.

let a1 = globalOne.evaluate("[1,2,3]");
let a2 = globalTwo.evaluate("[1,2,3]");
a1.prototype === a2.prototype; // false
a1 instanceof globalTwo.Array; // false
a2 instanceof globalOne.Array; // false

In the above code, the prototype object of the array in the Realm sandbox is different from the array in the original environment.

Only the API provided by ECMAScript syntax can be run in the Realm sandbox, but the API provided by the host environment cannot be run.

globalTwo.evaluate("console.log(1)");
// throw an error: console is undefined

In the above code, there is no console object in the Realm sandbox, resulting in an error. Because console is not a grammatical standard, it is provided by the host environment.

If you want to solve this problem, you can use the following code.

globalTwo.console = globalOne.console;

The Realm() constructor can accept a parameter object, and the intrinsics property of the parameter object can specify the method that the Realm sandbox inherits from the original top-level object.

const r1 = new Realm();
r1.global === this;
r1.global.JSON === JSON; // false

const r2 = new Realm({ intrinsics: "inherit" });
r2.global === this; // false
r2.global.JSON === JSON; // true

In the above code, under normal circumstances, the JSON method of the sandbox is different from the original JSON object. However, after the Realm() constructor accepts { intrinsics:'inherit' } as a parameter, it will inherit the methods of the original top-level object.

Users can define their own subclasses of Realm to customize their own sandbox.

class FakeWindow extends Realm {
  init() {
    super.init();
    let global = this.global;

    global.document = new FakeDocument(...);
    global.alert = new Proxy(fakeAlert, {... });
    // ...
  }
}

In the above code, FakeWindow simulates a fake top-level object window.

#! Command

Unix command line scripts all support the #! command, also known as Shebang or Hashbang. This command is placed on the first line of the script to specify the executor of the script.

For example, the first line of a Bash script.

#!/bin/sh

The first line of the Python script.

#!/usr/bin/env python

There is now a proposal that introduces the #! command for JavaScript scripts, which is written in the first line of the script file or module file.

// Write in the first line of the script file
#!/usr/bin/env node
'use strict';
console.log(1);

// Write in the first line of the module file
#!/usr/bin/env node
export {};
console.log(1);

With this line, the Unix command line can directly execute the script.

# The way the script was executed before
$ node hello.js

# hashbang way
$ ./hello.js

For JavaScript engines, #! will be interpreted as a comment, and this line will be ignored.

import.meta

When developers use a module, they sometimes need to know some information about the template itself (such as the path of the module). There is now a proposal that adds a meta attribute import.meta to the import command to return the meta information of the current module.

import.meta can only be used inside the module, and an error will be reported if it is used outside the module.

This property returns an object whose various properties are the meta information of the currently running script. The specific attributes are not specified in the standard, and are determined by each operating environment. Generally speaking, import.meta has at least the following two attributes.

(1) import.meta.url

import.meta.url returns the URL path of the current module. For example, if the path of the main file of the current module is https://foo.com/main.js, import.meta.url will return this path. If there is a data file data.txt in the module, then you can use the following code to get the path of this data file.

new URL("data.txt", import.meta.url);

Note that in the Node.js environment, import.meta.url always returns the local path, which is the string of the file:URL protocol, such as file:///home/user/foo.js.

(2) import.meta.scriptElement

import.meta.scriptElement is a browser-specific meta attribute, which returns the <script> element of the loaded module, which is equivalent to the document.currentScript attribute.

// HTML code is
// <script type="module" src="my-module.js" data-foo="abc"></script>

// execute the following code inside my-module.js
import.meta.scriptElement.dataset.foo;
// "abc"