For против forEach() против for/in и против for/of в JavaScript

Spread the love

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

  1. for (let i = 0; i < arr.length; ++i)
  2. arr.forEach((v, i) => { /* ... */ })
  3. for (let i in arr)
  4. for (const v of arr)

Я опишу различия между этими конструкциями циклов, используя несколько крайних случаев. Я также сошлюсь на соответствующие правила ESLint.

Синтаксический обзор

Циклические конструкции for и for/in предоставляют вам доступ к индексу в массиве, а не к фактическому элементу. Например, предположим, что вы хотите распечатать значения следующего массива:

const arr = ['a', 'b', 'c'];

С помощью for и for/in вам нужно использовать конструкцию arr [index]:

for (let index = 0; index < arr.length; ++index) {
  console.log(arr[index]);
}

for (let index in arr) {
  console.log(arr[index]);
}

С двумя другими конструкциями, forEach() и for/of, вы сразу получаете доступ к самому элементу массива. С forEach() вы можете так же получить индекс массива index, с for/of индекс не доступен.

arr.forEach((v, index) => console.log(v));

for (const v of arr) {
  console.log(v);
}

Нечисловые свойства

Массивы JavaScript являются объектами. Это означает, что вы можете добавлять строковые свойства в ваш массив, а не только числа.

const arr = ['a', 'b', 'c'];

typeof arr; // 'object'

// Assign to a non-numeric property
arr.test = 'bad';

arr.test; // 'abc'
arr[1] === arr['1']; // true, JavaScript arrays are just special objects

Три из четырех циклических конструкций игнорируют нечисловое свойство. Однако for/in фактически выведет слово «bad»:

const arr = ['a', 'b', 'c'];
arr.test = 'bad';

// Prints "a, b, c, bad"
for (let i in arr) {
  console.log(arr[i]);
}

Вот почему перебор массива с использованием for/in – это, как правило, плохая практика. Другие циклические конструкции правильно игнорируют нечисловой ключ:

const arr = ['a', 'b', 'c'];
arr.test = 'abc';

// Prints "a, b, c"
for (let i = 0; i < arr.length; ++i) {
  console.log(arr[i]);
}

// Prints "a, b, c"
arr.forEach((el, i) => console.log(i, el));

// Prints "a, b, c"
for (const el of arr) {
  console.log(el);
}

Вывод: избегайте использования for/in для массива, если вы не уверены, что хотите перебрать нечисловые ключи или унаследованные ключи. Используйте правило guard-for-in ESLint для запрета for/in конструкций.

Пустые элементы

Массивы JavaScript допускают пустые элементы. Массив ниже синтаксически верный и имеет длину 3 элемента:

const arr = ['a',, 'c'];

arr.length; // 3

Что еще более запутывает, так это то, что циклические конструкции трактуют [‘a’ ,, ‘c’] иначе, чем [‘a’, undefined, ‘c’]. Ниже показано, как четыре циклических конструкции обрабатывают [‘a’ ,, ‘c’] с пустым элементом. for/in и for/each пропускают пустой элемент, for и for/of – нет.

// Prints "a, undefined, c"
for (let i = 0; i < arr.length; ++i) {
  console.log(arr[i]);
}

// Prints "a, c"
arr.forEach(v => console.log(v));

// Prints "a, c"
for (let i in arr) {
  console.log(arr[i]);
}

// Prints "a, undefined, c"
for (const v of arr) {
  console.log(v);
}

Если вам интересно, все 4 конструкции выведут “a, undefined, c” для [‘a’, undefined, ‘c’].

Есть еще один способ добавить пустой элемент в массив:

// Equivalent to `['a', 'b', 'c',, 'e']`
const arr = ['a', 'b', 'c'];
arr[5] = 'e';

forEach() и for/in пропускают пустые элементы в массиве, for и for/of – нет. Поведение forEach() может вызвать проблемы, однако можно заметить, что дыры в массивах JavaScript, как правило, встречаются редко, поскольку они не поддерживаются в JSON:

