EcmaScript 10 — JavaScript в этом году

в 17:54, , рубрики: #, BigInt, ECMAScript, ecmascript 10, globalThis, import.meta, import(dynamic), javascript, json, toString, ооп, Программирование, Разработка веб-сайтов

Стандартизация JS перешла на годичный цикл обновлений, а начало года — отличное время для того чтобы узнать, что нас ждёт в юбилейной — уже десятой редакции EcmaScript!

ES9 — актуальная версия спецификации.

ES10 — всё ещё черновик.

На сегодняшний день в Stage 4 # — всего несколько предложений.

А в Stage 3 # — целая дюжина!

Из них, на мой взгляд, самые интересные — приватные поля классов #, шебанг грамматика для скриптов #, числа произвольной точности #, доступ к глобальному контексту # и динамические импорты #.

 
КДПВ: Жёлтый магнит с надписью «JS ES10» на экране монитора —  от kasper.green & elfafeya.art
        Автор фото: kasper.green; Жёлтый магнит: elfafeya.art & kasper.green

Содержание

Пять стадий #

Stage 4 — Final #

•      catch — аргумент стал необязательным #;

•      Symbol().description — акцессор к описанию символа #;

•      'строки EcmaScript' — улучшенная совместимость с JSON форматом #;

•      .toString() — прототипный метод обновлён #.


Stage 3 — Pre-release #

•      # — приватное всё у классов, через октоторп #;

•      #!/usr/bin/env node — шебанг грамматика для скриптов #;

•      BigInt() — новый примитив, для чисел произвольной точности #;

•      globalThis — новый способ доступа к глобальному контексту #;

•      import(dynamic) — динамический импорт #;

•      import.meta — мета-информация о загружаемом модуле #;

•      Object.fromEntries() — создание объекта из массива пар — ключзначение #;

•      JSON.stringify() — фикс метода #;

•      RegExp — устаревшие возможности #;

•      .trimStart() и .trimEnd() — прототипные методы строк #;

•      .matchAll().match() с глобальным флагом #;

•      .flat() и .flatMap() — прототипные методы массивов #.

Итоги #


Пять стадий

   Stage 0   ↓   Strawman  Наметка           Идея, которую можно реализовать через Babel-плагин.;

   Stage 1   ↓   Proposal  Предложение     Проверка жизнеспособности идеи.;

   Stage 2   ↓   Draft  Черновик                  Начало разработки спецификации.;

   Stage 3   ↓   Candidate  Кандидат         Предварительная версия спецификации.;

   Stage 4  ֍  Finished  Завершён           Финальная версия спецификации на этот год.


Мы рассмотрим только Stage 4 — де-факто, вошедший в стандарт.

И Stage 3 — который вот-вот станет его частью.


 

֍ Stage 4

Эти изменения уже вошли в стандарт.

Необязательный аргумент у catch

https://github.com/tc39/proposal-optional-catch-binding

До ES10 блок catch требовал обязательного аргумента для сбора информации об ошибке, даже если она не используется:

function isValidJSON(text) {
  try {
    JSON.parse(text);
    return true;
  } catch(unusedVariable) { // переменная не используется
    return false;
  }
}

EcmaScript 10 — JavaScript в этом году - 2
Edge пока не обновлён до ES10, и ожидаемо валится с ошибкой

Начиная с редакции ES10, круглые скобки можно опустить и catch станет как две капли воды похож на try.

EcmaScript 10 — JavaScript в этом году - 3
Мой Chrome уже обновился до ES10, а местами и до Stage 3. Дальше скриншоты будут из Chrome

исходный код

function isValidJSON(text) {
  try {
    JSON.parse(text);
    return true;
  } catch { // без аргумента
    return false;
  }
}

 
 

Доступ к описанию символьной ссылки

https://tc39.github.io/proposal-Symbol-description/

Описание символьной ссылки можно косвенно получить методом toString():

const symbol_link = Symbol("Symbol description")
String(symbol_link) // "Symbol(Symbol description)"

Начиная с ES10 у символов появилось свойство description, доступное только для чтения. Оно позволяет без всяких танцев с бубном получить описание символа:

