Автор материала, перевод которого мы сегодня публикуем, говорит, что новые возможности JavaScript, которые попали в стандарт ES2019, уже официально доступны в браузерах Chrome, Firefox и Safari, а также на платформе Node.js. Если нужно поддерживать устаревшие браузеры, то воспользоваться новшествами можно, транспилируя JS-код с помощью Babel. Здесь мы рассмотрим примеры использования некоторых новых возможностей JS.



Метод Object.fromEntries


В ES2017 появился метод Object.entries. Он преобразует объект в массив. Например, это может выглядеть так:

let students = {
  amelia: 20,
  beatrice: 22,
  cece: 20,
  deirdre: 19,
  eloise: 21
}

Object.entries(students) 
// [
//  [ 'amelia', 20 ],
//  [ 'beatrice', 22 ],
//  [ 'cece', 20 ],
//  [ 'deirdre', 19 ],
//  [ 'eloise', 21 ]
// ]

Этот метод стал замечательным дополнением к возможностям языка. Дело в том, что он позволял удобно обрабатывать данные объектов с помощью многочисленных методов, встроенных в прототип Array. Среди этих методов, например, можно отметить map, filter, reduce. Но для того, чтобы преобразовать массив обратно в объект, к сожалению, удобных средств не существовало. Всё приходилось делать вручную, с помощью цикла:

let students = {
  amelia: 20,
  beatrice: 22,
  cece: 20,
  deirdre: 19,
  eloise: 21
}

// преобразуем объект в массив для того чтобы воспользоваться методом .filter()
let overTwentyOne = Object.entries(students).filter(([name, age]) => {
  return age >= 21
}) // [ [ 'beatrice', 22 ], [ 'eloise', 21 ] ]

// преобразуем многомерный массив обратно в объект
let drinkingAgeStudents = {}
for (let [name, age] of overTwentyOne) {
    drinkingAgeStudents[name] = age;
}
// { beatrice: 22, eloise: 21 }

Метод Object.fromEntries создан для того чтобы избавиться от подобных циклов. Он позволяет решить ту же самую задачу с помощью гораздо меньшего объёма кода. Это вполне может способствовать тому, чтобы разработчики чаще пользовались бы методами массивов для обработки преобразованных в массивы объектов.

let students = {
  amelia: 20,
  beatrice: 22,
  cece: 20,
  deirdre: 19,
  eloise: 21
}

// преобразуем объект в массив для того чтобы воспользоваться методом .filter()
let overTwentyOne = Object.entries(students).filter(([name, age]) => {
  return age >= 21
}) // [ [ 'beatrice', 22 ], [ 'eloise', 21 ] ]

// преобразуем многомерный массив обратно в объект
let drinkingAgeStudents = Object.fromEntries(overTwentyOne); 
// { beatrice: 22, eloise: 21 }

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

let students = [
  [ 'amelia', 22 ], 
  [ 'beatrice', 22 ], 
  [ 'eloise', 21], 
  [ 'beatrice', 20 ]
]

let studentObj = Object.fromEntries(students); 
// { amelia: 22, beatrice: 20, eloise: 21 }
// пропала первая запись beatrice!

?Поддержка


  • Chrome 75
  • Firefox 67
  • Safari 12.1

Метод Array.prototype.flat


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

Рассмотрим следующий пример. Здесь, в результате обработки массива объектов с помощью функции map, у нас оказывается многомерный массив. Его мы хотим сделать более «плоским».

let courses = [
  {
    subject: "math",
    numberOfStudents: 3,
    waitlistStudents: 2,
    students: ['Janet', 'Martha', 'Bob', ['Phil', 'Candace']]
  },
  {
    subject: "english",
    numberOfStudents: 2,
    students: ['Wilson', 'Taylor']
  },
  {
    subject: "history",
    numberOfStudents: 4,
    students: ['Edith', 'Jacob', 'Peter', 'Betty']
  }
]

