Как мога да направя цикъл през всички записи в масив с помощта на JavaScript?
Мислех, че е нещо подобно:
forEach(instance in theArray)
Където theArray
е моят масив, но това изглежда е неправилно.
TL;DR
for-in
, освен ако не го използвате с предпазни мерки или поне не сте наясно защо може да ви ухапе.for-of
(само в ES2015+),Array#forEach
(spec
| MDN
) (или неговите роднини some
и подобни) (само ES5+),for
,for-in
с предпазни мерки.
Но има още много неща, които да изследвате, четете нататък...JavaScript разполага с мощна семантика за циклиране през масиви и масивоподобни обекти. Разделих отговора на две части: Опции за истински масиви и опции за неща, които са просто масивоподобни, като например обект arguments
, други итерабилни обекти (ES2015+), колекции DOM и т.н.
Набързо ще отбележа, че можете да използвате опциите на ES2015 сега, дори и на двигатели ES5, чрез транспириране* на ES2015 към ES5. За повече информация потърсете "ES2015 transpiling" / "ES6 transpiling"...
Добре, нека разгледаме нашите възможности:
Имате три опции в ECMAScript 5 ("ES5"), най-широко поддържаната версия в момента, и още две, добавени в ECMAScript 2015 ("ES2015", "ES6"):
forEach
и свързаните с него (ES5+)for
for-in
правилноfor-of
(използвайте итератор по подразбиране) (ES2015+)forEach
и свързаните с негоВ която и да е мъглява съвременна среда (т.е. не в IE8), където имате достъп до функциите Array
, добавени от ES5 (директно или с помощта на polyfills), можете да използвате forEach
(spec
| MDN
):
var a = ["a", "b", "c"];
a.forEach(function(entry) {
console.log(entry);
});
forEach
приема функция за обратно извикване и, по избор, стойност, която да се използва като this
при извикване на това обратно извикване (не е използвана по-горе). Обратното извикване се извиква за всеки запис в масива, по ред, като се пропускат несъществуващи записи в редки масиви. Въпреки че по-горе използвах само един аргумент, обратното извикване се извиква с три: Стойността на всеки запис, индексът на този запис и препратка към масива, над който итерирате (в случай, че функцията ви вече не го има под ръка).
Освен ако'не поддържате остарели браузъри като IE8 (чийто пазарен дял според NetApps е малко над 4% към момента на писане на тази статия през септември 2016 г.), можете спокойно да използвате forEach
в уеб страница с общо предназначение без шим. Ако все пак трябва да поддържате остарели браузъри, лесно можете да направите shimming/polyfilling на forEach
(потърсете "es5 shim" за няколко варианта).
Предимството на forEach
е, че не се налага да декларирате променливите за индексиране и стойност в съдържащия обхват, тъй като те се предоставят като аргументи на функцията за итерация и по този начин са обхванати само от тази итерация.
Ако се притеснявате за разходите за изпълнение на функцията за всеки запис на масив, не се притеснявайте; подробности.
Освен това forEach
е функцията "loop through them all", но ES5 дефинира няколко други полезни "work your way through the array and do things" функции, включително:
every
(спира цикъла при първия път, когато обратната връзка върне false
или нещо невярно)some
(спира цикъла при първия път, когато обратната връзка върне true
или нещо вярно)filter
(създава нов масив, включващ елементите, при които функцията за филтриране връща true
, и пропускащ тези, при които връща false
)map
(създава нов масив от стойностите, върнати от обратната функция)reduce
(изгражда стойност чрез многократно извикване на обратната връзка, като предава предишни стойности; вижте спецификацията за подробности; полезно за сумиране на съдържанието на масив и много други неща)reduceRight
(подобно на reduce
, но работи в низходящ, а не във възходящ ред)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 и по-нови версии можете да направите променливите index и value локални за цикъла 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>
Ако имахте пет div-а, щяхте да получите "Index is: 0" ако щракнете върху първия и "Индексът е: 4", ако щракнете върху последния. Това не работи, ако използвате var
вместо let
.
for-in
правилноХората ще ви кажат да използвате for-in
, но for-in
не служи за това 11. for-in
преминава през изброимите свойства на обекта, а не през индексите на масив. Поредността не е гарантирана, дори в ES2015 (ES6). ES2015+ определя ред за свойствата на обектите (чрез [[OwnPropertyKeys]]
, [[Enumerate]]
и неща, които ги използват, като Object.getOwnPropertyKeys
), но не определя, че for-in
ще следва този ред. (Подробности в този друг отговор.)
Единствените реални случаи на използване на 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]);
}
}
Обърнете внимание на трите проверки:
дължината
на масив. (Например, дължината на масива'се побира в 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);
});
for-of
(имплицитно използване на итератор) (ES2015+)ES2015 добавя итератори в JavaScript. Най-лесният начин за използване на итератори е новият оператор for-of
. Той изглежда по следния начин:
const a = ["a", "b", "c"];
for (const val of a) {
console.log(val);
}
Под прикритие, това получава iterator от масива и преминава през него, като получава стойностите от него. При това не възниква проблемът, който възниква при използването на for-in
, тъй като се използва итератор, дефиниран от обекта (масива), а масивите дефинират, че итераторите им итерират през техните въпроси (а не през техните свойства). За разлика от for-in
в ES5, редът, в който се посещават записите, е числовият ред на техните индекси.
Понякога може да искате да използвате итератор експлицитно. Можете да направите и това, въпреки че е много по-тромаво от 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()
: Това е тази, която използвах по-горе. Тя връща итератор, в който всяка стойност
е записът в масива за тази итерация ("a"
, "b"
и "c"
в примера по-рано).keys()
: Връща итератор, в който всяка стойност
е ключът за тази итерация (така че за нашето a
по-горе това ще бъде "0"
, след това "1"
, след това "2"
).entries()
: Връща итератор, в който всяка стойност
е масив във формата [ключ, стойност]
за тази итерация.Освен истинските масиви, съществуват и масивоподобни обекти, които имат свойство дължина
и свойства с числови имена: екземпляри на NodeList
, обектът аргументи
и т.н. Как да прегледаме съдържанието им?
Поне някои, а може би повечето или дори всички подходи за масиви, описани по-горе, често се прилагат еднакво добре и за обекти, подобни на масиви:
Използвайте forEach
и свързаните с него (ES5+)
Различните функции в Array.prototype
са "умишлено общи" и обикновено могат да се използват за масивоподобни обекти чрез Function#call
или Function#apply
. (Вижте Кавеата за обекти, предоставени от хоста в края на този отговор, но това е рядък проблем.)
Да предположим, че искате да използвате forEach
за свойството Node
's childNodes
. Ще направите следното:
Array.prototype.forEach.call(node.childNodes, function(child) {
// Направете нещо с child
});
Ако ще правите това често, може би ще искате да копирате референцията на функцията в променлива за повторна употреба, напр:
// (Всичко това вероятно е в някоя функция за определяне на обхвата)
var forEach = Array.prototype.forEach;
// След това по-късно...
forEach.call(node.childNodes, function(child) {
// Направете нещо с 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"));
Вижте Пещера за обекти, предоставени от хоста по-долу. По-специално, обърнете внимание, че това няма да се получи в IE8 и по-ранни версии, които не позволяват използването на предоставени от хоста обекти като this
по този начин.
Използвайте синтаксис на разпространение (...
)
Възможно е също така да използвате ES2015's spread syntax с JavaScript двигатели, които поддържат тази функция:
var trueArray = [...iterableObject];
Така например, ако искаме да преобразуваме NodeList
в истински масив, със синтаксиса на разпространение това става съвсем кратко:
var divs = [...document.querySelectorAll("div")];
Използвайте Array.from
(spec) | (MDN)
Array.from
(ES2015+, но лесно се попълва многократно) създава масив от обект, подобен на масив, като по желание първо предава записите през функция за съпоставяне. И така:
var divs = Array.from(document.querySelectorAll("div"));
Ако пък искате да получите масив от имената на таговете на елементите с даден клас, ще използвате функцията mapping:
// Функция стрела (ES2015):
var divs = Array.from(document.querySelectorAll(".some-class"), element => element.tagName);
// Стандартна функция (тъй като Array.from
може да бъде премерен):
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]]
винаги да генерира false. (Не можах да намеря еквивалентен израз в спецификацията на ES2015, но със сигурност все още е така.) Отново, от момента на писане на този текст, обичайните масивоподобни обекти, предоставени от хоста в съвременните браузъри [например екземпляри наNodeList
] **обработват правилно[[HasProperty]]
, но е важно да се тества.)
Забележка: Този отговор е безнадеждно остарял. За по-съвременен подход вижте методите, достъпни за масив. Интересни методи могат да бъдат:
Стандартният начин за итериране на масив в JavaScript е обикновеният цикъл for
:
var length = arr.length,
element = null;
for (var i = 0; i < length; i++) {
element = arr[i];
// Do something with element
}
Имайте предвид обаче, че този подход е добър само ако имате плътен масив и всеки индекс е зает от елемент. Ако масивът е рядък, този подход може да доведе до проблеми с производителността, тъй като ще се итерира над много индекси, които не съществуват в масива. В този случай цикълът for .. in
може да се окаже по-добра идея. Въпреки това трябва да използвате подходящи предпазни мерки, за да гарантирате, че се действа само върху желаните свойства на масива (т.е. елементите на масива), тъй като цикълът for..in
ще бъде изброяван и в по-старите браузъри или ако допълнителните свойства са дефинирани като enumerable
.
В ECMAScript 5 ще има метод forEach на прототипа на масива, но той не се поддържа в по-старите браузъри. Така че, за да можете да го използвате последователно, трябва или да имате среда, която го поддържа (например Node.js за сървърен JavaScript), или да използвате "Polyfill". Полифилът за тази функционалност обаче е тривиален и тъй като улеснява четенето на кода, е добре да бъде включен.
Ако искате да направите цикъл в масив, използвайте стандартния цикъл от три части for
.
for (var i = 0; i < myArray.length; i++) {
var arrayItem = myArray[i];
}
Можете да получите някои оптимизации на производителността, като кеширате myArray.length
или го преглеждате в обратна посока.