symbol_link.description
// "Symbol description"

В случае если описание не задано, вернётся — undefined:

const without_description_symbol_link = Symbol()
without_description_symbol_link.description
// undefined

const empty_description_symbol_link = Symbol('')
empty_description_symbol_link.description
// ""

 
 

Строки EcmaScript совместимые с JSON

https://github.com/tc39/proposal-json-superset

EcmaScript до десятой редакции утверждает, что JSON является подмножеством JSON.parse, но это неверно.

JSON строки могут содержать неэкранированные символы разделителей линий U+2028 LINE SEPARATOR и абзацев U+2029 PARAGRAPH SEPARATOR.

Строки ECMAScript до десятой версии — нет.

Если в Edge вызвать eval() со строкой "u2029",
он ведёт себя так, словно мы сделали перенос строки — прямо посреди кода:

EcmaScript 10 — JavaScript в этом году - 4

 

C ES10 строками — всё в порядке:

EcmaScript 10 — JavaScript в этом году - 5

 
 

Доработка прототипного метода .toString()

http://tc39.github.io/Function-prototype-toString-revision/

Цели изменений

  • убрать обратно несовместимое требование:

Если реализация не может создать строку исходного кода, соответствующую этим критериям, она должна вернуть строку, для которой eval будет выброшено исключение с ошибкой синтаксиса.

  • уточнить «функционально эквивалентное» требование;

  • стандартизировать строковое представление встроенных функций и хост-объектов;

  • уточнить требования к представлению на основе «фактических характеристик» объекта;

  • убедиться, что синтаксический анализ строки содержит то же тело функции и список параметров, что и оригинал;

  • для функций, определенных с использованием кода ECMAScript, toString должен возвращать фрагмент исходного текста от начала первого токена до конца последнего токена, соответствующего соответствующей грамматической конструкции;

  • для встроенных функциональных объектов toStringне должны возвращать ничего, кроме NativeFunction;

  • для вызываемых объектов, которые не были определены с использованием кода ECMAScript, toString необходимо вернуть NativeFunction;

  • для функций, создаваемых динамически (конструкторы функции или генератора) toString, должен синтезировать исходный текст;

  • для всех других объектов, toString должен бросить TypeError исключение.

// Пользовательская функция
function () { console.log('My Function!'); }.toString();
// function () { console.log('My Function!'); }

// Метод встроенного объекта объект
Number.parseInt.toString();
// function parseInt() { [native code] }

// Функция с привязкой контекста
function () { }.bind(0).toString();
// function () { [native code] }

// Встроенные вызываемые функциональный объекты
Symbol.toString();
// function Symbol() { [native code] }

// Динамически создаваемый функциональный объект
Function().toString();
// function anonymous() {}

// Динамически создаваемый функциональный объект-генератор
function* () { }.toString();
// function* () { }

// .call теперь обязательно ждёт, в качестве аргумента, функцию
Function.prototype.toString.call({});
// Function.prototype.toString requires that 'this' be a Function"


 
 

֍ Stage 3

Предложения вышедшие из статуса черновика, но ещё не вошедшие в финальную версию стандарта. 

Приватныестатическиепубличные методысвойстваатрибуты у классов

https://github.com/tc39/proposal-class-fields
https://github.com/tc39/proposal-private-methods
https://github.com/tc39/proposal-static-class-features

В некоторых языках есть договорённость, называть приватные методы через видимый пробел ( «_» — такая_штука, ты можешь знать этот знак под неверным названием — нижнее подчёркивание).

Например так:

<?php
class AdultContent {
    private $_age = 0;
    private $_content = '…is dummy example content (•)(•) —3 (.)(.) only for adults…';
    function __construct($age) {
        $this->_age = $age;
    }
    function __get($name) {
        if($name === 'content') {
            return " (age: ".$this->_age.") → ".$this->_getContent()."rn";
        }
        else {
            return 'without info';
        }
    }
    private function _getContent() {
        if($this->_contentIsAllowed()) {
            return $this->_content;
        }
        return 'Sorry. Content not for you.';
    }
    private function _contentIsAllowed() {
        return $this->_age >= 18;
    }
    function __toString() {
        return $this->content;
    }
}
echo "<pre>";