let courseStudents = courses.map(course => course.students)
// [
//   [ 'Janet', 'Martha', 'Bob', [ 'Phil', 'Candace' ] ],
//   [ 'Wilson', 'Taylor' ],
//   [ 'Edith', 'Jacob', 'Peter', 'Betty' ]
// ]

// тут мы могли бы попытаться воспользоваться чем-то вроде [].concat.apply([], courseStudents)

Теперь в нашем распоряжении имеется метод Array.prototype.flat, который принимает необязательный аргумент, указывающий то, на какой уровень надо «поднять» элементы массива.

let courseStudents = [
  [ 'Janet', 'Martha', 'Bob', [ 'Phil', 'Candace' ] ],
  [ 'Wilson', 'Taylor' ],
  [ 'Edith', 'Jacob', 'Peter', 'Betty' ]
]

let flattenOneLevel = courseStudents.flat(1)
console.log(flattenOneLevel)
// [
//   'Janet',
//   'Martha',
//   'Bob',
//   [ 'Phil', 'Candace' ],
//   'Wilson',
//   'Taylor',
//   'Edith',
//   'Jacob',
//   'Peter',
//   'Betty'
// ]

let flattenTwoLevels = courseStudents.flat(2)
console.log(flattenTwoLevels)
// [
//   'Janet',   'Martha',
//   'Bob',     'Phil',
//   'Candace', 'Wilson',
//   'Taylor',  'Edith',
//   'Jacob',   'Peter',
//   'Betty'
// ]

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

let courseStudents = [
  [ 'Janet', 'Martha', 'Bob', [ 'Phil', 'Candace' ] ],
  [ 'Wilson', 'Taylor' ],
  [ 'Edith', 'Jacob', 'Peter', 'Betty' ]
]

let defaultFlattened = courseStudents.flat()
console.log(defaultFlattened)
// [
//   'Janet',
//   'Martha',
//   'Bob',
//   [ 'Phil', 'Candace' ],
//   'Wilson',
//   'Taylor',
//   'Edith',
//   'Jacob',
//   'Peter',
//   'Betty'
// ]

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

let courseStudents = [
  [ 'Janet', 'Martha', 'Bob', [ 'Phil', 'Candace' ] ],
  [ 'Wilson', 'Taylor' ],
  [ 'Edith', 'Jacob', 'Peter', 'Betty' ]
]

let alwaysFlattened = courseStudents.flat(Infinity)
console.log(alwaysFlattened)
// [
//   'Janet',   'Martha',
//   'Bob',     'Phil',
//   'Candace', 'Wilson',
//   'Taylor',  'Edith',
//   'Jacob',   'Peter',
//   'Betty'
// ]

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

?Поддержка


  • Chrome 75
  • Firefox 67
  • Safari 12

Метод Array.prototype.flatMap


Вместе с методом flat в нашем распоряжении теперь оказался и новый комбинированный метод — Array.prototype.flatMap. Выше мы, на самом деле, уже видели пример ситуации, в которой этот метод может пригодиться, но давайте рассмотрим ещё один пример.

Предположим, перед нами стоит задача вставки неких элементов в массив. Как мы решили бы её раньше, до появления новых возможностей JS? Например — так:

let grades = [78, 62, 80, 64]

let curved = grades.map(grade => [grade, grade + 7])
// [ [ 78, 85 ], [ 62, 69 ], [ 80, 87 ], [ 64, 71 ] ]

let flatMapped = [].concat.apply([], curved) 
// теперь массив оказался плоским. Тут можно было бы использовать метод flat, но раньше этого метода в JS не существовало
// [
//  78, 85, 62, 69,
//  80, 87, 64, 71
// ]

Теперь, когда у нас есть метод Array.prototype.flat, этот код можно улучшить:

let grades = [78, 62, 80, 64]

