Хабрахабр

[Перевод] Новшества JavaScript: итоги Google I/O 2019. Часть 2

Сегодня мы публикуем вторую часть перевода материала о новшествах 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. Максимальное значение, которое можно безопасно представить с помощью типа данных Number, представляет собой (2⁵³ — 1), то есть — 9007199254740991. 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.

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

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]

→ Поддержка

▍Метод .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]

→ Поддержка

▍Метод Object.fromEntries()

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

var obj = ;
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}

→ Поддержка

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

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

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

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

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

Это ключевое слово играет ту же роль, что и механизмы доступа к подобному значению в браузерах, в Node.js и в веб-воркерах. В некоторых окружениях для доступа к глобальному значению this можно пользоваться ключевым словом self. Используя знания о том, как в разных средах называется глобальное значение 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();

→ Поддержка

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

Стандарт 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 минуту назад» и так далее. Если материалы веб-сайта переведены на разные языки — в сборку сайта приходится включать все возможные комбинации относительных конструкций, описывающих время.

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

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

→ Поддержка

▍Intl.ListFormat()

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

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

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

→ Поддержка

▍Intl.Locale()

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

Locale объект содержит в себе все заданные региональные настройки. Создаваемый с помощью Intl. Его метод .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]) возвращает промис, который разрешается после того, как любой из переданных ему промисов оказывается разрешённым, и отклоняется при отклонении хотя бы одного из таких промисов.

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

▍Метод 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?

Показать больше

Похожие публикации

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»