echo strval(new AdultContent(10));
//  (age: 10) →  Sorry. Content not for you

echo strval(new AdultContent(25));
// (age: 25) →  …is dummy example content (•)(•) —3 only for adults…

$ObjectAdultContent = new AdultContent(32);
echo $ObjectAdultContent->content;
// (age: 32) →  …is dummy example content (•)(•) —3 only for adults…
?>

Напомню — это только договорённость. Ничто не мешает использовать префикс для других целей, использовать другой префикс, или не использовать вовсе.

Лично мне импонирует идея использовать видмый пробел в качестве префикса для функций, возвращающих this. Так их можно объединять в цепочку вызовов.

Разработчики спецификации EcmaScript пошли дальше и сделали префикс-октоторп ( «#» —решётка, хеш ) частью синтаксиса.

Предыдущий пример на ES10 можно переписать следующим образом:

export default class AdultContent {

  // Приватные атрибуты класса
  #age = 0
  #adult_content = '…is dummy example content (•)(•) —3 (.)(.) only for adults…'

  constructor(age) {
    this.#setAge(age)
  }

  // Статический приватный метод
  static #userIsAdult(age) {
    return age > 18
  }

  // Публичное свойство
  get content () {
    return `(age: ${this.#age}) → ` + this.#allowed_content
  }

  // Приватное свойство
  get #allowed_content() {
      if(AdultContent.userIsAdult(this.age)){
        return this.#adult_content
    }
    else {
        return 'Sorry. Content not for you.'
    }
  }

  // Приватный метод
  #setAge(age) {
      this.#age = age
  }

  toString () {
    return this.#content
  }
}

const AdultContentForKid = new AdultContent(10)

console.log(String(AdultContentForKid))
// (age: 10) → Sorry. Content not for you.

console.log(AdultContentForKid.content)
// (age: 10) → Sorry. Content not for you.

const AdultContentForAdult = new AdultContent(25)

console.log(String(AdultContentForAdult))
// (age: 25) → …is dummy example content (•)(•) —3 (.)(.) only for adults…

console.log(AdultContentForAdult.content)
// (age: 25) → …is dummy example content (•)(•) —3 (.)(.) only for adults…

Пример излишне усложнён для демонстрации приватных свойств, методов и атрибутов разом. Но в целом JS — радует глаз своей лаконичностью по сравнению с PHP вариантом. Никаких тебе private function _..., ни точек с запятой в конце строки, и точка вместо «->» для перехода вглубь объекта.

Геттеры именованные. Для динамических имён — прокси-объекты.

Вроде бы мелочи, но после перехода на JS, всё меньше желания возвращаться к PHP.

К слову приватные акцессоры доступны только с Babel 7.3.0 и старше.

Крайняя версия на npmjs.com — 7.2.2

Ждём в Stage 4!

 
 

Шебанг грамматика

https://github.com/tc39/proposal-hashbang

Хешбэнг — знакомый юниксойдам способ указать интерпретатор для исполняемого файла:

#!/usr/bin/env node
// в скрипте
'use strict';
console.log(1);

#!/usr/bin/env node
// в модуле
export {};
console.log(1);

в данный момент, на подобный фортель, Chrome выбрасывает SyntaxError: Invalid or&nbsp;unexpected token
 
 

Большие числа с BigInt

https://github.com/tc39/proposal-bigint

работает в Chrome

Максимальное целое число, которое можно безопасно использовать в JavaScript (2⁵³ — 1):

console.log(Number.MAX_SAFE_INTEGER)
// 9007199254740991

BigInt нужен для использования чисел произвольной точности.

Объявляется этот тип несколькими способами:

// используя 'n' постфикс в конце более длинных чисел
910000000000000100500n
// 910000000000000100500n

// напрямую передав в конструктор примитива BigInt() без постфикса
BigInt( 910000000000000200500 )
// 910000000000000200500n

// или передав строку в тот-же конструктор
BigInt( "910000000000000300500" )
// 910000000000000300500n

