Сложно уследить за новшествами различных версий ECMAScript, а ещё сложнее — найти полезные примеры их применения, не перекапывая горы информации. Поэтому сегодня мы публикуем перевод материала, автор которого проанализировал 18 новых возможностей ECMAScript, в число которых входят те, что имеются в уже вышедших стандартах ES2016 и ES2017, а также — те, которые должны появиться в стандарте ES2018. Автор этой статьи обещает, что каждый, кто её прочтёт, узнает много интересного и полезного о новых возможностях JavaScript.



ECMAScript 2016



?1. Array.prototype.includes()


Метод includes — это простой метод объектов типа Array, который позволяет выяснить, имеется ли в массиве некий элемент (он, в отличие от indexOf, подходит и для работы со значениями NaN).


ECMAScript 2016 (ES7) — пример использования Array.prototype.includes()

Интересно то, что сначала этот метод хотели назвать contains, но оказалось, что такое название уже используется в Mootools, в результате было принято решение использовать имя includes.

?2. Инфиксный оператор возведения в степень


Математические операции, вроде сложения и вычитания, реализуются в JavaScript с помощью инфиксных операторов, таких, как, соответственно, «+» и «-». Существует и нашедший широкое применение в программировании инфиксный оператор, который используется для возведения в степень. Такой оператор, выглядящий как «**», был представлен в ECMAScript 2016, он может служить заменой Math.pow().


ECMAScript 2016 (ES7) — использование инфиксного оператора возведения в степень

ECMAScript 2017



?1. Object.values()


Метод Object.values() — это новая функция, которая похожа на Object.keys(), но возвращает все значения собственных свойств объекта, исключая любые значения в цепочке прототипов.


ECMAScript 2017 (ES8) — использование Object.values()

?2. Object.entries()


Метод Object.entries() похож на метод Object.keys(), но вместо того, чтобы возвращать лишь ключи, он возвращает, в виде массива, и ключи, и значения. Это упрощает выполнение операций, предусматривающих использование объектов в циклах, или операций преобразования обычных объектов в объекты типа Map.

Вот первый пример использования этого метода.


ECMAScript 2017 (ES8)? —? использование Object.entries() в циклах

Вот второй пример.


ECMAScript 2017 (ES8)? —? использование Object.entries() для преобразования объекта типа Object в объект типа Map

?3. Дополнение строк до заданной длины


У объектов типа String теперь есть два новых метода: String.prototype.padStart() и String.prototype.padEnd(). Они позволяют присоединять к строкам, в их начало или конец, некоторое количество символов для дополнения строк до заданной длины.

'someString'.padStart(numberOfCharcters [,stringForPadding]); 
'5'.padStart(10) // '          5'
'5'.padStart(10, '=*') //'=*=*=*=*=5'
'5'.padEnd(10) // '5         '
'5'.padEnd(10, '=*') //'5=*=*=*=*='

Это оказывается удобным, если нужно выровнять текст, например, при выводе в консоль.

3.1 Пример работы с padStart()


В следующем примере у нас имеется список чисел разной длины. Мы хотим добавить в начало этих чисел «0», причём, таким образом, чтобы сделать все их состоящими из 10 цифр. Нужно это для того, чтобы аккуратно вывести их на экран. Для того чтобы решить эту задачу, мы можем воспользоваться командой padStart(10, '0').


ECMAScript 2017 ?— пример использования?padStart()

3.2 Пример работы с padEnd()


Метод padEnd() оказывается весьма полезным при выводе на экран множества строк разной длины, которые нужно выровнять по правому краю страницы.

В следующем примере показано совместное использование методов padEnd(), padStart(), и Object.entries() для формирования строк, которые хорошо смотрятся при выводе на экран.


ECMAScript 2017? —? пример использования padEnd(), padStart() и Object.Entries()

3.3. Использование padStart() и padEnd() с эмотиконами и другими двухбайтовыми символами


Эмотиконы и другие двухбайтовые символы представлены в кодировке Unicode последовательностями из нескольких байтов. Поэтому методы padStart() и padEnd() работают с ними не так, как того можно ожидать.

Например, предположим, что мы хотим сделать так, чтобы длина строки heart оказалась равной 10 символам путём добавления к ней эмотикона ?. Результат, при обычно подходе, будет выглядеть так:

//Обратите внимание на то, что вместо 5 сердечек тут лишь 2 таких, которые нам нужны, и одно, выглядящее не так, как ожидается
'heart'.padStart(10, "?"); // при выводе получается '??heart'

Проблема тут в том, что то сердечко, которым мы хотим дополнить строку, представлено двухбайтовым кодом '\u2764\uFE0F'. Слово heart имеет длину 5 символов и для дополнения этой строки до длины 10 у нас остаётся лишь 5 свободных мест. В результате JS добавляет к строке два эмотикона, используя код '\u2764\uFE0F', занимающий четыре свободных места, что даёт два значка сердца. Последнее свободное место заполняется первым байтом, символом с кодом '\u2764', который как раз и даёт то сердце, которое выглядит не так, как остальные.

Вот, кстати, страница, которой можно воспользоваться для работы с символами Unicode.

?4. Object.getOwnPropertyDescriptors()


Этот метод возвращает все сведения (включая данные о геттерах и сеттерах) для всех свойств заданного объекта. Главная причина добавления этого метода заключается в том, чтобы позволить создавать мелкие копии объектов и клонировать объекты, создавая новые объекты, при этом копируя, помимо прочего, геттеры и сеттеры. Метод Object.assign() этого не умеет. Он позволяет выполнять мелкие копии объектов, но не работает с их геттерами и сеттерами.

В следующих примерах показана разница между Object.assign() и Object.getOwnPropertyDescriptors(), а также продемонстрировано использование метода Object.defineProperties() для копирования исходного объекта car в новый объект, ElectricCar. Тут можно заметить, что благодаря использованию Object.getOwnPropertyDescriptors(), функция discount, играющая роль и геттера, и сеттера, также копируется в целевой объект.

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


Копирование объектов с использованием Object.assign()

Вот как выглядит выполнение той же операции, но уже с использованием Object.getOwnPropertyDescriptors().


ECMAScript 2017 (ES8)? —? использование Object.getOwnPropertyDescriptors()

?5. Завершающие запятые в параметрах функций


Это небольшое обновление, которое позволяет ставить запятую после последнего параметра функции. Зачем это нужно? Например, для того, чтобы помочь при работе с инструментами вроде git blame, избавляя разработчиков, вносящих изменения в код, от необходимости менять (без особой нужды в данном случае) строки, написанные тем, кто работал над этим кодом раньше.

В следующем примере показана проблема редактирования кода и её решение с помощью запятой, которая находится за последним параметром функции.


ECMAScript 2017 (ES8)? — запятая, поставленная после последнего параметра функции, облегчает редактирование кода

?6. Конструкция Async/Await


Это нововведение я назвал бы самым важным и самым полезным. Асинхронные функции позволяют избавиться от так называемого «ада коллбэков» и улучшить внешний вид и читаемость кода.
Ключевое слово async сообщает JavaScript-интерпретатору о том, что функцию, объявленную с этим ключевым словом, нужно воспринимать по-особому. Систем приостанавливается, достигая ключевого слова await в этой функции. Она считает, что выражение после await возвращает промис и ожидает разрешения или отклонения этого промиса перед продолжением.

В следующем примере функция getAmount() вызывает две асинхронные функции — getUser() и getBankBalance(). Сделать это можно и в промисе, но использование конструкции async/await позволяет решить эту задачу проще и элегантнее.


ECMAScript 2017 (ES 8) — простой пример использования конструкции Async/Await

6.1. Асинхронные функции и возврат промисов


Если вы ожидаете результата от функции, объявленной с использованием ключевого слова async, вам нужно будет использовать выражение промиса .then() для того, чтобы этот результат получить.

В следующем примере мы хотим вывести результат в консоль, используя команду console.log(), но сделать это нужно за пределами функции doubleAndAdd(). Поэтому нам нужно подождать, используя .then(), для того, чтобы передать полученный результат console.log().


ECMAScript 2017 (ES 8) — демонстрация возврата промиса конструкцией async/await

6.2. Параллельный вызов функций с использованием async/await


В предыдущем примере мы дважды пользуемся ключевым словом await, каждый раз ожидая завершения операции в течение одной секунды (общее время ожидания — 2 секунды). Вместо этого мы можем распараллелить выполнения задач, так как a и b не зависят друг от друга. Сделать это можно с помощью конструкции Promise.all().


