Стандартизация JS перешла на годичный цикл обновлений, а начало года — отличное время для того чтобы узнать, что нас ждёт в юбилейной — уже десятой редакции EcmaScript!
ES9 — актуальная версия спецификации.
ES10 — всё ещё черновик.
На сегодняшний день в Stage 4 # — всего несколько предложений.
А в Stage 3 # — целая дюжина!
Из них, на мой взгляд, самые интересные — приватные поля классов #, шебанг грамматика для скриптов #, числа произвольной точности #, доступ к глобальному контексту # и динамические импорты #.

Автор фото: kasper.green; Жёлтый магнит: elfafeya.art & kasper.green
Содержание
Пять стадий #
Stage 4 — Final #
• catch — аргумент стал необязательным #;
• Symbol().description — акцессор к описанию символа #;
• 'строки EcmaScript' — улучшенная совместимость с JSON форматом #;
• .toString() — прототипный метод обновлён #.
• Object.fromEntries() — создание объекта из массива пар — ключ\значение #;
• .flat() и .flatMap() — прототипные методы массивов #.
Stage 3 — Pre-release #
• # — приватное всё у классов, через октоторп #;
• #!/usr/bin/env node — шебанг грамматика для скриптов #;
• BigInt() — новый примитив, для чисел произвольной точности #;
• globalThis — новый способ доступа к глобальному контексту #;
• import(dynamic) — динамический импорт #;
• import.meta — мета-информация о загружаемом модуле #;
• JSON.stringify() — фикс метода #;
• RegExp — устаревшие возможности #;
• .trimStart() и .trimEnd() — прототипные методы строк #;
• .matchAll() — .match() с глобальным флагом #;
Итоги #
Пять стадий
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; } }

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

Мой 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",
он ведёт себя так, словно мы сделали перенос строки — прямо посреди кода:

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

Доработка прототипного метода .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"
Создание объекта методом Object.fromEntries()
https://github.com/tc39/proposal-object-from-entries
работает в Chrome
Аналог _.fromPairs из lodash:
Object.fromEntries([['key_1', 1], ['key_2', 2]]) // {key_1: 1; key_2: 2}
Одномерные массивы с .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() эквивалентен последовательному вызову .map().flat(). Функция обратного вызова, передаваемая в метод, должна возвращать массив который станет частью общего плоского массива:
['Hello', 'World'].flatMap(word => [...word]) // ["H", "e", "l", "l", "o", "W", "o", "r", "l", "d"]
С использованием только .flat() и .map(), пример можно переписать так:
['Hello', 'World'].map(word => [...word]).flat() // ["H", "e", "l", "l", "o", "W", "o", "r", "l", "d"]
Также нужно учитывать, что у .flatMap() в отличии от .flat() нет настроек глубины обхода. А значит склеен будет только первый уровень.
֍ 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()."\r\n"; } 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 unexpected token
Большие числа с BigInt
https://github.com/tc39/proposal-bigint

Максимальное целое число, которое можно безопасно использовать в 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

Хотелось переменные в строках импорта‽ С динамическими импортами это стало возможно:
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" }
Фикс метода 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)
Так строка \uDF06\uD834 после обработки JSON.stringify() превращается в \\udf06\\ud834:
/* Непарные суррогатные единицы будут сериализованы с экранированием последовательностей */ JSON.stringify('\uDF06\uD834') '"\\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!
Итоги
Stage 4 привнёс скорее косметические изменения. Интерес представляет Stage 3. Большинство из предложений в Chrome уже реализованы, а свойства у объектов — очень ждём.
Исправления в статье
Если заметил в статье неточность, ошибку или есть чем дополнить — ты можешь написать мне личное сообщение, а лучше самому воспользоваться репозиторием статьи https://github.com/KasperGreen/es10. За активный вклад, награжу жёлтым магнитом-медалью с КДПВ.
Материалы по теме
Актуальная версия стандарта Ecma-262
Черновик следующей версии стандарта Ecma-262
Новые #приватные поля классов в JavaScript
Обзор возможностей стандартов ES7, ES8 и ES9
BigInt — длинная арифметика в JavaScript
ECMAScript Proposal: Array.prototype.{flat,flatMap}
Публичные и приватные поля классов
JavaScript: Большое целое Ну почему
UPD (март):
Сменили статус на Stage-4:
• .trimStart() и .trimEnd() — прототипные методы строк #;
• .matchAll() — .match() с глобальным флагом #;

Автор фото: kasper.green; Жёлтый магнит: elfafeya.art & kasper.green