// пример очень большого числа длиной 1642 знака
BigInt( "9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999" )
\ 9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999n

Это новый примитивный тип:

typeof 123;
// → 'number'
typeof 123n;
// → 'bigint'

Его можно сравнивать с обычными числами:

42n === BigInt(42);
// → true
42n == 42;
// → true

Но математические операции нужно проводить в пределах одного типа:

20000000000000n/20n
// 1000000000000n

20000000000000n/20
// Uncaught TypeError: Cannot mix BigInt and other types, use explicit conversions

Поддерживается унарный минус, унарный плюс возвращает ошибку:

 -2n
 // -2n

 +2n
 // Uncaught TypeError: Cannot convert a BigInt value to a number

 

globalThis — новый способ доступа к глобальному контексту

https://github.com/tc39/proposal-global

работает в Chrome

Поскольку реализации глобальной области видимости зависят от конкретного движка, раньше приходилось делать что-то вроде этого:

var getGlobal = function () {
    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');
};

И даже такой вариант не гарантировал, что всё точно будет работать.

globalThis — общий для всех платформ способ доступа к глобальной области видимости:

// Обращение к глобальному конструктору массива
globalThis.Array(1,2,3)
// [1, 2, 3]

// Запись собственных данных в глобальную область видимости
globalThis.myGLobalSettings = {
    it_is_cool: true
}

// Чтение собственных данных из глобальной области видимости
globalThis.myGLobalSettings
// {it_is_cool: true}

 
 

Динамический import(dynamic)

https://github.com/tc39/proposal-dynamic-import

работает в Chrome

Хотелось переменные в строках импорта‽ С динамическими импортами это стало возможно:

import(`./language-packs/${navigator.language}.js`)

Динамический импорт — асинхронная операция. Возвращает промис, который после загрузки модуля возвращает его в функцию обратного вызова.

Поэтому загружать модули можно — отложенно, когда это необходимо:

element.addEventListener('click', async () => {
    // можно использовать await синтаксис для промиса
    const module = await import(`./events_scripts/supperButtonClickEvent.js`)
    module.clickEvent()
})

Синтаксически, это выглядит как вызов функции import(), но не наследуется от Function.prototype, а значит вызвать через call или apply — не удастся:

import.call("example this", "argument")
// Uncaught SyntaxError: Unexpected identifier

 
 

import.meta — мета-информация о загружаемом модуле.

https://github.com/tc39/proposal-import-meta

работает в Chrome

В коде загружаемого модуля стало возможно получить информацию по нему. Сейчас это только адрес по которому модуль был загружен:

console.log(import.meta);
// { url: "file:///home/user/my-module.js" }

 
 

Создание объекта методом Object.fromEntries()

https://github.com/tc39/proposal-object-from-entries

Аналог _.fromPairs из lodash:

Object.fromPairs([['key_1', 1], ['key_2', 2]])
// {key_1: 1; key_2: 2}

 
 

Фикс метода JSON.stringify()

https://github.com/tc39/proposal-well-formed-stringify

В разделе 8.1 RFC 8259 требуется, чтобы текст JSON, обмениваемый за пределами замкнутой экосистемы, кодировался с использованием UTF-8, но JSON.stringify может возвращать строки, содержащие кодовые точки, которые не представлены в UTF-8 (в частности, суррогатные кодовые точки от U+D800 до U+DFFF)

Так строка uDF06uD834 после обработки JSON.stringify() превращается в \udf06\ud834:

/* Непарные суррогатные единицы будут сериализованы с экранированием последовательностей */
JSON.stringify('uDF06uD834')
'"\udf06\ud834"'
JSON.stringify('uDEAD')
'"\udead"'

Такого быть не должно, и новая спецификация это исправляет. Edge и Chrome уже обновились.

 
 

Устаревшие возможности RegExp

https://github.com/tc39/proposal-regexp-legacy-features

Спецификация для устаревших функций RegExp, вроде RegExp.$1, и RegExp.prototype.compile() метода.

 
 

Прототипные методы строк .trimStart() и .trimEnd()

https://github.com/tc39/proposal-string-left-right-trim

