Стандарт EcmaScript 2015 (ES6) существует уже несколько лет. Он принёс с собой множество новых возможностей, разные способы использования которых далеко не всегда очевидны. Вот обзор некоторых из этих возможностей с примерами и комментариями.

image

1. Обязательные параметры функции


ES6 позволяет задавать значения формальных параметров по умолчанию, что позволяет, при вызове функции без указания значений этих параметров, подставлять их стандартные значения. Это позволяет задавать параметры, без передачи которых функция работать не будет.

В следующем примере мы задаём функцию required() как значение по умолчанию для параметров a и b. Это означает, что если a или b не будут переданы функции при вызове, будет вызвана функция required() и мы получим сообщение об ошибке.

const required = () => {throw new Error('Missing parameter')};
//При вызове этой функции произойдёт ошибка, если параметры "a" или "b" не заданы
const add = (a = required(), b = required()) => a + b;
add(1, 2) //3
add(1) // Error: Missing parameter.

2. Секреты метода reduce


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

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

?2.1. Использование reduce для одновременного выполнения мэппинга и фильтрации массива


Представим, что перед нами стоит следующая задача. Имеется список элементов, каждый из которых надо модифицировать (что сводится к использованию метода map), после чего отобрать из него несколько элементов (это можно сделать с помощью метода filter). Эта задача вполне решаема последовательным применением методов map и filter, но так по списку элементов придётся пройтись дважды. А нас это не устраивает.

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

const numbers = [10, 20, 30, 40];
const doubledOver50 = numbers.reduce((finalList, num) => {
  
  num = num * 2; //удвоить каждое число (аналог map)
  
  //отобрать числа > 50 (аналог filter)
  if (num > 50) {
    finalList.push(num);
  }
  return finalList;
}, []);
doubledOver50; // [60, 80]

?2.2. Использование reduce вместо map или filter


Если вы проанализировали вышеприведённый пример, то для вас окажется совершенно очевидной возможность использования метода reduce вместо map или filter.

?2.3. Использование reduce для анализа расстановки скобок


Вот ещё один пример полезных возможностей метода reduce.

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

Эту задачу можно решить с помощью метода reduce так, как показано ниже. Тут мы используем переменную counter с начальным значением, равным 0. Мы увеличиваем её значение на единицу, если находим символ «(», и уменьшаем, если находим символ «)». Если скобки в строке сбалансированы, на выходе должен получиться 0.

//Возвращает 0 если скобки сбалансированы.
const isParensBalanced = (str) => {
  return str.split('').reduce((counter, char) => {
    if(counter < 0) { //matched ")" before "("
      return counter;
    } else if(char === '(') {
      return ++counter;
    } else if(char === ')') {
      return --counter;
    }  else { //найден какой-то другой символ
      return counter;
    }
    
  }, 0); //<-- начальное значение для счётчика
}
isParensBalanced('(())') // 0 <-- скобки сбалансированы
isParensBalanced('(asdfds)') //0 <-- скобки сбалансированы
isParensBalanced('(()') // 1 <-- скобки несбалансированы
isParensBalanced(')(') // -1 <-- скобки несбалансированы

?2.4. Подсчёт количества совпадающих значений массива (преобразование массива в объект)


Иногда нужно подсчитать одинаковые элементы массива или преобразовать массив в объект. Для решения этих задач также можно использовать reduce.

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

var cars = ['BMW','Benz', 'Benz', 'Tesla', 'BMW', 'Toyota'];
var carsObj = cars.reduce(function (obj, name) { 
   obj[name] = obj[name] ? ++obj[name] : 1;
  return obj;
}, {});
carsObj; // => { BMW: 2, Benz: 2, Tesla: 1, Toyota: 1 }

На самом деле, возможности reduce на этом не ограничиваются. Если вам интересны подробности об этом полезном методе — изучите примеры с этой страницы.

3. Деструктурирование объектов


?3.1. Удаление ненужных свойств