ECMAScript 2017 (ES 8) ?— использование Promise.all для параллельного выполнения await-команд

6.3. Обработка ошибок при использовании конструкции async/await


Существуют различные способы обработки ошибок при использовании конструкции async/await.

Вариант 1: использование try/catch внутри функции


ECMAScript 2017? —? использование try/catch внутри конструкции async/await

Вариант 2: использование catch в каждом выражении await

Так как каждое выражение await возвращает промис, перехватывать ошибки можно в каждом таком выражении.


ECMAScript 2017 — Использование конструкции catch с каждым выражением await

Вариант 3: использование конструкции catch для всей async/await-функции


ECMAScript 2017? —? перехват ошибок во всей асинхронной функции

ECMAScript 2018




ECMAScript 2018 сейчас находится на стадии final draft, выход стандарта ожидается в июне-июле 2018 года. Все возможности, рассмотренные ниже, находятся на этапе Stage-4 и станут частью ECMAScript 2018.

?1. Разделяемая память и атомарные операции


Разделяемая память (shared memory) и атомарные операции (atomics) — это потрясающая, весьма продвинутая возможность, затрагивающая ядро JS-движков.

Основная идея этой возможности заключается в том, чтобы позволить JS-разработчикам писать высокопроизводительные параллельные приложения, дать им возможность управлять памятью самостоятельно, не отдавая выполнение всех аспектов этой задачи JS-движку.

Реализовано это с помощью глобального объекта нового типа, который называется SharedArrayBuffer. Его основная задача заключается в хранении данных в разделяемом пространстве памяти. В результате такими данными могут совместно пользоваться главный поток JS и потоки веб-воркеров.

До настоящего момента, если нам нужно было организовать обмен данными между главным потоком и веб-воркером, нам приходилось создавать копии данных и отправлять их с помощью функции postMessage(). Теперь же всё будет уже не так, как раньше.

Благодаря использованию SharedArrayBuffer разные потоки могут получать доступ к данным практически мгновенно. Однако общий доступ к памяти из разных потоков может вызвать состояние гонок. Для того чтобы избежать этого состояния, предусмотрен глобальный объект Atomics. Он предоставляет различные методы для блокирования разделяемой памяти на время выполнения операций с ней из конкретного потока. Он, кроме того, даёт методы для безопасного обновления данных в разделяемой памяти.

Обычно этой возможностью не рекомендуют пользоваться напрямую, говоря о том, что лучше будет применять библиотеки, построенные на базе SharedArrayBuffer. Однако такие библиотеки пока не написаны.

Если вам интересна эта тема — вот, вот и вот — несколько полезных материалов.

?2. Устранение ограничений тегированных шаблонных строк


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

В ES2015+ есть возможность, называемая тегированным шаблоном, которая позволяет разработчикам настраивать интерполяцию строк. Например, при стандартном подходе это выглядит так.


Стандартный вариант использования шаблонных строк

При использовании тегированных шаблонов можно написать функцию, принимающую, как параметры, неизменную часть строкового литерала, например, [ ‘Hello ‘, ‘!’ ] и переменные для замены, например, [ 'Raja']. Например, пусть это будет функция greet. Такая функция может возвращать то, что нужно разработчику.

В следующем примере показано, как наше собственное теговое выражение, функция, присоединяет к строке приветствие, основываясь на времени суток (например — «Good Morning!» или «Good afternoon») и возвращает готовую строку.


Пример тегового выражения, функции, которая демонстрирует настраиваемое дополнение строк

Узнав о тегированных шаблонных строках, многие стремятся использовать эту возможность в различных сферах, например, для работы с командами в терминале или при создании URL для выполнения HTTP-запросов. Однако, тут нужно учитывать одно важное ограничение. Оно заключается в том, что ES2015 и ES2016 позволяют использовать лишь управляющие последовательности вроде "\u" (unicode), "\x"(hexadecimal), которые формируют нечто такое, что понятно системе, например, `\u00A9` или \u{2F804} или \xA9.

Поэтому, если имеется тегированная функция, внутри которой используются правила из другой области (например, при работе с терминалом), где может понадобиться использовать что-то вроде \ubla123abla, что не выглядит с точки зрения системы правильным кодом, появится сообщение об ошибке.