let flatMapped = grades.map(grade => [grade, grade + 7]).flat()
// [
//  78, 85, 62, 69,
//  80, 87, 64, 71
// ]

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

let grades = [78, 62, 80, 64]

let flatMapped = grades.flatMap(grade => [grade, grade + 7]);
// [
//  78, 85, 62, 69,
//  80, 87, 64, 71
// ]

Вспомните о том, что по умолчанию метод Array.prototype.flat работает так, будто ему передана единица. Метод flatMap ведёт себя точно так же, то есть — «поднимает» элементы массива лишь на 1 уровень. Он представляет собой результат комбинации методов map и flat.

let grades = [78, 62, 80, 64]

let flatMapped = grades.flatMap(grade => [grade, [grade + 7]]);
// [
//   78, [ 85 ],
//   62, [ 69 ],
//   80, [ 87 ],
//   64, [ 71 ]
// ]

?Поддержка


  • Chrome 75
  • Firefox 67
  • Safari 12

Методы String.prototype.trimStart и String.prototype.trimEnd


Ещё одно приятное новшество ES2019 — это псевдонимы, которые дают некоторым строковым методам более понятные имена. Раньше в нашем распоряжении были методы String.prototype.trimRight и String.prototype.trimLeft:

let message = "   Welcome to CS 101    "
message.trimRight()
// '   Welcome to CS 101'
message.trimLeft()
// 'Welcome to CS 101   '
message.trimRight().trimLeft()
// 'Welcome to CS 101'

Методы это замечательные, но хорошо то, что им дали имена, которые больше соответствуют их цели. А цель их заключается в удалении начальных и конечных пробельных символов из строк.

let message = "   Welcome to CS 101    "
message.trimEnd()
// '   Welcome to CS 101'
message.trimStart()
// 'Welcome to CS 101   '
message.trimEnd().trimStart()
// 'Welcome to CS 101'

?Поддержка


  • Chrome 75
  • Firefox 67
  • Safari 12

Необязательный аргумент блока catch


Ещё одна приятная возможность ES2019 — это то, что аргумент в блоках try-catch теперь стал необязательным. Ранее всем блокам catch надо было передавать, в качестве параметра, объект исключения. Аргумент приходилось передавать catch даже в том случае, если он не использовался.

try {
  let parsed = JSON.parse(obj)
} catch(e) {
  // e можно игнорировать или использовать
  console.log("error")
}

Теперь это не так. Если объект исключения не используется в блоке catch — тогда в этот блок не нужно и ничего передавать.

try {
  let parsed = JSON.parse(obj)
} catch {
  console.log("error")
}

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

?Поддержка


  • Chrome 75
  • Firefox 67
  • Safari 12

Изменения в методе Function.prototype.toString


Стандарт ES2019 принёс изменения в то, как работает метод функций toString. Ранее он немного искажал оформление выводимого кода:

function greeting() {
  const name = 'CSS Tricks'
  console.log(`hello from ${name}`)
}

greeting.toString()
//'function greeting() {\nconst name = \'CSS Tricks\'\nconsole.log(`hello from ${name} //`)\n}

Теперь этот метод отражает реальное представление исходного кода функций.

function greeting() {
  const name = 'CSS Tricks'
  console.log(`hello from ${name}`)
}

greeting.toString()
// 'function greeting() {\n' +
//  "  const name = 'CSS Tricks'\n" +
//  '  console.log(`hello from ${name}`)\n' +
//  '}'

?Поддержка



Итоги


Здесь мы рассмотрели примеры использования лишь совсем немногих новых возможностей JavaScript. Если вы интересуетесь новшествами JS — загляните в этот репозиторий и в эту таблицу.