$ node
> JSON.parse('{"arr":["a","b","c"]}')
{ arr: [ 'a', 'b', 'c' ] }
> JSON.parse('{"arr":["a",null,"c"]}')
{ arr: [ 'a', null, 'c' ] }
> JSON.parse('{"arr":["a",,"c"]}')
SyntaxError: Unexpected token , in JSON at position 12

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

Вывод: for/in и forEach() не реагируют на пустые элементы, также известные как «дыры», в массиве. Редко есть какая-либо причина рассматривать дыры как особый случай, а не рассматривать индекс как значение undefined. Если вы допускаете наличие дыр, ниже приведен пример файла .eslintrc.yml, который запрещает вызов forEach().

parserOptions:
  ecmaVersion: 2018
rules:
  no-restricted-syntax:
    - error
    - selector: CallExpression[callee.property.name="forEach"]
      message: Do not use `forEach()`, use `for/of` instead

Контекст функции

Функциональный контекст – это причудливый способ сказать, к чему относится this. for, for/in и for/of сохраняют значение this из внешней области видимости, но обратный вызов forEach() будет иметь другое значение this, если вы не используете стрелочную функцию.

'use strict';

const arr = ['a'];

// Prints "undefined"
arr.forEach(function() {
  console.log(this);
});

Вывод: всегда используйте функции стрелок с forEach(). Для этого используйте правило ESLint no-arrow-callback, чтобы ожидать функции стрелок для всех callbacks.

Async/Await и генераторы

Другой крайний случай с forEach() – это то, что он не совсем правильно работает с async/await или генераторами. Если ваш callback forEach() является синхронным, то это не имеет значения, но вы не сможете использовать await внутри callback forEach ():

async function run() {
  const arr = ['a', 'b', 'c'];
  arr.forEach(el => {
    // SyntaxError
    await new Promise(resolve => setTimeout(resolve, 1000));
    console.log(el);
  });
}

Вы также не сможете использовать yield:

function* run() {
  const arr = ['a', 'b', 'c'];
  arr.forEach(el => {
    // SyntaxError
    yield new Promise(resolve => setTimeout(resolve, 1000));
    console.log(el);
  });
}

Но приведенные выше примеры отлично работают с for/of:

async function asyncFn() {
  const arr = ['a', 'b', 'c'];
  for (const el of arr) {
    await new Promise(resolve => setTimeout(resolve, 1000));
    console.log(el);
  }
}

function* generatorFn() {
  const arr = ['a', 'b', 'c'];
  for (const el of arr) {
    yield new Promise(resolve => setTimeout(resolve, 1000));
    console.log(el);
  }
}

Даже если вы пометите свой callback forEach() как async, вам будет сложно заставить асинхронный метод forEach() работать последовательно. Например, приведенный ниже скрипт будет печатать 0-9 в обратном порядке.

async function print(n) {
  // Wait 1 second before printing 0, 0.9 seconds before printing 1, etc.
  await new Promise(resolve => setTimeout(() => resolve(), 1000 - n * 100));
  // Will usually print 9, 8, 7, 6, 5, 4, 3, 2, 1, 0 but order is not strictly
  // guaranteed.
  console.log(n);
}

async function test() {
  [0, 1, 2, 3, 4, 5, 6, 7, 8, 9].forEach(print);
}

test();

T

Вывод: если вы используете async/await или генераторы, помните, что forEach() является синтаксическим сахаром. Как сахар, его следует использовать экономно и не для всего.

Заключение

Как правило, for/of – это самый надежный способ перебора массива в JavaScript. Он более лаконичен, чем обычный цикл for, и не имеет такого количества граничных случаев, как for/in и forEach(). Основным недостатком for/of является то, что вам нужно проделать дополнительную работу для доступа к индексу массива (см. дополнение), и вы не можете строить цепочки кода, как вы можете это делать с помощью forEach(). Но если вы знаете все особенности forEach(), то во многих случаях его использование делает код более лаконичным.

Дополнение: Чтобы получить доступ к текущему индексу массива в цикле for/of, вы можете использовать функцию  Array#entries().

for (const [i, v] of arr.entries()) {
  console.log(i, v); // Prints "0 a", "1 b", "2 c"
}

Оригинал: For vs forEach() vs for/in vs for/of in JavaScript


Spread the love

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

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