В ES2018 ограничения ослаблены. Теперь можно использовать конструкции, выглядящие как неправильные управляющие последовательности, возвращая значения в объекте, одно свойство которого («cooked» в нашем примере), содержит undefined, а второе («raw») содержит то, что нам нужно.

function myTagFunc(str) { 
 return { "cooked": "undefined", "raw": str.raw[0] }
} 

var str = myTagFunc `hi \ubla123abla`; //вызов myTagFunc

str // { cooked: "undefined", raw: "hi \\ubla123abla" }

?3. Флаг регулярных выражений dotAll


Сейчас, при использовании регулярных выражений, хотя считается, что символ точки соответствует любому одиночному символу, он не соответствует символам перевода строки вроде \n \r \f и так далее.

Например, до этого нововведения всё выглядело так:

/first.second/.test('first\nsecond'); //false

Благодаря данному улучшению точка теперь соответствует абсолютно любому символу. Для того, чтобы старые регулярные выражения продолжали бы работать так, как раньше, при создании регулярных выражений, следующих новым правилам, нужно использовать флаг \s.

//ECMAScript 2018
/first.second/s.test('first\nsecond'); //true - обратите внимание на /s

Вот API этого предложения из документации.


ECMAScript 2018 — теперь, благодаря флагу /s, можно сделать так, чтобы точка в регулярных выражениях соответствовала абсолютно всем символам, включая \n

?4. Захват именованных групп в регулярных выражениях


Это улучшение представляет собой полезную возможность регулярных выражений, которая имеется в других языках вроде Python и Java. Речь идёт об именованных группах. Эта возможность позволяет разработчикам писать регулярные выражения с назначением имён (идентификаторов) в формате (?<name>...) для групп. Это имя облегчает работу с группами.

4.1. Базовый пример работы с именованными группами


В следующем примере мы используем имена (?<year>), (?<month>) и (?day) для группировки различных частей даты, с которой мы работаем с использованием регулярного выражения. Итоговый объект при таком подходе будет содержать свойство groups, которое будет иметь свойства year, month и day с соответствующими значениями.


ECMAScript 2018? — пример работы с именованными группами

4.2. Использование именованных групп внутри регулярного выражения


Мы можем использовать конструкции вида \k<group name> для того, чтобы ссылаться на группы внутри самого регулярного выражения. Эта техника продемонстрирована в следующем примере.


ECMAScript 2018? —? использование именованных групп внутри регулярного выражения с применением конструкции \k<group name>

4.3. Использование именованных групп в String.prototype.replace()


еперь возможность использования именованных групп встроена в метод строк replace(). Это позволяет, например, организовать перестановку слов в строках. Например, вот как изменить строку "firstName, lastName" на "lastName, firstName".


ECMAScript 2018 — использование именованных групп регулярных выражений в строковом методе replace()

?5. Работа со свойствами объектов с использованием оператора rest


Оператор rest, выглядящий как три точки, позволяет извлекать свойства объекта, которые пока из него не извлечены.

5.1. Извлечение из объекта только необходимых свойств



ECMAScript 2018? —? деструктурирование объекта с помощью оператора rest

5.2. Удаление ненужных свойств объектов



ECMAScript 2018? —? удаление ненужных свойств объектов

?6. Работа со свойствами объектов с использованием оператора spread


Оператор spread тоже выглядит как три точки. Разница между ним и оператором rest заключается в том, что он используется для создания новых объектов.

Оператор spread используется в правой части выражения со знаком присваивания. Оператор rest используется в левой части выражения.


ECMAScript 2018? — создание объекта с использованием оператора rest

?7. Ретроспективная проверка в регулярных выражениях


Ретроспективная проверка в регулярных выражениях позволяет узнать, существует ли некая строка сразу перед некоторой другой строкой.

Теперь для того, чтобы произвести положительную ретроспективную проверку, можно использовать группу вида (?<=…) (знак вопроса, знак «меньше» и знак равенства).

Далее, можно использовать группу вида (?>!…) (знак вопроса, знак «меньше», восклицательный знак) для выполнения отрицательной ретроспективной проверки. Поиск при таком подходе продолжается только в том случае, если слева от текущей позиции в тексте отсутствует выражение, указанное в скобках.