Бывает так, что из объекта требуется удалить ненужные свойства. Возможно, это свойства, которые содержат секретные сведения, возможно — они слишком велики. Вместо того, чтобы перебирать весь объект и удалять подобные свойства, можно извлечь эти свойства в переменные и оставить нужные свойства в rest-параметре.

В следующем примере нам нужно избавиться от свойств _internal и tooBig. Их можно присвоить переменным с такими же именами и сохранить оставшиеся свойства в rest-параметре cleanObject, который можно использовать позже.

let {_internal, tooBig, ...cleanObject} = {el1: '1', _internal:"secret", tooBig:{}, el2: '2', el3: '3'};
console.log(cleanObject); // {el1: '1', el2: '2', el3: '3'}

?3.2. Деструктурирование вложенных объектов в параметрах функции


В следующем примере свойство engine — это вложенный объект объекта car. Если нам нужно узнать, скажем, свойство vin объекта engine, его легко можно деструктурировать.

var car = {
  model: 'bmw 2018',
  engine: {
    v6: true,
    turbo: true,
    vin: 12345
  }
}
const modelAndVIN = ({model, engine: {vin}}) => {
  console.log(`model: ${model} vin: ${vin}`);
}
modelAndVIN(car); // => model: bmw 2018  vin: 12345

?3.3. Слияние объектов


ES6 поддерживает оператор расширения (spread), выглядящий как три точки. Обычно его применяют для работы с массивами, но его можно использовать и с обычными объектами.

В следующем примере мы используем оператор расширения для формирования нового объекта из двух существующих. Ключи свойств объекта №2 переопределят ключи свойств объекта №1. В частности, свойства b и c из object2 переопределят такие же свойства объекта object1.

let object1 = { a:1, b:2,c:3 }
let object2 = { b:30, c:40, d:50}
let merged = {…object1, …object2} //применим оператор расширения для формирования нового объекта
console.log(merged) // {a:1, b:30, c:40, d:50}

4. Коллекции


?4.1. Удаление повторяющихся значений из массивов


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

let arr = [1, 1, 2, 2, 3, 3];
let deduped = [...new Set(arr)] // [1, 2, 3]

?4.2. Использование методов массивов с коллекциями


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

Предположим, у нас имеется коллекция, которую нужно отфильтровать так, чтобы в ней содержались только элементы, которые больше 3. Вот как это сделать.

let mySet = new Set([1,2, 3, 4, 5]);
var filtered = [...mySet].filter((x) => x > 3) // [4, 5]

5. Деструктурирование массивов


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

?5.1. Обмен значений переменных


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

let param1 = 1;
let param2 = 2;
//поменяем местами значения param1 и param2 
[param1, param2] = [param2, param1];
console.log(param1) // 2
console.log(param2) // 1

?5.2. Приём и обработка нескольких значений, возвращаемых функцией


В следующем примере мы загружаем некую статью, находящуюся по адресу /post, и связанные с ней комментарии с адреса /comments. Тут применяется конструкция async/await, функция возвращает результат в виде массива.

Деструктурирование упрощает присвоение результатов работы функции соответствующим переменным.

async function getFullPost(){
  return await Promise.all([
    fetch('/post'),
    fetch('/comments')
  ]);
}
const [post, comments] = getFullPost();

Итоги


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