работает в Chrome

По аналогии с методами .padStart() и .padEnd(), обрезают пробельные символы в начале и конце строки соответственно:

const one = "      hello and let ";
const two = "us begin.        ";
console.log( one.trimStart() + two.trimEnd() )
// "hello and let us begin."

 
 

.matchAll() — новый прототипный метод строк.

https://github.com/tc39/proposal-string-matchall

работает в Chrome

Работает похоже на метод .match() с включенным флагом g, но возвращает итератор:

const string_for_searh = 'olololo'

// Вернёт первое вхождение с дополнительной информацией о нём
string_for_searh.match(/o/)
// ["o", index: 0, input: "olololo", groups: undefined]

//Вернёт массив всех вхождений без дополнительной информации
string_for_searh.match(/o/g)
// ["o", "o", "o", "o"]

// Вернёт итератор
string_for_searh.matchAll(/o/)
// {_r: /o/g, _s: "olololo"}

// Итератор возвращает каждое последующее вхождение с подробной информацией,
// как если бы мы использовали .match без глобального флага
for(const item of string_for_searh.matchAll(/o/)) {
  console.log(item)
}
// ["o", index: 0, input: "olololo", groups: undefined]
// ["o", index: 2, input: "olololo", groups: undefined]
// ["o", index: 4, input: "olololo", groups: undefined]
// ["o", index: 6, input: "olololo", groups: undefined]

Аргумент должен быть регулярным выражением, иначе будет выброшено исключение:

'olololo'.matchAll('o')
// Uncaught TypeError: o is not a regexp!

 
 

Одномерные массивы с .flat() и .flatMap()

https://github.com/tc39/proposal-flatMap

работает в Chrome

Массив обзавёлся прототипами .flat() и .flatMap(), которые в целом похожи на реализации в lodash,
но всё же имеют некоторые отличия. Необязательный аргумент — устанавливает максимальную глубину обхода дерева:

const deep_deep_array = [
  '≥0 — первый уровень',
  [
    '≥1 — второй уровень',
    [
      '≥2 — третий уровень',
      [
        '≥3 — четвёртый уровень',
        [
          '≥4 — пятый уровень'
        ]
      ]
    ]
  ]
]

// 0 — вернёт массив без изменений
deep_deep_array.flat(0)
//  ["≥0 — первый уровень", Array(2)]

// 1 — глубина по умолчанию
deep_deep_array.flat()
//  ["первый уровень", "второй уровень", Array(2)]

deep_deep_array.flat(2)
//  ["первый уровень", "второй уровень", "третий уровень", Array(2)]

deep_deep_array.flat(100500)
// ["первый уровень", "второй уровень", "третий уровень", "четвёртый уровень", "пятый уровень"]

.flatMap() не эквивалентен последовательному вызову .flat().map(). Функция обратного вызова, передаваемая в метод, должна возвращать массив который станет частью общего плоского массива:

['Hello', 'World'].flatMap(word => [...word])
// ["H", "e", "l", "l", "o", "W", "o", "r", "l", "d"]

 


 
 

Итоги

Stage 4 привнёс скорее косметические изменения. Интерес представляет Stage 3. Большинство из предложений в Chrome уже реализованы, за исключением пожалуй Object.fromEntries(), наличие которого не критично, а приватные свойства очень ждём.

 


 

Исправления в статье

 
Если заметил в статье неточность, ошибку или есть чем дополнить — ты можешь написать мне личное сообщение, а лучше самому воспользоваться репозиторием статьи https://github.com/KasperGreen/es10

Материалы по теме

Актуальная версия стандарта Ecma-262

Черновик следующей версии стандарта Ecma-262

ECMAScript

Новые #приватные поля классов в JavaScript

Обзор возможностей стандартов ES7, ES8 и ES9

Шебанг

BigInt — длинная арифметика в JavaScript


 
 

Альтернативный вариант КДПВ с жёлтым магнитом от elfafeya.art
       Автор фото: kasper.green; Жёлтый магнит: elfafeya.art & kasper.green

Автор: KasperGreen

Источник

* - обязательные к заполнению поля