Рассмотрим пример положительной ретроспективной проверки. Предположим, нам надо проверить, находится ли символ # перед словом winning (то есть, нас интересует конструкция вида #winning), при этом нужно, чтобы регулярное выражение вернуло лишь строку «winning». Вот как можно этого достичь.


ECMAScript 2018 — использование конструкции (?<=…) для положительной ретроспективной проверки

Рассмотрим пример отрицательной ретроспективной проверки. Предположим, нам нужно извлечь из строки числа, перед которыми стоит знак , но не знак $. Вот как это сделать.


EMAScript 2018 — использование конструкции (?<!…) для отрицательной ретроспективной проверки

?8. Использование управляющих последовательностей Unicode в регулярных выражениях


Раньше писать регулярные выражения, направленные на анализ Unicode-символов, было непросто. Конструкции вида \w, \W, \d помогали лишь в работе с латинскими символами и цифрами. А как быть с символами других языков, вроде хинди или греческого?

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

Например база данных Unicode группирует все символы хинди (например — ??????, здесь, кстати, три символа) по свойству Script со значением Devanagari, и по ещё одному свойству, которое выглядит как Script_Extensions, с тем же значением. Это позволяет нам выполнить поиск по значению Script=Devanagari и получить все символы хинди.

Unicode-блок Devanagari может использоваться для различных индийских языков, таких, как маратхи, хинди, санскрит и других.

Начиная с ECMAScript 2018 можно использовать конструкцию вида \p{Script=Devanagari} для поиска символов всех языков письменности деванагари. Такая конструкция позволяет работать со всеми подобными символами.


ECMAScript 2018 — использование возможности по работе с Unicode-символами письменности деванагари

Похожим образом, все греческие символы собраны в группе Script_Extensions (Script) со значением Greek. Это даёт возможность выполнять поиск греческих символов с использованием конструкции вида Script_Extensions=Greek или Script=Greek.

Вот как с помощью конструкции \p{Script=Greek} искать греческие символы.


ECMAScript 2018? — использование возможности по работе с греческими символами

Далее, база данных Unicode хранит различные типы эмотиконов в блоках Emoji, Emoji_Component, Emoji_Presentation, Emoji_Modifier, и Emoji_Modifier_Base со свойством true. Это даёт возможность искать эмотиконы, просто используя свойство Emoji.

Таким образом, мы можем использовать конструкции вида \p{Emoji} и \p{Emoji_Modifier} для поиска эмотиконов различных видов. Вот соответствующий пример.


ECMAScript 2018 — поиск различных эмотиконов

И, наконец, мы можем использовать управляющую последовательность, представленную заглавной буквой P (\P) вместо прописной p (\p) для проверки строк на отсутствие совпадений.

?9. Promise.prototype.finally()


Метод finally() — это новый метод объектов Promise. Основное назначение этого метода заключается в том, чтобы позволить выполнять функцию обратного вызова после resolve() или reject() для того, чтобы корректно завершать операции, например, высвобождая ресурсы там, где это необходимо. Коллбэк finally() вызывается без какого-либо значения, он выполняется в любом случае.

Рассмотрим различные варианты его использования.


ECMAScript 2018? —? использование finally() в случае вызова resolve()


ECMAScript 2018? —? использование finally() в случае вызова reject()


ECMAScript 2018? —? использование finally() при возникновении ошибки


ECMAScript 2018? —? использование finally() при возникновении ошибки в выражении catch()

?10. Асинхронная итерация


Это — просто невероятно полезная возможность. Она позволяет без труда создавать циклы, работающие с асинхронным кодом.

В рамках этой возможности добавляется новый оператор цикла вида for-await-of, который позволяет вызывать асинхронные функции, возвращающие промисы (или обрабатывать массивы, содержащие промисы) в цикле. Самое интересное здесь то, что цикл ждёт разрешения каждого промиса перед переходом к следующему шагу.


ECMAScript 2018 — использование нового цикла for-await-of

Итоги


В этом материале были рассмотрены новшества ES2016, ES2017 и ES2018, а так же были приведены примеры использования данных конструкций. Надеемся, это пригодится всем, кто хочет упорядочить свои знания в области новых и ожидаемых возможностей JavaScript и поможет продуктивно их использовать.

