Новшества JavaScript: итоги Google I/O 2019. Часть 2 +45


Сегодня мы публикуем вторую часть перевода материала о новшествах JavaScript. Здесь мы поговорим о разделителях разрядов чисел, о BigInt-числах, о работе с массивами и объектами, о globalThis, о сортировке, об API интернационализации и о промисах.



> Первая часть

Разделители разрядов чисел


Длинные числа, которые встречаются в программах, тяжело читать. Например — 1000000000 — это один миллиард в десятичной системе счисления. Но с одного взгляда понять это тяжело. Поэтому, если читатель программы встретит в коде нечто подобное — ему, чтобы правильно это воспринять, придётся внимательно считать нули.

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

var billion = 1_000_000_000;
console.log( billion ); // 1000000000

Разделители можно использовать для произвольного деления чисел на фрагменты. JavaScript, при работе с числами, просто игнорирует разделители. Их можно применять при записи любых чисел: целых, с плавающей точкой, двоичных, шестнадцатеричных, восьмеричных.

console.log( 1_000_000_000.11 ); // 1000000000.11
console.log( 1_000_000_000.1_012 ); // 1000000000.1012
console.log( 0xFF_00_FF ); // 16711935
console.log( 0b1001_0011 ); // 147
console.log( 0o11_17 ); // 591

> Поддержка


  • TC39: Stage 3
  • Chrome: 75+
  • Node: 12.5+

Тип данных bigint


Числа в JavaScript создаются с использованием функции-конструктора Number.

Максимальное значение, которое можно безопасно представить с помощью типа данных Number, представляет собой (2?? — 1), то есть — 9007199254740991. Увидеть это число можно, воспользовавшись конструкцией Number.MAX_SAFE_INTEGER.

Обратите внимание на то, что, когда в JS-коде используют числовой литерал, JavaScript обрабатывает его, создавая на его основе объект с помощью конструктора Number. Прототип данного объекта содержит методы работы с числами. Это происходит со всеми примитивными типами данных.

Что произойдёт в том случае, если мы попытаемся прибавить что-нибудь к числу 9007199254740991?

console.log( Number.MAX_SAFE_INTEGER ); // 9007199254740991
console.log( Number.MAX_SAFE_INTEGER + 10 ); // 9007199254741000

Результат сложения Number.MAX_SAFE_INTEGER и числа 10, выводимый вторым вызовом console.log(), неверен. Происходит это из-за того, что JS не может правильно выполнять вычисления с числами, превышающими значение Number.MAX_SAFE_INTEGER. Справиться с этой проблемой можно, воспользовавшись типом данных bigint.

Тип bigint позволяет представлять целые числа, которые больше, чем Number.MAX_SAFE_INTEGER. Работа с BigInt-значениями похожа на работу со значениями типа Number. В частности, в языке имеется функция BigInt(), с помощью которой можно создавать соответствующие значения, и встроенный примитивный тип данных bigint, используемый для представления больших целых чисел.

var large = BigInt( 9007199254740991 );
console.log( large ); // 9007199254740991n
console.log( typeof large ); // bigint

JavaScript добавляет n в конец BigInt-литералов. Для нас это означает то, что такие литералы можно записывать, добавляя n в конец целых чисел.

Теперь, когда в нашем распоряжении имеются BigInt-числа, мы можем безопасно производить математические операции на больших числах, имеющих тип bigint.

var large = 9007199254740991n;
console.log( large + 10n ); // 9007199254741001n

Число типа number — это не то же самое, что число типа bigint. В частности, речь идёт о том, что BigInt-числа могут быть только целыми. В результате оказывается, что нельзя выполнять арифметические операции, в которых используются типы bigint и number.

Надо отметить, что функция BigInt() может принимать различные числа: десятичные, двоичные, шестнадцатеричные, восьмеричные. Внутри этой функции они будут преобразованы к числам, для представления которых используется десятичная система счисления.

Тип bigint также поддерживает разделители разрядов:

var large = 9_007_199_254_741_001n;
console.log( large ); // 9007199254741001n

> Поддержка


  • TC39: Stage 3
  • Chrome: 67+
  • Node: 10.4+
  • Firefox: 68+

Новые методы массивов: .flat() и .flatMap()


Здесь мы поговорим о новых методах прототипа объекта Array — о методах .flat() и .flatMap().

?Метод .flat()


Теперь у объектов типа Array имеется новый метод — .flat(n). Он возвращает новый массив, позволяя рекурсивно поднимать элементы массивов на указанный уровень n. По умолчанию n равно 1. Этому методу можно передать n, равное Infinity, что позволяет преобразовывать массив со вложенными массивами в одномерный массив.