Уважаемые читатели! Если вы знаете какие-нибудь интересные приёмы использования возможностей ES6 — просим ими поделиться.

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


  1. k12th
    27.03.2018 15:52

    Строго говоря, async/await и ... для объектов в ES6 (то есть ES2015) не входят. Первое попало только в ES2017, второе будет в ES2018.


    А reduce крутая вещь, да.


    1. staticlab
      27.03.2018 16:18
      +2

      Но reduce был ещё в ES5.


      1. k12th
        27.03.2018 16:21
        +1

        Действительно. В общем, не очень точное название у статьи:)


  1. vlreshet
    27.03.2018 17:35

    Фишка с required аргументами понравилась, это та вещь которой по непонятной причине нет «из коробки»


    1. staticlab
      27.03.2018 17:48

      Если у вас транспилируется stage 2, то можно писать и так:


      const requiredArgument = new Error('Missing argument')
      const add = (a = throw new Error('Argument `a` is required'), b = throw new Error('Argument `b` is required')) => a + b;


      1. vitaliy2
        27.03.2018 20:17

        Вообще, проверка как в статье бесполезна, т.?к. чаще всего, если делается проверка аргумента, то будет сделана и проверка типа, и даже, возможно, диапазона значений. Здесь же только проверка, что не undefined, да и со стандартной ошибкой, что как-то скупо…

        Но если бы можно было обозначать примерно в таком стиле, то мб и пошло:

        function(arg!, otherArg!) {}

        Тоже только на undefined, но короче и с нормальной ошибкой.


        1. Keyten
          28.03.2018 11:56

          Почему бы и да?


          const required = varName => throw new Error(`Variable ${varName} shouldnt be undefined`);
          
          function myFunc(a = required('a')){...}


          1. Tarik02
            28.03.2018 15:14

            Два раза повторять название параметра, не очень удобно.


  1. Aquahawk
    27.03.2018 18:03
    -1

    var filtered = [...mySet].filter((x) => x > 3) // [4, 5]

    напишите тестик на производительность этого на сете хотяб на пару тысяч элементов, а лучше больше, по сравнению с foreach по этому сету где сложат всё нужное в отдельный заранее созданный массив. И прогоните на современных браузерах включая мобилки. Я прям вот специально сейчас не пойду и не буду писать такой тест, потому что делал подобное уже 100 раз. Гарантирую подлинное удовольствие каждому разработчику который будет это делать.


    1. Aquahawk
      27.03.2018 20:09

      вот из старых примеров например умножение матрицы jsperf.com/letfor2016



  1. IonianWind
    27.03.2018 18:12
    +3

    async function getFullPost(){
      return await Promise.all([
        fetch('/post'),
        fetch('/comments')
      ]);
    }
    const [post, comments] = getFullPost();

    Так работать не будет


    Нужно вызывать (внутри async-функции) так:


    const [post, comments] = await getFullPost();

    Или, на худой конец, так:


    getFullPost()
        .then([post, comments] => {
            /*some code*/
        })
        .catch(error => {
            /*some code*/
        });

    Потому что функция getFullPost возвращает не массив, а Promise


    1. Hatsmith
      27.03.2018 20:10
      -1

      Я конечно не спец по джаваскрипту. Но вроде getFullPost возвращает уже выполненный (не уверен как правильно назвать) промис?
      Там стоит return await Promise...


      1. Keyten
        28.03.2018 11:57

        А перед этой функцией стоит async. Такая функция всегда возвращает промис.


    1. wert_lex
      27.03.2018 20:12

      Более того, return await считается антипаттерном, ибо не несёт никакого смысла.


      1. vitaliy2
        27.03.2018 20:26
        -1

        В данном случае более чем несёт. При return await промис, созданный getFullPost, будет заполнен массивом с двумя значениями, а без return будет заполнен undefined.

        В данном случае можно было бы вообще убрать async await, но это может:

        1. Нарушать стиль (раньше все async-функции использовали async и await, а теперь нет)
        2. Усложнять код (надо перечитывать код, чтобы убедиться, что мы возвращаем нужный промис, а при вызове функции используем await)
        3. Если потребуется добавить ещё await'ы в функцию, и тогда убранный async await придётся возвращать


        1. mayorovp
          27.03.2018 20:30

          Пункт 2 — мимо, потому что при вызове оператор await нужно ставить или не ставить независимо от того объявлена ли функция как async


          1. vitaliy2
            27.03.2018 20:39

            Имелось ввиду, что всем итак понятно, что async-функция возвращает промис, и если у нас 95% функций — это async, то когда попадётся асинхронная функция, не помеченная как async, ты сразу начнёшь думать, а нет ли такого, что я где-то в коде не заметил, что это на самом деле асинхронная функция, и с ней тоже надо было использовать await.

            Т.?е. ты привык, что твои асинхронные функции выделены как async и уже на автомате используешь await с ними, а тут функция выбивается из стандартного шаблона — она не async, но всё-равно возвращает промис, и всё-равно надо использовать await.

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

            Ну и просто return await Promise может читаться лучше и быть меньше подвержен ошибкам. Поэтому может быть проще.


        1. wert_lex
          27.03.2018 20:48

          В данном случае более чем несёт. При return await промис, созданный getFullPost, будет заполнен массивом с двумя значениями, а без return будет заполнен undefined.

          Нет, разницы никакой нет.
          return await "дождется" результата Promise.all и "завернёт" его в промис, return ничего ждать не будет и сразу вернёт промис.
          В кавычках, потому что это чуть иначе работает на самом деле, но смысл именно такой.


          1. vitaliy2
            27.03.2018 20:54

            Я почему-то подумал про опускание return. Вообще да, если опустить await, результат будет тот же самый — он всё-равно дождётся этого промиса и только тогда заполнит автосозданный промис.

            До появления официальной спецификации это, кстати, не поддерживалось, вроде, в Babel, и промис заполнялся промисом, а не его результатом.


            1. wert_lex
              27.03.2018 21:02

              А тут чуть хитрее: Promise<Promise<A>> === Promise<A>


              1. vitaliy2
                27.03.2018 21:09

                Ага, это встроено в сам промис, а не async: когда мы вызываем resolve(anotherPromise), он будет его ждать. Resolve в anotherPromise тоже при этом может ждать, и также и далее.


              1. burdakovd
                28.03.2018 13:40

                С точки зрения типизации выглядит адово и не нужно.

                Почему язык за меня решает что я хотел вернуть

                Promise<A>
                если может быть я действительно хотел вернуть
                Promise<Promise<A>>
                ?

                Ну и
                Promise<A>
                в общем случае не эквивалентен
                A
                , но если `A` в свою очередь является промисом, то срабатывает особая логика.

                Вам бы понравилось если бы скажем `push()` для массива определял случай когда аргумент является массивом, и делал `concat()` вместо `push()`?


                1. mayorovp
                  28.03.2018 13:54
                  +1

                  Зато concat умеет делать push если аргумент не является массивом…


                1. vitaliy2
                  29.03.2018 21:47

                  Почему язык за меня решает что я хотел вернуть
                  Так сложилось исторически: когда async-await ещё не было, это было способом строить цепочки промисов, например:
                  promise.then(v => {
                  	return anotherPromise;
                  }).then(v2 => {
                  	return someAnotherPromiseAgain;
                  }).then(v3 => {
                  	…
                  });

                  Если бы он не ждал anotherPromise, то в v2 мы бы получали не значение, а мгновенно этот промис, который в данном случае бесполезен.


                  1. vitaliy2
                    29.03.2018 21:56

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

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


      1. kirill89
        28.03.2018 10:17

        Не совсем. Например при использовании try-catch делать return await очень даже оправдано:

        async function t() { return Promise.reject(new Error('t')); }
        async function a() {
          try {
          	return await t();
          } catch(e) { console.log(`In a: ${e}`); }
        }
        async function b() {
          try {
          	return t();
          } catch(e) { console.log(`In b: ${e}`); }
        }
        
        a().catch(e => console.log(`Outside of a: ${e}`));
        b().catch(e => console.log(`Outside of b: ${e}`));
        
        // In a: Error: t
        // Outside of b: Error: t
        


        Получается что наобороть return без await в async функциях — антипаттерн. Можно потерять catch. Я понимаю что это довольно редкий случай, но это делает его еще более опастным.


        1. wert_lex
          29.03.2018 09:33

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


          Но пример годный :)


    1. Sayonji
      28.03.2018 13:03

      ru_vds Надо исправить, люди же читают и запоминают неправильно!


  1. AlexComin
    27.03.2018 18:14

    Отличная, статья. Без воды, как в принципе и все остальные Ваши статьи.


  1. abarkov
    27.03.2018 18:28

    Я так понимаю что «Слияние Объектов» это то же самое что Object.assign?
    Можно ли сделать вот так, чтоб не мутировать object1 и object2 или они и так не мутируют?

    const object3 = { {}, ...object1, ...object2}


    UPD: проверил, объекты не мутируют, крутая фича.


    1. justboris
      27.03.2018 18:42

      Первый пустой объект вам не нужен, он и так уже как бы объявлен окружающими скобками


      const object3 = { ...object1, ...object2 }

      Babel как раз это разложит в Object.assign({}, object1, object2). Пример


    1. wert_lex
      27.03.2018 20:13

      Да, стандарт требует одинакового поведения для {...a, ...b} и Object.assign({}, a, b)


  1. ReklatsMasters
    27.03.2018 19:47

    В первом примере довольно не очевидно когда вызывается функция required() — в момент объявления add или в момент вызова. Поэтому я предпочту не запутывать себя и буду контролировать аргументы внутри функции.


    1. vitaliy2
      27.03.2018 20:45

      В момент вызова. Просто перечитайте доки.


      1. pesh1983
        28.03.2018 09:14

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


        1. vitaliy2
          28.03.2018 14:42

          Не понял Вас. Оно всегда в момент вызова. Или Вы про другие языки?


          1. pesh1983
            29.03.2018 00:27

            Я про то, что поведение в данном контексте совсем неочевидно. В данном случае каждая функция, привязанная к аргументу как значение по умолчанию, выполняется каждый раз (или один раз?) при необходимости посчитать значение по умолчанию для аргумента, а не единожды в момент создания искомой функции с этими аргументами. И если в любом другом контексте выражение имя_функции() имеет однозначную трактовку, а именно исполнение кода этой функции сразу, как только интерпретатор добирается до этого места в коде, то в данном контексте оно так не работает.
            Вдвойне запутывает ситуация, когда надо вычислить значения аргументов сразу при инициализации функции. Предположу, что это делается как-то так


            const required = (p1, p2) => {...};
            defaultA = required(v1, v2);
            defaultB = required(v3, v4);
            const add = (a = defaultA, b = defaultB) => a + b;

            Но почему не тем синтаксисом, что описан в статье? Казалось бы, это же поведение конструкции по умолчанию.
            По сути тут меняется поведение базовой конструкции в зависимости от контекста, что-то типа "оно работает так, но вот в этом конкретном случае — по-другому". Это ведёт к тому, что вам нужно ещё и контекст контролировать, что добавляет дополнительную бессмысленную сложность при чтении кода. И это при том, что во всех остальных языках, в которых такая конструкция именно вызывает функцию, это поведение соблюдается везде, независимо от контекста. И нет никакой двоякой трактовки, конструкция везде работает одинаково — выполняет функцию в том месте, где встречается это выражение.
            Почему например, нельзя было придумать новый синтаксис для выполнения функции в контексте аргументов по умолчанию? Такое же уже делали (arrow functions, например). Тогда бы каждая конструкция однозначно трактовалась независимо от контекста. Как пример,


            const required = () => {...};
            const add = (a = @required, b = @required) => a + b;

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


            1. vitaliy2
              29.03.2018 00:35

              Я же говорю, в js оно вычисляется заново при каждом вызове функции. Вы что-то перепутали. Смотрите здесь раздел «Вычисляются во время вызова».

              поведение меняется в зависимости от контекста
              В том то и дело, что здесь поведение никогда не меняется: оно всегда вычисляется в момент вызова функции, и так в любом контексте. Если функция была вызвана 100 раз, то и будет вычислено 100 раз. Вы просто что-то перепутали.


              1. pesh1983
                29.03.2018 09:42

                Да, вы правы. Все выражение вычисляется каждый раз по-новому, а не только при создании функции. Мне почему-то казалось, что это делается только для функций. Вот такая конструкция работает ожидаемо


                let a = 1;
                let b = 2;
                let fn = (c = a + b) => c;
                fn() == 3
                a = 2
                fn() == 4

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


                let a = 1;
                let b = 2;
                let argA = a; // только так безопасно далее менять a
                let fn = (c = argA + b) => c;
                ...
                a = 2; // не скажется на результате вычисления функции

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


                let fn = function (c = a + b) { 
                    let fn1 = (k = c + a) => k;
                    ... // тут мы что-то могли сделать с 'c'
                    return fn1();
                }
                ... // ну и тут мы что-то могли сделать с 'a' и 'b' дополнительно

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


                let argFn = () => 1;
                let fn = (c = @argA) => c;

                или разделить выполнение сразу и на каждый вызов, типа


                let fn = (c = a + b) => c; // сразу
                let fn = (c = {a + b}) => c; // на каждый вызов

                Это примеры, синтаксис можно придумать любой. Основная суть в том, чтобы добавить гибкости. Но все вышеперечисленное — это конечно, ИМХО.


                1. vitaliy2
                  29.03.2018 11:32

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

                  Ну а если нужны те же самые, никто не мешает сохранить во внешнюю переменную.

                  А те минусы, которые Вы привели — это просто от плохого знания замыканий. Для тех, кто давно пишет на js или других языках с замыканиями, такие вещи более чем очевидны. А вот кто недавно познакомился, пока ещё могут плохо понимать. Это не минус js. Сами по себе замыкания иногда очень полезны (без них не получится работать с функциями как с объектом), просто не нужно ожидать, что все языки одинаковые. Можете считать, что в js просто более развитое ООП (хотя приватные свойства, да, нельзя).

                  Ну и если Вы боитесь, что значение переменной может измениться, слово const вместо let тоже никто не отменял.


  1. vitaliy2
    27.03.2018 20:09

    2.3. Использование reduce для анализа расстановки скобок

    Конечно понятно, что приведённые пункты с reduce — это показание возможностей, а не советы, но раз уж на то пошло, возможно, вместо

    return str.split('').reduce((counter, char) => {

    стоит написать:
    return [...str].reduce((counter, char) => {

    Хотя результат не изменится, т.?к. при подсчёте скобок символы с кодом больше 65535 не повлияют (работать тоже, возможно, будет медленнее). Но зато наглядно показываются возможности ES6, а также делается намёк на существование суррогатных пар.

    2.4. Подсчёт количества совпадающих значений массива

    Здесь три обращения к хэшу (когда элемент дублируется):
    obj[name] = obj[name] ? ++obj[name] : 1;

    Поэтому правильнее написать так:
    obj[name] = (obj[name] || 0) + 1;

    Это два обращения к хэшу в любых случаях.


  1. Devoter
    28.03.2018 07:15

    Всё-таки, применение reduce во многих случаях спорно, особенно, в качестве замены map-filter. Если хотим производительности, то for уделяет их всех на две головы, если хотим наглядности — map-filter нагляднее.


    1. LWXD
      28.03.2018 10:10
      -1

      reduce работает быстрее чем for. Вот к примеру, результат времени работы reduce и for, в массиве из 10.000.000 где каждое значение умножается само на себя:
      Время выполнения для reduce ~= 163.312ms
      Время выполнения for ~= 282.706ms


      1. evkochurov
        28.03.2018 10:30

        На код можно взглянуть, с помощью которого сделан замер?


        1. LWXD
          28.03.2018 10:39

          Это конечно не самый идеальный вариант кода, но все же…

          JS
          const now = require('performance-now');
          
          let array = [];
          for(let i = 0; i < 10000000; i++) {
          	array.push(i);
          }
          
          let start = now();
          var arr2 = array.reduce((num, item) => {
          	return num * num;
          }, 1);
          let end = now();
          console.log('Время выполнения = ' + (end - start).toFixed(3));
          
          start = now();
          let arr3 = [];
          for(let i = 0; i < 10000000; i++) {
          	arr3.push(i * i);
          }
          end = now();
          console.log('Время выполнения = ' + (end - start).toFixed(3));
          


          1. justboris
            28.03.2018 10:49

            У вас же две разных функциональности в коде


            В reduce вы считаете факториал 10000000. Значение arr2 будет числом


            var arr2 = array.reduce((num, item) => {
                return num * num;
            }, 1);

            Во втором случае у вас будет массив с квадратами исходных чисел


            let arr3 = [];
            for(let i = 0; i < 10000000; i++) {
                arr3.push(i * i);
            }

            Это не разные операции, дают совсем разный результат, сравнивать их бессмысленно


            1. LWXD
              28.03.2018 10:56

              вот блин, точно. Не заметил сразу -_- надо же не num, а item умножать и + то что число в итоге выходит. Тогда да, for шустрее


            1. LWXD
              28.03.2018 13:00

              У меня с reduce тут совсем ужас получился


              1. vitaliy2
                28.03.2018 14:47

                push вообще медленная штука, т.?к. эквивалентен 5 операциям (по крайней мере в V8, который делает запас по длине массива 25%).

                PS. Это во всех языках так, где есть динамический массив.

                А в js ещё при записи в динамический массив всякие проверки добавляться могут (хотя бы на длину массива).


    1. dom1n1k
      28.03.2018 11:24

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


  1. AxisPod
    28.03.2018 09:24

    2.1 как-то совсем не по нраву, особенно учитывая, что выпиливается декларативность, код для восприятия становится только сложнее

    Для reduce/reduceRight я бы привел в пример композицию методов для обратоки данных.


  1. Keyten
    28.03.2018 12:29

    Можно ещё и хитро маппить один объект в другой.
    Например, хотим из


    { catName: 'Alex', fur: { color: 'black', length: 3 }, catAge: 8 }

    Получить


    { type: 'cat', name: 'Alex', furColor: 'black', furLength: 3, age: 3 }

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


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


    function mapAnimal(source){
      return {
        type: 'cat',
        ...(source.catName && {
          name: source.catName
        }),
        // и так далее
      };
    }

    Или даже как-то так:


    function mapAnimal(source){
      let {undefined, ...result} = {
        type: 'cat',
        [source.catName && 'name']: source.catName,
        [source.fur && source.fur.color && 'furColor']: source.fur.color
      };
      return result;
    }

    Можно ещё деструктурировать source прям в записи аргументов.


  1. rockon404
    28.03.2018 12:37

    Почему в примерах местами var, местами let, местами const?
    var вообще в ES6 не надо использовать. let только если значение переменной переопределяется в коде. Для всех примеров следовало использовать const.


    1. Keyten
      28.03.2018 19:54

      Чем вам var помешало?


      1. rockon404
        29.03.2018 16:18

        А скажите хоть один кейс когда лучше вместо let/const использовать var в ES6? Я ни одного не знаю и за все время работы с ES6 ни разу var не использовал. Наличие этого ключевого слова в спецификации обусловленно лишь обратной совместимостью с более ранними версиями. В современных гайдлайнах, вроде Airbnb, var не рекомендовано к использованию. Да и тема целесообразности его использования не раз поднималась во множестве статей и обсуждений, которые в большинстве своем сводятся к тому, что для var в ES6 нет места.


        1. faiwer
          29.03.2018 17:07
          +2

          Скорее всего вам ответят что-нибудь вроде:


          try
          {
            // code
            var some = any();
            // code
          }
          catch(err)
          {
            if(some)
              some.dispose();
          }

          Мотивируя это тем, что без var придётся выносить let some; на уровень выше try-catch и тем самым плодить "лишние строчки".


          P.S.: По моему скромному мнению — строчки экономить не нужно. И предварительная декларация такого рода переменных лучше, чем по месту.


          1. vlreshet
            29.03.2018 17:41

            Кстати да, с самого начала интересно почему не предусмотрели этот момент. Можно же считать try-catch одним «блоком», с одной областью видимости. Не может же быть try без catch, или наоборот.


          1. Keyten
            30.03.2018 02:34

            Эмм, раз уж вы взялись предсказывать мой ответ, почему тогда try-catch, а не if-else?


            1. faiwer
              30.03.2018 07:16

              А зачем вам var в if-else? В нём выполнится только 1 блок (либо if, либо else), что вы там share-ить будете? :) И предсказывал я скорее не ваш ответ, а в целом ответ тех, кто использует var, к примеру vintage.


              1. vintage
                30.03.2018 09:39
                -1

                var Foo = Foo || class Foo {}

                try {
                    var foo = bar()
                    lol()
                } finally {
                    return foo
                }

                if( foo ) {
                    var bar = lol()
                } else {
                    var bar = zol()
                }
                console.log( bar )

                for( var i = 0 ; i < list.length ; ++ i ) {
                    if( foo( i ) ) break
                    bar()
                }
                
                return list.slice( i )


                1. faiwer
                  30.03.2018 09:50

                  Да, точно, спасибо за примеры. О таком применении var в if-else я уже и забыл, слишком давно не использую.


        1. Keyten
          30.03.2018 02:34
          -1

          Вы зачем-то расширили моё утверждение. Я разве где-то утверждал, что он в чём-то лучше? Я скорее не понимаю, почему все стремятся использовать let вместо var везде, где нужно и где не нужно, если ошибки, связанные с var, попадаются очень редко, очень легко ловятся, и вообще достигаются, в основном, новичками в языке, не потрудившимися прочитать, как оно тут работает.
          Это примерно как возмутиться, что у массивов могут быть повторяющиеся значения, и поэтому рекомендовать всегда вместо него использовать Set.

          Моё мнение: они вполне равноправны. Да, где-то let бывает писать удобнее. Но ведь где-то и с var такая же ситуация! (а вот теперь утверждаю, да: всплытие на уровне функции действительно бывает удобным)


          1. faiwer
            30.03.2018 07:20
            +1

            всплытие на уровне функции действительно бывает удобным

            Эта тема немного холиварная. Большинство из тех, кто не использует var, считают, что "удобство" в ущерб очевидности, однозначности, понятности кода — скорее зло. В случае var едва ли оно обоснованное. До того как let и const в языке появились, это была частая причина поливать JS грязью.


            Кстати, в PHP вообще даже var писать не нужно. Просто пишешь $mySuperVariable = и радуешься жизни. "Удобно" :)


      1. k12th
        29.03.2018 17:14

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


        1. vintage
          30.03.2018 09:56

          Вот вам задачка для собеседования на одних let-ах:


          let x = 1
          switch( x ) {
              case 1 :
                  let x = 2
                  console.info( x )
                  break
              case 2 :
                  let x = 3
                  console.debug( x )
                  break
              default :
                  console.log( x )
          }


  1. Opty
    28.03.2018 13:14

    П.3.1 — несет кучу мусора, изначально непонятно зачем присвоение в переменные, которые потом не используются. А вот 2.3 порадовало, спасибо)


  1. gmixo
    28.03.2018 14:20

    3.2. Деструктурирование вложенных объектов в параметрах функции — небезопасно когда более одной вложенности

    car={'model': 'повозка'}
    
    modelAndVIN = ({model, engine: {vin}}) => {
      console.log(`model: ${model} vin: ${vin}`);
    }
    
    modelAndVIN(car)
    Uncaught TypeError: Cannot destructure property `vin` of 'undefined' or 'null'.