хуйу нас не матерятся
В JavaScrip'те есть много способов работы с массивами: for in, for of, array.map, array.forEach, array.reduce, array.filter. И хоть каждый из них имеет своё специфическое предназначение, часто они используются бездумно.
Я уже год собеседую js программистов, и у меня есть 1 простейшая задачка: законсольлогить все элементы массива. В как минимум половине случаев начинают юзать array.map, затем вспоминают про array.forEach и менее половины кандидатов знают про уже давно стандартные for of/for in.
Для любого нормального программиста понятно, что array.map/array.forEach работают медленнее чем for of/for in, потому что на каждую операцию над элементом массива вызывается функция, со всеми своими накладными расходами. Но я задумался, а насколько именно отличается производительность функция обхода массива?
Давайте напишем небольшой тестик:
const rounds = 1000;
let testArray = [];
for(let i=0; i<1000000; i++)
testArray.push(1);
let res = 0;
let start0 = new Date();
for(let i=0; i<rounds; i++)
for(let index in testArray) {
}
let end0 = new Date();
console.log('TEST0 for in', (end0 - start0)/1000);
res = 0;
let start1 = new Date();
for(let i=0; i<rounds; i++)
for(let item of testArray) {
}
let end1 = new Date();
console.log('TEST1 for of', (end1 - start1)/1000);
res = 0;
let start2 = new Date();
for(let i=0; i<rounds; i++)
for(let index=0; index<testArray.length; index++) {
}
let end2 = new Date();
console.log('TEST2 classic for', (end2 - start2)/1000);
res = 0;
let start3 = new Date();
for(let i=0; i<rounds; i++)
testArray.forEach(function(item) {
});
let end3 = new Date();
console.log('TEST3 array.forEach', (end3 - start3)/1000);
res = 0;
let start4 = new Date();
for(let i=0; i<100; i++)
testArray.map(function(item, index, array) {
});
let end4 = new Date();
console.log('TEST4 array.map', (end4 - start4)/1000);
res = 0;
let start5 = new Date();
for(let i=0; i< rounds; i++)
testArray.reduce(function(accumulator, item) {
});
let end5 = new Date();
console.log('TEST5 array.reduce', (end5 - start5)/1000);
Запускаем и через пару минут получаем результаты:
$ node test2.js
TEST0 for in 130.6
TEST1 for of 3.053
TEST2 classic for 0.748
TEST3 array.forEach 12.883
TEST4 array.map 14.2
TEST5 array.reduce 13.552
Самый быстрый- это классический счётчик с инкрементом, за ним идёт for of.
На порядок медленнее оказались олдскульные array.forEach/array.map/array.reduce. И хоть результаты у них схожие, не забываем, что array.map возвращает массив, и под это ещё и память лишняя юзается.
Наиболее медленным оказался for in, причём медленнее на порядок. Я даже задумался протестить, не будет ли быстрее обходить ключи объекта через Object.keys а не через for in. Для этого опять напишем тестик:
const rounds = 100;
let testObj = {};
for(let i=0; i<1000000; i++)
testObj[ `a${i}` ] = 1;
let start0 = new Date();
for(let i=0; i<rounds; i++)
for(let index in testObj) {
}
let end0 = new Date();
console.log('TEST0 for in', (end0 - start0)/1000);
let start1 = new Date();
for(let i=0; i<rounds; i++) {
let keys = Object.keys(testObj)
for(let index=0; index<keys.length; index ++) {
}
}
let end1 = new Date();
console.log('TEST1 for in', (end1 - start1)/1000);
И результат:
$ node test2.js
TEST0 for in 43.046
TEST1 for in 37.787
Итого, Object.keys(testObj) + for, на 12% быстрее for in. Скорее всего под капотом там происходит что-то похожее, только вместо классического for+ инкремент используется что-то типа array.forEach который и даёт 14 секунд отставания.
Сам себе шлю привет из 2021, всё что тут написал, именно для ноды, уже не совсем актуально. Она, зараза, научилась в хитрые оптимизации, и классический фор уже может рабоать +- так же как форич. Единственно, не всегда предугадаешь, когда оптимизация сработает, а когда нет ...
TEST0 for in 264.588 VM78:26 TEST1 for of 1.353 VM78:36 TEST2 classic for 0.818 VM78:46 TEST3 array.forEach 21.524 VM78:56 TEST4 array.map 2.181 VM78:66 TEST5 array.reduce 22.217