var nums = [1, [2, [3, [4, 5]]]];
console.log( nums.flat() ); // [1, 2, [3, [4,5]]]
console.log( nums.flat(2) ); // [1, 2, 3, [4,5]]
console.log( nums.flat(Infinity) ); // [1, 2, 3, 4, 5]

> Поддержка


  • TC39: Stage 4
  • Chrome: 69+
  • Node: 11+
  • Firefox: 62+

?Метод .flatMap()


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

var nums = [1, 2, 3];
var squares = nums.map( n => [ n, n*n ] )
console.log( squares ); // [[1,1],[2,4],[3,9]]
console.log( squares.flat() ); // [1, 1, 2, 4, 3, 9]

Решение этой задачи можно упростить, воспользовавшись методом .flatMap(). Он преобразует массивы, возвращаемые переданной ему функцией обратного вызова, так, как преобразовывал бы их метод .flat() с параметром n, равным 1.

var nums = [1, 2, 3];
var makeSquare = n => [ n, n*n ];
console.log( nums.flatMap( makeSquare ) ); // [1, 1, 2, 4, 3, 9]

> Поддержка


  • TC39: Stage 4
  • Chrome: 69+
  • Node: 11+
  • Firefox: 62+

?Метод Object.fromEntries()


Извлечь из объекта пары вида ключ:значение можно, воспользовавшись статическим методом Object, который возвращает массив, каждый элемент которого является массивом, содержащим, в качестве первого элемента, ключ, а в качестве второго — значение.

var obj = { x: 1, y: 2, z: 3 };
var objEntries = Object.entries( obj );
console.log( objEntries ); // [["x", 1],["y", 2],["z", 3]]

Теперь в нашем распоряжении имеется статический метод Object.fromEntries(), который позволяет преобразовать подобную структуру обратно в объект.

var entries = [["x", 1],["y", 2],["z", 3]];
var obj = Object.fromEntries( entries );
console.log( obj ); // {x: 1, y: 2, z: 3}

Метод entries() использовался для облегчения фильтрации и маппинга данных, хранящихся в объектах. В результате получался массив. Но до сих пор у задачи преобразования подобного массива в объект не было красивого решения. Именно для решения этой задачи и можно использовать метод Object.fromEntries().

var obj = { x: 1, y: 2, z: 3 };
// [["x", 1],["y", 2],["z", 3]]
var objEntries = Object.entries( obj );
// [["x", 1],["z", 3]]
var filtered = objEntries.filter(
 ( [key, value] ) => value % 2 !== 0 // выбираем значения, которые являются нечётными числами
);
console.log( Object.fromEntries( filtered ) ); // {x: 1, z: 3}

Если для хранения пар ключ:значение используется структура данных Map, то данные в ней хранятся в порядке их добавления в неё. При этом то, как хранятся данные, напоминает массив, возвращаемый методом Object.entries(). Метод Object.fromEntries() легко использовать и для преобразования в объект структур данных Map.

var m = new Map([["x", 1],["y", 2],["z", 3]]);
console.log( m ); // {"x" => 1, "y" => 2, "z" => 3}
console.log( Object.fromEntries( m ) ); // {x: 1, y: 2, z: 3}

> Поддержка


  • TC39: Stage 4
  • Chrome: 73+
  • Node: 12+
  • Firefox: 63+

?Глобальное свойство globalThis


Мы знакомы с ключевым словом this, используемым в JavaScript. У него нет некоего жёстко заданного значения. Вместо этого значение this зависит от контекста, в котором к нему обращаются. В любом окружении ключевое слово this указывает на глобальный объект в том случае, когда к нему обращаются из контекста самого верхнего уровня. Речь идёт о глобальном значении this.

В браузерном JavaScript, например, глобальным значением this является объект window. Проверить это можно, воспользовавшись конструкцией console.log(this) на верхнем уровне JavaScript-файла (в самом внешнем контексте) или в JS-консоли браузера.


Обращение к this в консоли браузера

Глобальное значение this в Node.js указывает на объект global. Внутри веб-воркера оно указывает на сам воркер. Однако получить глобальное значение this — задача не из простых. Дело в том, что для этого нельзя где угодно обратиться к this. Например, если попытаться сделать это в конструкторе класса, то окажется, что this указывает на экземпляр соответствующего класса.

В некоторых окружениях для доступа к глобальному значению this можно пользоваться ключевым словом self. Это ключевое слово играет ту же роль, что и механизмы доступа к подобному значению в браузерах, в Node.js и в веб-воркерах. Используя знания о том, как в разных средах называется глобальное значение this, можно создать функцию, которая возвращает это значение:

const getGlobalThis = () => {
 if (typeof self !== 'undefined') return self;
 if (typeof window !== 'undefined') return window;
 if (typeof global !== 'undefined') return global;
 if (typeof this !== 'undefined') return this;
 throw new Error('Unable to locate global `this`');
};
var globalThis = getGlobalThis();

Перед нами — примитивный полифилл для получения глобального объекта this. Подробнее об этом можно почитать здесь. Теперь в JavaScript имеется ключевое слово globalThis. Оно даёт универсальный способ обращения к глобальному значению this для разных сред и не зависит от места программы, из которого к нему обращаются.

var obj = { fn: function() {
  console.log( 'this', this === obj ); // true
  console.log( 'globalThis', globalThis === window ); // true
} };
obj.fn();

> Поддержка


  • TC39: Stage 3
  • Chrome: 71+
  • Node: 12+
  • Firefox: 65+

Стабильная сортировка


Стандарт ECMAScript не предлагает конкретного алгоритма сортировки массивов, который должны реализовывать JavaScript-движки. Он лишь описывает API, используемое для сортировки. В результате, пользуясь разными JS-движками, можно столкнуться с различиями в производительности операций сортировки и в стабильности (устойчивости) алгоритмов сортировки.

Теперь же стандарт требует, чтобы сортировка массивов была бы стабильной. Подробности о стабильности сортировки можно почитать здесь. Суть же этой характеристики алгоритмов сортировки сводится к следующему. Алгоритм является стабильным в том случае, если результат сортировки, представляющий собой изменённый массив, содержит в себе элементы с одинаковыми значениями, на которые не повлияла сортировка, в том же порядке, в котором они были размещены в исходном массиве. Рассмотрим пример:

var list = [
  { name: 'Anna', age: 21 },
  { name: 'Barbra', age: 25 },
  { name: 'Zoe', age: 18 },
  { name: 'Natasha', age: 25 }
];
// возможный результат сортировки по полю age
[
  { name: 'Natasha', age: 25 }
  { name: 'Barbra', age: 25 },
  { name: 'Anna', age: 21 },
  { name: 'Zoe', age: 18 },
]

Здесь массив list, содержащий объекты, сортируют по полю age этих объектов. В массиве list объект со свойством name, равным Barbra, расположен до объекта со свойством name, равным Natasha. Так как значения age этих объектов равны, мы могли бы ожидать, что в отсортированном массиве эти элементы сохранят прежний порядок расположения относительно друг друга. Однако на практике на это рассчитывать было нельзя. То, как именно будет сформирован отсортированный массив, полностью зависело от используемого JS-движка.

Теперь же все современные браузеры и Node.js используют стабильный алгоритм сортировки, вызываемый при обращении к методу массивов .sort(). Это позволяет всегда, для одних и тех же данных, получать один и тот же результат:

// стабильные результаты сортировки
[
  { name: 'Barbra', age: 25 },
  { name: 'Natasha', age: 25 }
  { name: 'Anna', age: 21 },
  { name: 'Zoe', age: 18 },
]

В прошлом некоторые JS-движки поддерживали стабильную сортировку, но только для маленьких массивов. Для повышения производительности при обработке больших массивов они могли воспользоваться более быстрыми алгоритмами и пожертвовать стабильностью сортировки.

> Поддержка


  • Chrome: 70+
  • Node: 12+
  • Firefox: 62+

API интернационализации


API интернационализации предназначено для организации сравнения строк, для форматирования чисел, даты и времени так, как принято в различных региональных стандартах (локалях). Доступ к этому API организован через объект Intl. Этот объект предоставляет конструкторы для создания объектов-сортировщиков и объектов, форматирующих данные. Список поддерживаемых объектом Intl локалей можно найти здесь.

?Intl.RelativeTimeFormat()


Во многих приложениях часто бывает нужно вывести время в относительном формате. Это может выглядеть как «5 минут назад», «вчера», «1 минуту назад» и так далее. Если материалы веб-сайта переведены на разные языки — в сборку сайта приходится включать все возможные комбинации относительных конструкций, описывающих время.

Сейчас в JS имеется конструктор Intl.RelativeTimeFormat(locale, config), который позволяет создавать системы форматирования даты и времени для различных локалей. В частности, речь идёт об объектах, имеющих метод .format(value, unit), который позволяет генерировать различные относительные отметки времени. Выглядит это так:

// espanol (испанский язык)
var rtfEspanol= new Intl.RelativeTimeFormat('es', {
  numeric: 'auto'
});
console.log( rtfEspanol.format( 5, 'day' ) ); // dentro de 5 dias
console.log( rtfEspanol.format( -5, 'day' ) ); // hace 5 dias
console.log( rtfEspanol.format( 15, 'minute' ) ); // dentro de 15 minutos

