Существует множество способов циклического обхода массивов и объектов в JavaScript, при этом не всегда понятно какая между ними разницами и когда и как лучшие использовать одни или другие. Некоторые руководства по стилю программирования заходят так далеко, что просто запрещают определенные циклические конструкции. В этой статье я опишу различия между циклами с помощью 4 основных циклических конструкций:
for (let i = 0; i < arr.length; ++i)
arr.forEach((v, i) => { /* ... */ })
for (let i in arr)
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.
Другой крайний случай с 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
Краткий перевод: https://vuejs.org/guide/components/v-model.html Основное использование v-model используется для реализации двусторонней привязки в компоненте. Начиная с Vue…
Сегодня мы рады объявить о выпуске Vue 3.4 «🏀 Slam Dunk»! Этот выпуск включает в…
Vue.js — это универсальный и адаптируемый фреймворк. Благодаря своей отличительной архитектуре и системе реактивности Vue…
Недавно, у меня истек сертификат и пришлось заказывать новый и затем устанавливать на хостинг с…
Каким бы ни было ваше мнение о JavaScript, но всем известно, что работа с датами…
Все, кто следит за последними событиями в мире адаптивного дизайна, согласятся, что введение контейнерных запросов…
View Comments
"Но приведенные выше примеры отлично работают с for/of: ..."
... ниже ...
>>вы не сможете использовать await внутри callback forEach ():
если callback пометить как асинхронный, то await можно использовать.
arr.forEach(async el => {...
Вот только прежде чем массив переберется с await'ами - пойдет выполнение кода, который будет идти после arr.forEach. Я сталкивался с такой ситуацией и решал через await Promise(arr.map(async func)). Но при этом все запросы выполняются параллельно. Если надо массив обработать последовательно - то как указано в статье через for или for/of.
arr.test = 'bad';
arr.test; // 'abc' - ошибка, должно быть 'bad'
Шрифт комментариев серый нечитаемый