Уважаемые читатели! Сталкивались ли вы с ситуациями, в которых новые возможности JS заметно упрощают решение каких-нибудь задач?

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


  1. pterodaktil
    26.08.2019 14:29
    -1

    Аж глаза заболели от повсеместного let


    1. messersveet
      26.08.2019 15:36
      +1

      Чтобы глаза привыкли к let, надо сначала закрыть их, досчитать до десяти, и плавно открывать.
      Ну а вообще, 2019 годе, же, ну.


      1. pterodaktil
        26.08.2019 15:57

        2019 год же, const


        1. Tenebrius
          26.08.2019 17:50

          На два символа больше. Так много печатать лень =)


          1. codemafia
            26.08.2019 18:36

            Это полиция хорошего кода! Немедленно отойдите от клавиатуры в окно.


            1. justboris
              26.08.2019 18:58

              А вы точно полиция? У вас никнейм отклеился


  1. mayorovp
    26.08.2019 15:23

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


    Хотя на самом деле, раньше оно теряло выравнивание, а теперь не теряет.


  1. Keyten
    26.08.2019 17:35

    trimStart / trimEnd ведут к очевидным проблемам с rtl текстом: ведь как начало, так и конец строки находятся наоборот, и кто-то может ожидать, что trimStart на арабском тексте удалит пробелы справа.


    1. mayorovp
      27.08.2019 08:29
      +1

      Так ведь оно и правда удаляет пробелы справа… Собственно, потому от названий trimLeft/trimRight и отказались.


  1. karantir
    26.08.2019 17:37

    Продолжается портирование underscore в стандартную библиотеку. Логично.


  1. aleksandy
    27.08.2019 08:24
    -1

    Если объект исключения не используется в блоке catch

    То это плохой код.

    Исключение либо надо как-то обрабатывать, хотя бы в тот же лог записать, либо вообще не ловить.


    1. mayorovp
      27.08.2019 08:30

      Если это feature detect — то не нужно. Если исключение было при попытке записать что-то в лог — то его тоже в лог записывать не нужно.


      1. aleksandy
        27.08.2019 13:16

        1. А можно пример подобного feature detect? Я что-то не представляю, как по факту наличия какого-то (неизвестного!!!) исключения можно сделать вывод, о доступности/недоступности чего-либо.
        2. Как часто Вы заворачиваете console.log() в try-блок?


        1. mayorovp
          27.08.2019 13:23
          +1

          let templateEngine;
          
          try {
              eval("true");
              templateEngine = jitTemplateEngine;
          } catch {
              templateEngine = interpreterTemplateEngine;
          }


          1. justboris
            27.08.2019 18:53

            По-хорошему, в catch-блоке надо проверить тип ошибки, и выбросить ее дальше если он неправильный. Сэкономит потом время на отлов багов в этом месте


            1. mayorovp
              27.08.2019 18:58

              Но тут есть два вопроса:


              1. Какой тип ошибки предполагается увидеть? В стандарте не указывается какое исключение кидает eval будучи запрещённой через CSP!


              2. Какое ещё исключение в принципе возможно в этом блоке? От чего предполагается защищаться?



              1. justboris
                28.08.2019 04:15

                Хотя бы проверять, что это был EvalError, а не какой-то еще. Можно получить ReferenceError: jitTemplateEngine is not defined, если где-нибудь в имени переменной опечататься.


                1. mayorovp
                  28.08.2019 06:25

                  Так как вы надёжно отличите EvalError от какого-то ещё, если стандарт не указывает какоё именно исключение выбрасывать?


                  1. justboris
                    28.08.2019 08:46
                    +1

                    ?\(?)/?


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


        1. noodles
          28.08.2019 00:27
          +1

          1. А можно пример подобного feature detect? Я что-то не представляю, как по факту наличия какого-то (неизвестного!!!) исключения можно сделать вывод, о доступности/недоступности чего-либо.

          function isTouchDevice() {
            try {
              document.createEvent('TouchEvent');
              return true;
            }
            catch {
              return false;
            }
          }
          


  1. torbasow
    27.08.2019 12:39

    Не очень понятно, зачем бы на практике так часто уплощать массив, чтобы потребовались специальные методы.