TL; DR
Але є ще багато чого для вивчення, читання далі ...
JavaScript має потужну семантику для циклічного перегляду масивів та об’єктів, подібних до масиву. Відповідь я розділив на дві частини: Параметри справжнього масиву та параметри для речей, які просто схожі на масив, таких як argumentsоб'єкт, інші ітерабельні об'єкти (ES2015 +), колекції DOM тощо.
Я швидко відзначити , що ви можете використовувати ES2015 варіанти в даний час , навіть на двигунах ES5, по transpiling ES2015 в ES5. Шукати "ES2015 transpiling" / "ES6 transpiling" для отримання додаткової інформації ...
Гаразд, давайте розглянемо наші варіанти:
Для фактичних масивів
У ECMAScript 5 ("ES5") є три варіанти, найширша на даний момент версія, і ще два додані в ECMAScript 2015 ("ES2015", "ES6"):
- Використання
forEachта пов'язані з ним (ES5 +)
- Використовуйте просту
forпетлю
- Використовуйте
for-in правильно
- Використовувати
for-of(використовувати ітератор неявно) (ES2015 +)
- Використовуйте ітератор явно (ES2015 +)
Деталі:
1. Використання forEachта супутнє
У будь-якому нечітко сучасному середовищі (наприклад, не IE8), у якому ви маєте доступ до Arrayфункцій, доданих ES5 (безпосередньо або за допомогою поліфілів), ви можете використовувати forEach( spec| MDN):
var a = ["a", "b", "c"];
a.forEach(function(entry) {
console.log(entry);
});
forEachприймає функцію зворотного дзвінка і, необов'язково, значення, яке слід використовувати, як thisпри виклику цього зворотного дзвінка (не використовується вище). Зворотний виклик викликається для кожного запису в масиві для того, щоб пропускати неіснуючі записи в розріджених масивах. Хоча я використовував лише один аргумент вище, зворотний виклик викликається трьома: значенням кожного запису, індексом цього запису та посиланням на масив, який ви повторюєте (на випадок, якщо ваша функція вже не є зручною ).
Якщо ви не підтримуєте застарілі веб-переглядачі на зразок IE8 (який NetApps показує трохи більше 4% частки ринку на момент написання цього повідомлення у вересні 2016 року), ви можете із задоволенням використовувати forEachна веб-сторінці загального призначення без пошкоджень. Якщо вам потрібно підтримувати застарілі браузери, шиммінг / поліфілінг forEachлегко зробити (пошук "es5 shim" для декількох варіантів).
forEach має перевагу, що вам не доведеться оголошувати змінні індексації та значення в області, що містить, оскільки вони подаються як аргументи функції ітерації, і так добре підходять до саме цієї ітерації.
Якщо ви турбуєтесь про витрати часу виконання виклику функції для кожного запису масиву, не будьте; деталі .
Крім того, forEachє функція "цикл через них усіх", але ES5 визначає кілька інших корисних функцій "пропрацюйте свій масив і виконайте справи", включаючи:
every(припиняє циклічно вперше повертатися зворотний виклик falseабо щось фальсифіковане)
some(припиняє циклічно вперше повертатися зворотний виклик trueчи щось тривожне)
filter(створює новий масив, що включає елементи, де функція фільтра повертається trueі опускає ті, де він повертається false)
map (створює новий масив із значень, повернутих зворотним дзвоном)
reduce (створює значення шляхом повторного виклику зворотного виклику, передаючи попередні значення; деталі див. у специфікації; корисно підсумовувати вміст масиву та багато іншого)
reduceRight(як reduce, але працює у порядку зменшення, а не у порядку зростання)
2. Використовуйте просту forпетлю
Іноді найкращі старі способи:
var index;
var a = ["a", "b", "c"];
for (index = 0; index < a.length; ++index) {
console.log(a[index]);
}
Якщо довжина масиву не буде змінюватися в протягом циклу, і це у виконанні чутливої коди (малоймовірно), трохи більше складний варіант захоплення довжиною фронту може бути крихітними трохи швидше:
var index, len;
var a = ["a", "b", "c"];
for (index = 0, len = a.length; index < len; ++index) {
console.log(a[index]);
}
І / або рахуючи відсталі:
var index;
var a = ["a", "b", "c"];
for (index = a.length - 1; index >= 0; --index) {
console.log(a[index]);
}
Але в сучасних JavaScript-двигунах рідко вам потрібно витягти цей останній шматочок соку.
У ES2015 та новіших версіях ви можете зробити свої змінні індексу та значення локальними для forциклу:
let a = ["a", "b", "c"];
for (let index = 0; index < a.length; ++index) {
let value = a[index];
console.log(index, value);
}
//console.log(index); // would cause "ReferenceError: index is not defined"
//console.log(value); // would cause "ReferenceError: value is not defined"
let a = ["a", "b", "c"];
for (let index = 0; index < a.length; ++index) {
let value = a[index];
console.log(index, value);
}
try {
console.log(index);
} catch (e) {
console.error(e); // "ReferenceError: index is not defined"
}
try {
console.log(value);
} catch (e) {
console.error(e); // "ReferenceError: value is not defined"
}
І коли ви це робите, не просто, valueа й indexвідтворюється для кожної ітерації циклу, тобто закриття, створені в тілі циклу, зберігають посилання на index(і value), створені для цієї конкретної ітерації:
let divs = document.querySelectorAll("div");
for (let index = 0; index < divs.length; ++index) {
divs[index].addEventListener('click', e => {
console.log("Index is: " + index);
});
}
let divs = document.querySelectorAll("div");
for (let index = 0; index < divs.length; ++index) {
divs[index].addEventListener('click', e => {
console.log("Index is: " + index);
});
}
<div>zero</div>
<div>one</div>
<div>two</div>
<div>three</div>
<div>four</div>
Якщо у вас було п'ять дівок, ви отримаєте "Індекс - 0", якщо ви натиснули перший, а "Індекс - 4", якщо ви натиснули останній. Це не працює, якщо ви використовуєте varзамість let.
3. Користуйтеся for-in правильно
Ви будете отримувати люди , які говорять вам використовувати for-in, але це не те , що for-inдля . for-inпроходить через перелічені властивості об'єкта , а не індекси масиву. Замовлення не гарантується , навіть у ES2015 (ES6). ES2015 + визначає порядок об’єкта властивостей (через [[OwnPropertyKeys]], [[Enumerate]]та речі, що їх використовують як Object.getOwnPropertyKeys), але він не визначив, що for-inбуде слідувати цьому порядку; ES2020, хоча. (Деталі в цій іншій відповіді .)
Єдині реальні випадки використання для for-inмасиву:
- Це розріджений масив з масивними прогалинами в ньому, або
- Ви використовуєте неелементні властивості і хочете включити їх у цикл
Дивлячись лише на цей перший приклад: Ви можете використовувати for-inдля відвідування цих розріджених елементів масиву, якщо використовуєте відповідні гарантії:
// `a` is a sparse array
var key;
var a = [];
a[0] = "a";
a[10] = "b";
a[10000] = "c";
for (key in a) {
if (a.hasOwnProperty(key) && // These checks are
/^0$|^[1-9]\d*$/.test(key) && // explained
key <= 4294967294 // below
) {
console.log(a[key]);
}
}
Зверніть увагу на три чеки:
Те , що об'єкт має свою власну власність під цим ім'ям (не один він успадковує від свого прототипу), і
Ключ - усі десяткові цифри (наприклад, звичайна форма рядка, а не наукове позначення) та
Це значення ключа при примусовому до числа є <= 2 ^ 32 - 2 (що становить 4 294 967 294). Звідки це число? Це частина визначення індексу масиву в специфікації . Інші числа (нецілі числа, від’ємні числа, числа, що перевищують 2 ^ 32 - 2) не є індексами масиву. Причина, що це 2 ^ 32 - 2, полягає в тому, що найбільше значення індексу стає нижчим за 2 ^ 32 - 1 , що є максимальним значенням, яке lengthможе мати масив . (Наприклад, довжина припадки у вигляді масиву в 32-розрядний беззнаковое ціле число.) (Належить до RobG за вказівку в коментарі на моєму блозі , що мій попередній тест був не зовсім правий.)
Ви б, звичайно, не робили цього вбудованого коду. Ви б написали функцію утиліти. Можливо:
// Utility function for antiquated environments without `forEach`
var hasOwn = Object.prototype.hasOwnProperty;
var rexNum = /^0$|^[1-9]\d*$/;
function sparseEach(array, callback, thisArg) {
var index;
for (var key in array) {
index = +key;
if (hasOwn.call(a, key) &&
rexNum.test(key) &&
index <= 4294967294
) {
callback.call(thisArg, array[key], index, array);
}
}
}
var a = [];
a[5] = "five";
a[10] = "ten";
a[100000] = "one hundred thousand";
a.b = "bee";
sparseEach(a, function(value, index) {
console.log("Value at " + index + " is " + value);
});
4. Використовувати for-of(використовувати ітератор неявно) (ES2015 +)
ES2015 додав ітератори до JavaScript. Найпростіший спосіб використання ітераторів - це новий for-ofоператор. Це виглядає приблизно так:
const a = ["a", "b", "c"];
for (const val of a) {
console.log(val);
}
Під обкладинками, що отримує ітератор з масиву і проходить через нього, отримуючи з нього значення. У цьому не виникає проблеми, яку for-inмає, оскільки він використовує ітератор, визначений об'єктом (масивом), а масиви визначають, що їх ітератори повторюють через записи (а не їх властивості). На відміну від for-inES5, порядок відвідування записів - це числовий порядок їх індексів.
5. Використовуйте ітератор явно (ES2015 +)
Іноді, можливо, ви хочете явно використовувати ітератор . Ви можете зробити це теж, хоча це набагато незграбніше, ніж . Це виглядає приблизно так:for-of
const a = ["a", "b", "c"];
const it = a.values();
let entry;
while (!(entry = it.next()).done) {
console.log(entry.value);
}
Ітератор - це об'єкт, що відповідає визначенню Ітератора в специфікації. Його nextметод повертає новий об'єкт результату щоразу, коли ви його викликаєте. Об'єкт результату має властивість, яка doneговорить нам, чи це зроблено, і властивість, що valueмає значення для цієї ітерації. ( doneнеобов'язково, якби це було false, valueнеобов’язково, якби було undefined.)
Значення valueзмінюється залежно від ітератора; масиви підтримують (принаймні) три функції, які повертають ітератори:
values(): Це я використовував вище. Вона повертає ітератор , де кожен valueє елементом масиву для цієї ітерації ( "a", "b"і "c"в прикладі вище).
keys(): Повертає ітератор, де кожен valueє ключем для цієї ітерації (так, як для нашого aвище, це було б "0", тоді "1", тоді "2").
entries(): Повертає ітератор, де кожен valueє масивом у формі [key, value]для цієї ітерації.
Для об’єктів, подібних до масиву
Крім справжніх масивів, є також об’єкти, схожі на масив, які мають lengthвластивість та властивості з числовими іменами: NodeListекземпляри, argumentsоб’єкт тощо. Як ми перебираємо їх вміст?
Використовуйте будь-який із варіантів, описаних вище для масивів
Принаймні деякі, а можливо, більшість або навіть усі підходи до масиву, наведені вище, часто однаково добре застосовуються до об’єктів, подібних до масиву:
Використання forEachта пов'язані з ним (ES5 +)
Різні функції на Array.prototype"навмисно загальних" і, як правило, можуть використовуватися на об'єктах, подібних до масиву, через Function#callабо Function#apply. (Дивіться Caveat щодо наданих господарями об’єктів наприкінці цієї відповіді, але це рідкісна проблема.)
Припустимо , ви хочете використовувати forEachна безлічі А Node«S childNodesвласності. Ви зробите це:
Array.prototype.forEach.call(node.childNodes, function(child) {
// Do something with `child`
});
Якщо ви будете робити це багато, ви можете захопити копію посилання на функцію в змінну для повторного використання, наприклад:
// (This is all presumably in some scoping function)
var forEach = Array.prototype.forEach;
// Then later...
forEach.call(node.childNodes, function(child) {
// Do something with `child`
});
Використовуйте просту forпетлю
Очевидно, простий forцикл стосується об’єктів, подібних до масиву.
Використовуйте for-in правильно
for-inз тими ж захисними засобами, що і з масивом, також слід працювати з об’єктами, схожими на масив; може застосовуватися застереження для об’єктів, наданих господарем на №1 вище.
Використовувати for-of(використовувати ітератор неявно) (ES2015 +)
for-ofвикористовує ітератор, наданий об'єктом (якщо такий є). Це включає об'єкти, надані хостом. Наприклад, специфікацію для NodeListвід querySelectorAllоновлено для підтримки ітерації. Спеціалізації для з HTMLCollectionбоку getElementsByTagNameне було.
Використовуйте ітератор явно (ES2015 +)
Див. №4.
Створіть справжній масив
В іншому випадку ви можете перетворити об’єкт, схожий на масив, у справжній масив. Робити це напрочуд просто:
Використовуйте sliceметод масивів
Ми можемо використовувати sliceметод масивів, який, як і інші методи, згадані вище, є "навмисно загальним", і тому може використовуватися з об'єктами, схожими на масив, наприклад:
var trueArray = Array.prototype.slice.call(arrayLikeObject);
Так, наприклад, якщо ми хочемо перетворити NodeListв справжній масив, ми могли б зробити це:
var divs = Array.prototype.slice.call(document.querySelectorAll("div"));
Дивіться Caveat для наданих господарями об’єктів нижче. Зокрема, зауважте, що це не вдасться в IE8 і раніше, що не дозволяє використовувати надані хостом об’єкти як thisподібні.
Використовувати синтаксис спред ( ...)
Також можливо використовувати синтаксис поширення ES2015 з двигунами JavaScript, які підтримують цю функцію. Наприклад for-of, для цього використовується ітератор, наданий об'єктом (див. №4 у попередньому розділі):
var trueArray = [...iterableObject];
Так, наприклад, якщо ми хочемо перетворити a NodeListв справжній масив, з синтаксисом розповсюдження це стає досить лаконічним:
var divs = [...document.querySelectorAll("div")];
Використовуйте Array.from
Array.from (специфікація) | (MDN) (ES2015 +, але легко поліфільований) створює масив з об’єкта, подібного до масиву, необов'язково спочатку передаючи записи через функцію зіставлення. Тому:
var divs = Array.from(document.querySelectorAll("div"));
Або якщо ви хочете отримати масив імен тегів елементів із заданим класом, ви використовуєте функцію зіставлення:
// Arrow function (ES2015):
var divs = Array.from(document.querySelectorAll(".some-class"), element => element.tagName);
// Standard function (since `Array.from` can be shimmed):
var divs = Array.from(document.querySelectorAll(".some-class"), function(element) {
return element.tagName;
});
Застереження для наданих господарем об'єктів
Якщо ви використовуєте Array.prototypeфункції з хоста , що надається масив типу об'єктів (списки DOM і інших речей , передбачених в браузері , а не двигун JavaScript), ви повинні бути впевнені, що тест в вашої цільової середовищі , щоб переконатися , що приймає наданий об'єкт поводиться правильно . Більшість поводиться належним чином (зараз), але важливо перевірити. Причина полягає в тому, що більшість Array.prototypeметодів, які ви, ймовірно, захочете використовувати, покладаються на наданий хостом об’єкт, що дає чесну відповідь на абстрактну [[HasProperty]]операцію. Станом на це написання, браузери роблять дуже гарну роботу з цього, але специфікація 5.1 дозволяє зробити можливість, що надається хостом об'єктом може бути не чесним. Це в §8.6.2 , кілька абзаців під великою таблицею на початку цього розділу), де написано:
Приймаючі об'єкти можуть реалізувати ці внутрішні методи будь-яким чином, якщо не вказано інше; Наприклад, одна з можливостей полягає в тому, що [[Get]]і [[Put]]для певного хост-об'єкта дійсно вибирають і зберігають значення властивостей, але [[HasProperty]]завжди генерують помилкові .
(Я не міг знайти еквівалентну багатослівність у специфікації ES2015, але це все одно має бути так.) Знову ж таки, з цього написання звичайні об’єкти, подібні до масиву, подібні до масивів у сучасних браузерах [ NodeListнаприклад, наприклад] , обробляють [[HasProperty]]правильно, але важливо перевірити.)
forEachі не простоfor. як було сказано, в c # було трохи інакше, і це мене бентежило :)