Возможности современного JavaScript, о которых вы могли не знать

Original author: Jimmy Breck-McKye
  • Translation


Несмотря на то, что в последние семь лет я пишу на JavaScript почти каждый рабочий день, должен признаться, что уделяю мало внимания сообщениям о нововведениях от ES. Главные возможности вроде async/await и прокси — это одно, но ещё каждый год идёт поток мелких поэтапных изменений, которые не попадают в моё поле зрения, поскольку всегда находится что-то более важное для изучения.

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

ES2015


Двоичные и восьмеричные литералы


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

Всё это может потребовать большого количества работы по скрытию/объединению двоичных чисел; мне всегда казалось, что их зря упрятали в десятичные. Именно для таких случаев в ES6 добавили формат двоичных литералов: 0b.

const binaryZero = 0b0;
const binaryOne  = 0b1;
const binary255  = 0b11111111;
const binaryLong = 0b111101011101101;

Это сильно упрощает работу с двоичными флагами:

// Pizza toppings
const olives    = 0b0001;
const ham       = 0b0010;
const pineapple = 0b0100;
const artechoke = 0b1000;

const pizza_ham_pineapple = pineapple | ham;
const pizza_four_seasons  = olives | ham | artechoke;

То же самое и с восьмеричными числами. В мире JS это нишевая возможность, но организации сетевой работы и некоторых файловых форматах они используются часто. Вы можете писать восьмеричные числа с помощью синтаксиса 0o.

Number.isNaN()


Не путать с window.isNaN(), это новый метод с гораздо более интуитивным поведением.

У классического isNaN есть несколько интересных хитростей:

isNaN(NaN)        === true
isNaN(null)       === false
isNaN(undefined)  === true
isNaN({})         === true
isNaN('0/0')      === true
isNaN('hello')    === true

Что нам это даёт? Во-первых, ни один из этих параметров на самом деле не является NaN. Как обычно, проблема во всеми «любимом» свойстве JavaScript: приведении типов. Аргументы для window.isNaN приведены к числам с помощью функции Number.

Эту проблему решает новый статический метод Number.isNaN(). Он раз и навсегда возвращает равенство аргументов, переданных ему и NaN. Это абсолютно однозначно:

Number.isNaN(NaN)       === true
Number.isNaN(null)      === false
Number.isNaN(undefined) === false
Number.isNaN({})        === false
Number.isNaN('0/0')     === false
Number.isNaN('hello')   === false

Сигнатура: Number.isNaN : (value: any) => boolean

ES2016


Оператор возведение в степень


Время от времени такое случается, так что хорошо иметь под рукой литеральный синтаксис для возведения в степень:

2**2 === 4
3**2 === 9
3**3 === 27

Странно, но я был уверен, что в JavaScript такое уже есть. Возможно, спутал с Python.

Array.prototype.includes()


Такое трудно было пропустить, но если в последние три года вы писали array.indexOf(x) !== -1, то возрадуйтесь новому методу includes:

[1, 2, 3].includes(2)    === true
[1, 2, 3].includes(true) === false

includes использует алгоритм Same Value Zero, который почти идентичен проверке на строгое равенство (===), за исключением того, что может обрабатывать значения NaN. Этот алгоритм тоже сравнивает объекты по ссылкам, а не содержимому:

const object1 = {};
const object2 = {};

const array = [object1, 78, NaN];

array.includes(object1) === true
array.includes(object2) === false
array.includes(NaN)     === true

includes может брать второй параметр, fromIndex, который позволяет вам предоставлять значение сдвига:

// positions   0  1  2  3  4
const array = [1, 1, 1, 2, 2];

array.includes(1, 2) === true
array.includes(1, 3) === false

Полезно.

Сигнатура: Array.prototype.includes : (match: any, offset?: Int) => boolean

ES2017


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


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

Это две большие возможности с довольно сложными API, так что здесь я их описывать не буду. За подробностями отправляю вас к этой статье: https://www.sitepen.com/blog/the-return-of-sharedarraybuffers-and-atomics/. Еще не все браузеры поддерживают эти функции, но надеюсь, что в ближайшие пару лет ситуация улучшится.

ES2018


Золотая жила регулярных выражений


В ES2018 появилась целая россыпь новых возможностей регулярных выражений:

Lookbehind-сопоставления (сопоставление с предыдущими символами)


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

const regex = /(?<=\$)\d+/;
const text  = 'This cost $400';
text.match(regex) === ['400']

Всё дело в новой lookbehind-группе, близнеце lookahead-групп:

Look ahead:  (?=abc)
Look behind: (?<=abc)

Look ahead negative:  (?!abc)
Look behind negative: (?<!abc)