> Поддержка


  • TC39: Stage 3
  • Chrome: 71+
  • Node: 12+
  • Firefox: 65+

?Intl.ListFormat()


Конструктор Intl.ListFormat позволяет комбинировать элементы списков с использованием слов and (и), и or (или). При создании соответствующего объекта конструктору передаётся локаль и объект с параметрами. Его параметр type может принимать значения conjunction, disjunction и unit. Например, если мы хотим скомбинировать элементы массива [apples, mangoes, bananas] с помощью conjunction-объекта, то получим строку вида apples, mangoes and bananas. Если воспользоваться disjunction-объектом — получим строку вида apples, mangoes or bananas.

У объекта, создаваемого конструктором Intl.ListFormat, есть метод .format(list), который выполняет комбинирование списков. Рассмотрим пример:

// espanol (испанский язык)
var lfEspanol = new Intl.ListFormat('es', {
  type: 'disjunction'
});
var list = [ 'manzanas', 'mangos', 'platanos' ];
console.log( lfEspanol.format( list ) ); // manzanas, mangos o platanos

> Поддержка



?Intl.Locale()


Понятие «региональный стандарт» — это обычно гораздо больше, чем просто название языка. Сюда могут входить и тип календаря, и сведения об используемых часовых циклах, и названия языков. Конструктор Intl.Locale(localeId, config) используется для создания форматированных строк локалей, основанных на передаваемом ему объекте config.

Создаваемый с помощью Intl.Locale объект содержит в себе все заданные региональные настройки. Его метод .toString() выдаёт отформатированную строку регионального стандарта.

const krLocale = new Intl.Locale( 'ko', {
  script: 'Kore', region: 'KR',
  hourCycle: 'h12', calendar: 'gregory'
} );
console.log( krLocale.baseName ); // ko-Kore-KR
console.log( krLocale.toString() ); // ko-Kore-KR-u-ca-gregory-hc-h12

Здесь можно почитать об идентификаторах и о тегах локалей в Unicode.

> Поддержка


  • TC39: Stage 3
  • Chrome: 74+
  • Node: 12+

Промисы


По состоянию на настоящий момент в JS имеются статические методы Promise.all() и Promise.race(). Метод Promise.all([...promises]) возвращает промис, который успешно разрешается после того, как будут разрешены все промисы, переданные методу в виде аргумента. Этот промис оказывается отклонённым в том случаем, если хотя бы один из переданных ему промисов будет отклонён. Метод Promise.race([...promises]) возвращает промис, который разрешается после того, как любой из переданных ему промисов оказывается разрешённым, и отклоняется при отклонении хотя бы одного из таких промисов.

Сообщество JS-разработчиков отчаянно нуждалось в статическом методе, промис, возвращаемый которым, разрешался бы после того, как все переданные ему промисы оказывались бы завершёнными (разрешёнными или отклонёнными). Кроме того, нужен был метод, напоминающий race(), который возвращал бы промис, ожидающий разрешения любого из переданных ему промисов.

?Метод Promise.allSettled()


Метод Promise.allSettled() принимает массив промисов. Возвращаемый им промис разрешается после того, как все промисы окажутся отклонёнными или разрешёнными. В результате оказывается так, что промис, возвращённый этим методом, не нуждается в блоке catch.

Дело в том, что этот промис всегда успешно разрешается. Блок then получает status и value от каждого промиса в порядке их появления.

var p1 = () => new Promise(
  (resolve, reject) => setTimeout( () => resolve( 'val1' ), 2000 )
);
 
var p2 = () => new Promise(
  (resolve, reject) => setTimeout( () => resolve( 'val2' ), 2000 )
); 
 
var p3 = () => new Promise(
  (resolve, reject) => setTimeout( () => reject( 'err3' ), 2000 )
);
var p = Promise.allSettled( [p1(), p2(), p3()] ).then(
  ( values ) => console.log( values )
);
// вывод
[ {status: "fulfilled", value: "val1"}
  {status: "fulfilled", value: "val2"}
  {status: "rejected", value: "err3"}
]

> Поддержка



?Метод Promise.any()


Метод Promise.any() похож на Promise.race(), но промис, возвращаемый им, не выполняет блок catch при отклонении одного из переданных этому методу промисов.

Вместо этого он ожидает разрешения всех промисов. Если не был разрешён ни один промис — тогда будет выполнен блок catch. Если же будет успешно разрешён любой из промисов — будет выполнен блок then.

Итоги


В этом материале мы рассмотрели некоторые новшества JavaScript, речь о которых шла на конференции Google I/O 2019. Надеемся, вы нашли среди них что-то такое, что вам пригодится.

Уважаемые читатели! Чего вам особенно не хватает в JavaScript?




К сожалению, не доступен сервер mySQL