Уважаемые читатели! Какие новшества ES2016, ES2017 и ES2018 кажутся вам наиболее полезными?

Комментарии (11)


  1. vanxant
    11.04.2018 13:27

    Так-то подборка неплохая. Но за эмодзи в листингах — персональный котёл в аду этому господину!


  1. 4urbanoff
    11.04.2018 14:39

    Благодаря данному улучшению точка теперь соответствует абсолютно любому символу. Для того, чтобы старые регулярные выражения продолжали бы работать так, как раньше, при создании регулярных выражений, следующих новым правилам, нужно использовать флаг \s.

    Я думаю тут неточность перевода или его источника. Старые регулярные выражения с точкой свое поведение не меняют, они так и будут работать, как и раньше, а вот чтоб они заработали по новому в режиме dotAll надо как раз добавить флаг /s.

    Спасибо за статью!


  1. movis08
    11.04.2018 14:48

    И жаль, что листинги в картинках, а не в коде.
    А статья супер!


  1. maxfarseer
    11.04.2018 16:12

    Плюсую, листинги должны быть кодом, а не картинками.


  1. 8bitjoey
    11.04.2018 17:19
    +1

    > Вариант 2: использование catch в каждом выражении await

    У вас там корявое объяснение: «each await expression is a Promise in itself». Это утверждение эквивалентно такому коду:

    (await doubleAfter1Sec(a)).catch(...)
    Хотя по факту он такой:
    await (doubleAfter1Sec(a).catch(...))
    И await не возвращает промиз, в этом как раз его смысл. Вы здорово сбиваете с толку такими примерами, как будто это что-то новое, привнесенное await'ом, а это всего лишь использование await'а со старыми добрыми промизами.

    И вообще, рекомендую http://es6-features.org/, как один из сайтов, где все новые фичи изложены кратко с минимальными примерами.


    1. AxisPod
      11.04.2018 18:55

      Я нормально понял логику работы async/await, когда в поглядел сгенерированный код в .net. Когда увидел в JS сразу порадовался, но использовать в проекте начали прям именно сегодня.


  1. AxisPod
    11.04.2018 17:34
    +1

    Надо проверять переводимые материалы :-)

    Решение в 6.2 явно корявейшее и таким я бы пользоваться не стал. Всё же матчасть стоит подтягивать. Можно просто и даже очень просто.

    async function doubleAndAdd2(a, b) {
      a = doubleAfter1Sec(a);
      b = doubleAfter1Sec(b);
      return await a + await b;
    }


    Ну и ошибонька, явно код не проверялся.
    6.1 и 6.2, надо писать setTimeout(() => resolve(param * 2), 1000);

    Можно поглядеть: repl.it/repls/AmusingZealousEnvironment


    1. AxisPod
      11.04.2018 17:40

      Ну да, это касается ES2017.


  1. bro-dev
    11.04.2018 18:54

    Не понял прикола с for await, ведь await в теле цикла делает тоже самое, но имхо более явно.


    1. mayorovp
      11.04.2018 19:22
      +2

      И правильно сделали что не поняли. Настоящая цель появления for await — это итерация по последовательности элементов, где без await невозможно даже определить будет ли следующий или последовательность уже закончилась.


      Например, эта конструкция нужна для итерации по асинхронному генератору. Вот пример такого генератора из пропозала:


      async function* readLines(path) {
        let file = await fileOpen(path);
      
        try {
          while (!file.EOF) {
            yield await file.readLine();
          }
        } finally {
          await file.close();
        }
      }


  1. Finesse
    12.04.2018 01:25

    Эмотиконы и другие двухбайтовые символы представлены в кодировке Unicode последовательностями из нескольких байтов. Поэтому методы padStart() и padEnd() работают с ними не так, как того можно ожидать.

    В статье причина этого явления объясняется немного неверно. Во-первых, эмотиконы состоят из 4-х байт (\u2764\uFE0F — это 4 байта, а не 2). Во-вторых в JavaScript в строках все символы 2-байтовые. В результате 1 эмотикон это 2 JS-символа. А есть эмотиконы, которые состоят больше, чем из 4-х байт, например, эмодзи с разным цветом кожи, которые образуются комбинацией других эмоджи.