К сожалению, сегодня нельзя транспилировать новый lookbehind-синтаксис под старые браузеры, так что вполне возможно, какое-то время вы сможете использовать его только в Node.

Именованные группы захвата


Теперь регулярные выражения могут выбирать подвыборки и использовать для простого парсинга. До недавнего времени мы могли ссылаться на такие фрагменты только по числам, например:

const getNameParts  = /(\w+)\s+(\w+)/g;
const name          = "Weyland Smithers";
const subMatches    = getNameParts.exec(name);

subMatches[1]     === 'Weyland'
subMatches[2]     === 'Smithers'

А теперь есть синтаксис присвоения имён этим подвыборкам (или группам записи): внутри скобок в начале ставим ?<titlе>, если хотим присвоить группе имя:

const getNameParts  = /(?<first>\w+)\s(?<last>\w+)/g;
const name          = "Weyland Smithers";
const subMatches    = getNameParts.exec(name);

const {first, last} = subMatches.groups
first             === 'Weyland'
last              === 'Smithers'

К сожалению, сейчас это работает только в Chrome и Node.

Теперь точки могут отмечать новые строки


Нужно только проставлять флаг /s, например, /someRegex./s, /anotherRegex./sg.

ES2019


Array.prototype.flat() и flatMap()


Я был очень рад увидеть это в MDN.

Попросту говоря, flat() преобразует многомерный массив в одномерный на заданную максимальную глубину (depth):

const multiDimensional = [
    [1, 2, 3],
   [4, 5, 6],
   [7,[8,9]]
];

multiDimensional.flat(2) === [1, 2, 3, 4, 5, 6, 7, 8, 9]

flatMap — это map, за которым идёт flat с глубиной 1. Это полезно, если нужно мапить функцию, которая возвращает массив, но при этом вам не нужно, чтобы результат представлял собой вложенную структуру данных:

const texts = ["Hello,", "today I", "will", "use FlatMap"];

// with a plain map
const mapped = texts.map(text => text.split(' '));
mapped === ['Hello', ['today', 'I'], 'will', ['use', 'FlatMap']];

// with flatmap
const flatMapped = texts.flatMap(text => text.split(' '));
flatMapped === ['Hello', 'today', 'I', 'will', 'use', 'FlatMap'];

Неограниченные перехваты


Теперь вы можете писать выражения try/catch без привязки к киданию ошибок:

try {
 // something throws
} catch {
 // don't have to do catch(e)
}

Кстати, перехваты, в которых вы не учитываете значение e, иногда называют обработкой исключений-покемонов. Потому что вы должны поймать их все!

Методы обрезки строковых значений


Незначительно, но приятно:

const padded         = '          Hello world   ';
padded.trimStart() === 'Hello world   ';
padded.trimEnd()   === '          Hello world';
Mail.ru Group
1,026.65
Building the Internet
Share post

