Website development
JavaScript
Programming
31 January 2019

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

Стандартизация 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() — прототипный метод обновлён #.


•      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&nbsp;unexpected token
 
 


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


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


поддержка браузерами

Поддержка браузерами примитива 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


ECMAScript


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


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


Шебанг


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


Статья на Хабре Путь JavaScript модуля


Материал на английском Почему не private x


Статья на Хабре ECMAScript Proposal: Array.prototype.{flat,flatMap}


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


JavaScript: Большое целое Ну почему




UPD (март):


Сменили статус на Stage-4:


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


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


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

Only registered users can participate in poll.Log in, please.
Самая ожидаемая фича:
30.58% catch — необязательный аргумент 74
0.83% Symbol().description 2
47.93% # — приватное всё у классов, через октоторп 116
7.44% #!/usr/bin/env node — шебанг грамматика для скриптов 18
29.75% BigInt() 72
16.53% globalThis 40
52.48% import(dynamic) 127
6.2% import.meta 15
16.12% Object.fromEntries() 39
15.29% .trimStart() и .trimEnd() 37
30.99% flat() и .flatMap() 75
1.24% другое. Напишу в комментариях 3
242 users voted. 63 users abstained.

+31
28.7k 92
Comments 61