Comments 53

    +3
    У меня в Chrome 78.0.3904.70
    Number.isNaN(0/0) === true
    Ну и если предположить, что
    Number.isNaN(0/0) === false 
    , то чему будет равно 0/0?
      0

      Ну, так делить на ноль вроде как нельзя, нет? Или что вас удивляет?

        0
        В JS можно, как раз NaN и получается.
          0

          Ну, так NaN это нот-э-намбэр, то есть не валидная мат-операция, к примеру. Попробуйте пятерку разделить на "привет", то же самое будет.

            +3
            Согласен, и Number.isNaN вернёт true для 0/0 (и для 6/«привет»), а в статье написано что вернёт false.
              0

              А, ясно. Я не читал примеры, но там, судя по всему, ошибка.

                +3
                Number.isNaN('0/0') === false
                Number.isNaN(0/0) === true
                В статье все правильно написано.
                Первое — вполне валидная строка (и я тоже не понимаю зачем она там для примера) и она действительно вернет false.
                Второе — мат.операция, результат которой NaN
                  +3
                  В статье исправили, раньше было с ошибкой — просто 0/0 без кавычек.
              +3
              Обращу внимание что NaN выйдет только при 0/0.
              Если делить любое другое число на 0 то получим Infinity.
              И кстати
              isNaN(Infinity) === false
              Number.isNaN(Infinity) === false
              
                +1
                Ещё в копилку:
                Number.isNaN((230 - 230 / 100 * 100) / 0) === false
                  0
                  Тут дело просто в порядке операций
                  Number.isNaN(((230 - 230) / 100 * 100) / 0) === true
                  

                  +2
                  … что абсолютно соответствует принятым в матане соглашениям. 0/0 — неопределённость, 1/0 — бесконечность, и они друг другу не равны. Вот если вы попробуете сделать 2/0 — 1/0, тогда снова будет NaN
                    0
                    Интересный у вас матан. По определению «a» делится на «b», если существует такое «с», что a = bc.

                    Рассмотрим 1 / 0. Тогда нам нужно такое «с», что 1 = 0 * c. Очевидно, его не существует. Результат умножения бесконечности на ноль не определен, потому что бесконечность — не число. Если пользоваться теорией пределов, то можно получить любое значение. Да и даже чисто умозрительно: если 1 / 0 = inf и 2 / 0 = inf, то 1 / 0 = 2 / 0 = inf. Множим на ноль и получаем что inf * 0 = 1 = 2. А если множить на ноль по другому, то получим что inf * 0 = 0 = 0.
                    Но тогда непонятно почему 1 / 0 = inf, если inf * 0 = 0.

                    Короче, с бесконечностями можно играться только в пределах.


                    А вот ноль на ноль делиться. Действительно: 0 = 0 * с. «c» тут может быть любым. Таким образом, хоть 0 на 0 и делиться (согласно определению), но результат этого деления не определен.

                    С точки зрения математики, бесконечность числом не является. А вот в IEEE 754 — является, судя по всему. Во всяком случае там +inf, -inf и NaN — разные сущности.
                      +1
                      Нормальный у меня матан, советский, качественный. У авторов IEEE 754 был такой же. Бесконечность — два особых числа, о их сути можно спорить (зависит от того, какие аксиомы мы выбрали в теории множеств и арифметике, которые обе заведомо неполны по Геделю). Но в любом случае для бесконечностей определены все арифметические операции с обычными числами кроме 0, а также часть операций с 0 и другой бесконечностью.
                      А вот неопределённость — это не число, not-a-number. Абсолютно любая операция числа с не-числом даёт не-число.
                        0
                        Но в любом случае для бесконечностей определены все арифметические операции с обычными числами кроме 0, а также часть операций с 0 и другой бесконечностью.


                        Извините, но нет.
                        1 + inf = inf
                        2 + inf = inf
                        Такое возможно только если inf — это ноль. Но это не ноль по определению.

                        В принципе, из этого уже видно что добавление inf в множество чисел ломает групповую операцию. Т.е. с точки зрения теории групп, бесконечность никак не может входить в множество чисел. Но окей, тут действительно можно спорить о выборе аксиоматики для чисел. Давате зайдем с другой стороны.

                        То что вы описываете, называется «расширенной числовой прямой»: к обычным числам мы добавляем две неведомых зверюшки-бесконечности. И даже там деление на ноль не приводит к бесконечности:

                        When dealing with both positive and negative extended real numbers, the expression 1/0 is usually left undefined, because, although it is true that for every real nonzero sequence f that converges to 0, the reciprocal sequence 1/f is eventually contained in every neighborhood of {-inf, +inf} it is not true that the sequence 1/f must itself converge to either -inf or +inf. (wiki)


                        Короче, проблема в том, что 1 / f не сходится к +бесконечности или -бесконечности. Оно сходится и туда и туда одновременно. Собственно, поэтому в анализе и различают «просто» бесконечность, положительную и отрицательную бесконечности.

                        В том время как в IEEE 754 деление положительного числа на ноль дает положительную бесконечность, а отрицательного — отрицательную. Хотя, согласно матану, положительную бесконечность можно получить если брать односторонний предел 1 / f когда f стремиться к нулю справа. А если брать обычный предел, то как было написано выше получится «просто» бесконечность. Которой нет ни на расширенной числовой прямой, ни в IEEE 754.

                        Короче, еще раз: в обычной арифметике на ноль делить можно только сам ноль. На расширенной числовой прямой — тоже. Имеет смысл рассматривать пределы вида 1 / x, когда x->0. Но делить на ноль — все равно нельзя. Зато можно делить на бесконечность, да. И
                          +1
                          Вы зачем-то пытаетесь приплести матан, хотя здесь просто арифметика. В ней нет пределов, разрывов производных и прочей зауми, там только числа и 4 операции.
                          И да, откуда у вас появилось требование единственности нейтрального элемента операции сложения? То, что вы знаете про моноиды, ещё не значит, что всё вокруг обязано быть моноидами.
                            0

                            А если нет никаких пределов и разрывов — то и бесконечностей быть не должно, на нормальной числовой прямой их нет.

                              0
                              на нормальной числовой прямой их нет

                              Почему нет? Вы лично проверяли?:) В некоторых аксиоматиках нет, в других есть. Первые используются в школьных учебниках, вторые — на практике.
                              0
                              Ну так просто в арифметике нет вообще никаких бесконечностей. Им там просто неоткуда взяться. И результат деления на 0 либо не существует (если делим не-ноль), либо — не определен (если делим ноль).

                              И да, откуда у вас появилось требование единственности нейтрального элемента операции сложения?

                              Вообще -то я не говорил про требование единственности нейтрального элемента. Тем более, что бесконечность им не является: а + (-а) = 0, а не inf.

                              Я сказал что добавление бесконечности ломает групповую операцию. Потому что inf + -inf не определено, например.

                              Вообще, если вы не хотите приплетать матан, то тогда вообще непонятно зачем добавлять бесконечности. Пользы то от этого никакой не будет. Ну да, можно будет сказать a / 0 = inf. А дальше что? Профит в чем? Проблемы вижу: придется в каждую теорему теории чисел добавлять фразу наподобие «кроме случаев, когда n = inf». Попробуйте докажите основную теорему арифметики, если у вас могут быть бесконечности…
                                0
                                Вы ищете теоретический смысл, а он тут сугубо практический.
                                Обычная машинная арифметика — это арифметика остатков по модулю 2N. Она не очень-то подходит для обычных вычислений из-за переполнений. Скажем, складывая на 8-битном процессоре 100+100, вы получите результат -56. Мало того, что он некорректен, он выглядит нормально. Если такое произойдёт в процессе вычисления какой-то длинной формулы, вы никак не сможете понять, что результат расчёта неверен. И дальше эта ошибка поползёт по всей системе.
                                Известны тысячи багов типа «дупликации» игровых денег, предметов и т.п. как раз из-за переполнения. Но абсолютно то же самое происходит и в бухгалтерском софте, банковском, системах управления станками и самолётами и т.д. — там, где подобные штуки могут приводить — и приводят — к очень тяжёлым последствиям.
                                В IEEE 754 переполнения нет именно благодаря бесконечностям. Причем, если где-то всплыла бесконечность, вы от неё уже не избавитесь и хотя бы сразу заметите. Это не идеальный способ, но вычисления в числах с произвольной длиной до сих доступны очень не везде.
                                  0
                                  Вы ищете теоретический смысл, а он тут сугубо практический.


                                  Ну как бы да. Математика — теоретическая наука.

                                  Напомню, все началось с того, что вы заявили что в матане 1 / 0 — это бесконечность. Я всего лишь хотел указать на вашу ошибку и предостеречь других.

                                  Она не очень-то подходит для обычных вычислений из-за переполнений.


                                  Зато она быстрая. А где это действительно важно — используется арифметика с насыщением. Некоторые DSP даже аппаратно ее реализуют.

                                  Но абсолютно то же самое происходит и в бухгалтерском софте, банковском, системах управления станками и самолётами и т.д. — там, где подобные штуки могут приводить — и приводят — к очень тяжёлым последствиям.


                                  Ну как бы есть библиотеки, которые реализуют saturating arithmetic, есть поддержка проверки переполнения со стороны компиляторов. Надо просто знать инструмент, которым пользуешься.

                                  Кстати, не знаю за станки и самолеты, но финансы в принципе не используют арифметику с плавающей точкой. Только fixed point (или вообще integer, с расчетами в младших единицах валюты).

                                  В IEEE 754 переполнения нет именно благодаря бесконечностям.

                                  Это все безусловно хорошо. Но мы то говорили про деление на ноль?

                                  Было бы правильно сказать что в IEEE 754 приняты соглашения, которые приводят к тому, что деление ненулевого числа на 0 приводит к плюс или минус бесконечности, в зависимости от знака числа и от знака нуля. А матан тут вообще не при чем.

                                  И кстати, два нуля — тоже нехилое отступление от теоретической арифметики.
                                    0
                                    Математика — теоретическая наука

                                    Прикладные математики мягко скажем удивлены:)
                                    или вообще integer, с расчетами в младших единицах валюты

                                    да, те, которые я видел изнутри, используют целые сотые доли копеек/центов (это позволяет записывать числа типа 12.34%), а десятичная точка выводится только в интерфейсе. И тоже не от хорошей жизни.
                                    два нуля — тоже нехилое отступление от теоретической арифметики.

                                    Это всё же лучше, чем INT_MIN (который -128 в 8-битных целых), который иногда работает как -0, а иногда как нормальное число, и для которого -(-128)=-128
                                      0

                                      Когда это INT_MIN работает как -0?

                                        0
                                        При умножении на нечётные положительные или чётные отрицательные числа, например.
                        0
                        0/0 действительно неопределенность, все логично.
                        А вот дальше я боюсь вы ошибаетесь.
                        Вот если вы попробуете сделать 2/0 — 1/0, тогда снова будет NaN

                        Что-то я вижу что любое другое число деля на 0 выходит Infinity
                        1/0 === Infinity
                        100/0 === Infinity
                        1024/0 === Infinity
                        -512/0 === -Infinity
                        
                          0
                          Да, но если вы возьмёте именно то что я написал (2/0 — 1/0), то будет NaN. Тут вопрос в вычитании, а не в делении. Вычитать бесконечности нельзя.
                            0
                            все. вы имели в виду вычитание бесконечности от бесконечности.
                            не правильно понял ваш коммент
                +5
                > Теперь точки могут отмечать новые строки
                > Нужно только проставлять флаг /s

                Возможно, вы имели в виду:
                Теперь точка в регулярных выражениях может захватывать переводы строк, эмодзи и некоторые другие символы, которые раньше не захватывала, если включить флаг /s.
                  +5
                  > Кстати, перехваты, в которых вы не учитываете значение e, иногда называют обработкой исключений-покемонов. Потому что вы должны поймать их все!

                  Вот это тоже странно.

                  Шутка про исключения-покемоны возникла в джаве, где можно отлавливать исключения по типу. В ES мы всегда «ловим их все», если пишем catch.
                    +2
                    Читают, читают тэги, перестаньте уже
                      0
                      Тэги не читают, а теги — читают.
                      +1
                      Автору спасибо за обзор последних ES, но вот тема ES2019 не раскрыта.
                      Там одни chaining (это про '.?'. Кому перечислить денег за внедрение этой фишки?..) и pipelines чего стоят!
                        0

                        Что вы имеете в виду говоря "pipelines"? Если вы про оператор |> — то он до сих пор в stage 1, и в ES2019 не сможет попасть никаким чудом.

                          0

                          Строго говоря, сейчас ничего новое вообще не может попасть в ES2019, так как ES2019 уже финализирован и утвержден. Но если перейти по ссылке, то там написано, что chaining и pipelines не входят в ES2019, странно, что предыдущий комментатор этого не заметил.

                        0
                        Хмм… `trimStart` уже есть… Осталось дождаться `leftPad`.
                        0
                        Кому нужны двоичные литералы в языке, который не поддерживает целых чисел?
                          0

                          А в чём заключается противоречие?

                            0
                            Обычно такие литералы используются для битовых масок. Но для JS с его 64-битовыми float непонятно, что логические операции с этими масками в принципе делают.
                              +1

                              А что непонятно-то? Результат математической операции не должен зависеть от представления операндов в памяти.


                              3 | 5 будет 7 хоть в С++, хоть в Javascript

                                0
                                Чему будет равен "-1 | 1", а «2.3 | -1»?
                                  0

                                  А какая разница-то? Вы бы ещё про "foo" | 1 спросили...

                                    0
                                    Ну вообще это (выражение a|0) — быстрый способ отбросить дробную часть числа, и, главное, подсказка для интерпретатора, что нужно перейти в целые числа. Если посмотрите asm.js, то там такого полно.
                                      0

                                      Это уже детали реализации, которые к математике отношения не имеют.

                                0
                                Если числа целые (то есть если у них нет дробной части), то делают именно то, что вы ожидаете от битовых операций над беззнаковыми числами.
                                  0
                                  Хм, обнаружил, что применение битовых операций к числу в хроме приводит их к целочисленному значению. А значит можно делить целочисленно: a/b | 0, не используя Math.floor, который округляет отрицательные числа не так, как хотелось бы нематематикам.
                                    0
                                    а ещё num >>> 0 и получем uint32. И полноценная арифметика на интах работает
                                      0

                                      Это не только в хроме, это в стандарте так. Трюк с |0 даже вошёл в asm.js как указание на целое число. Но это всё следствия динамической типизации, операция | не для этого придумывалась.

                                    +2
                                    В JS-движках (по крайней мере в V8) есть оптимизации, которые вычленяют короткое целочисленное значение из числовых операндов при возможности и оперируют уже с ними. Рантаймовая магия, о которой юзерам языка думать не нужно.
                                  +1

                                  В свежем Chrome уже можно попробовать BigInt: typeof 2n

                                  0
                                  Кстати есть еще Object.fromEntries, только в IE и Edge нет
                                    +1
                                    const regex = /(?<=\$)\d+/;
                                    const text = 'This cost $400';
                                    text.match(regex) === ['400'] // false ибо сравнение с массивом

                                    Будет более верне сравнивать примитивы:

                                    text.match(regex)[0] === '400'

                                    Only users with full accounts can post comments. Log in, please.