Большинство программистов javascript и программистов других языков хорошо понимают, как перебирать элементы массива, выполняя какое-либо действие. Недавняя волна постов в блогах, вдохновленных функциональностью, превозносила достоинства функций высшего порядка, таких как map, filter и reduce, во имя создания «лучшего кода». Я хотел бы бросить вызов этой тенденции и объяснить, почему ниже. Я знаю. Такой храбрый.

Краткий обзор

Вот три наиболее распространенных подхода к повторению массива для увеличения некоторого значения в каждом члене:

// 1 - functional style
array.forEach((item) => item.id++)
// 2 - algol style
for (var i = 0; i < array.length; i++) array[i].id++
// 3 - python style
for (var item of array) item.id++

Первое может быть выполнено с помощью встроенных функций es5, функций lodash или даже более причудливой херни, такой как rambda или любой другой библиотеки, которая встраивается в каррирование для каждого вызова функции.

Второй представляет довольно старую школу программирования, и большинство программистов, вероятно, знакомы с этой формой из других языков, вдохновленных алголом.

Третий вариант скрывает детали второго варианта и позволяет привязать каждый элемент к локальному имени внутри блока цикла для удобочитаемости.

Плохие новости

Вот неполный список недостатков каждого из вышеперечисленных вариантов:

  1. Закрытие среды для захвата переменных, ДРУГИХ, чем элемент, часто требует, чтобы вы генерировали объект функции КАЖДЫЙ раз, когда вы вызываете этот код. Это происходит потому, что функция высшего порядка поддерживает только два аргумента внутренней функции: каждый элемент и необязательное значение контекста «это». Это создает нагрузку на GC, а также подразумевает ТОННУ вызовов функций для выполнения одной и той же работы (хотя большинство хороших JIT, скорее всего, оптимизируют вторую жалобу).
  2. Главный недостаток этого варианта в том, что каждый элемент внутри блока не называется интуитивно понятным именем.
  3. Из-за этого у многих возникают проблемы. На первый взгляд, это чистый сахар для второго варианта. Это не тот случай. Синтаксис for-of был введен в спецификацию javascript как часть нового протокола Iterator, который объекты могут дополнительно реализовывать. Подробное обсуждение этого выходит за рамки этой статьи, но погуглите и прочитайте, если вы знакомы с этим. Этот синтаксис очень полезен для генераторов, ленивых запросов, обхода дерева и т. д. Однако это также означает, что базовая спецификация для for-of значительно сложнее, чем та, которая традиционно требуется для массивов (хотя массив также реализует протокол Iterator). . Это приводит к созданию литерала объекта PER-ITEM, поскольку форма возвращаемого значения итератора имеет вид { done: Bool, value: Any}. Кроме того, протокол итератора по определению представляет собой последовательность вызовов функций и, следовательно, намного медленнее, чем простая итерация на основе блоков.

О сахаре и составе

Многие программисты, пришедшие из языков с этапом компиляции, привыкли использовать функции высшего порядка и композицию для выражения своих намерений. Часто сложная проблема разбивается на «шаги», что дает читателю и автору возможность построить предполагаемое поведение из более мелких частей:

// f(g(x))
filter(friend => friend.age > 15, map(user => user.friend, users) 

// common JS library composition style
compose(filter.bind(null, friend => friend.age > 15)
        map.bind(null, user => user.friend)(users)

// common JS library chaining style
chain(users).map(user => user.friend)
            .filter(friend => friend.age > 15)
            .value()

// better style using implicit partial application (ala rambda)
compose(filter(compose(gt(15), dot('age'))), 
        map(dot('friend')))(users)

// infix composition/application operator ala sweet.js
filter(gt(15) . dot('age')) . map(dot('friend')) $ users

Все эти формы делают одно и то же, и некоторые из них, возможно, более читабельны, чем другие. Однако ключевым моментом во всем этом является то, что код javascript интерпретируется во время выполнения, а НЕ компилируется. Таким образом, вы надеетесь, что JIT сможет проанализировать ваш исполняемый код и определить, что он может преобразовать эту композицию в один цикл, одновременно встраивая лямбда-выражения для сокращения вызовов функций. Короче говоря, вы надеетесь получить следующее:

var results = []
for (var i = 0; i < users.length; i++) {
  if (users[i].friend.age > 15) results.push(users[i].friend)
}

Для простых композиций, возможно, JIT действительно справится, но нет никакой гарантии, и код без сахара имеет ту же длину/сложность, что и большинство форм, показанных выше.

Компилятор с поддержкой типов может с гораздо большей уверенностью знать, какие оптимизации можно применить к вашему слащавому коду для достижения максимальной производительности во время выполнения. При использовании JIT вы полагаетесь как на возможности JIT, так и на рассмотрение во время выполнения полной спецификации для различных конструкций кода. Многие функции/поведения в javascript сильно перегружены, и поэтому среды выполнения, соответствующие стандартам, часто должны создавать менее оптимизированный код, чем может гарантировать ваш конкретный вариант использования.

Лучшее

Я предлагаю вам попытаться найти баланс между тремя факторами:

  1. Представление. Это означает грубую скорость выполнения во время выполнения
  2. Генерация мусора. Это определяет, как часто сборщику мусора нужно будет выполнять работу по очистке после нас.
  3. Выразительность. Приятно иметь возможность легко читать наш код без ущерба для гибкости и скорости.

Таким образом, я предлагаю вам использовать эту форму для обработки ваших потребностей в итерации массива в javascript:

for (var i = 0, item; item = array[i++];) {
  item.id++
}

Когда вам нужно повторить и изменить (да, друзья, эта страшная необходимость действительно возникает), вы можете легко адаптировать приведенный выше код, поскольку у вас есть доступ к вашей индексной переменной и контроль над поведением вашего повторения.

for (var i = 0, item; item = array[i];) {
  if (item.id > 200) array.splice(array.indexOf(item), 1)
  else               i++
}

Заключительные мысли и дополнение к тесту

Мои искренние симпатии как программиста связаны с выразительными языками со строгими типами, которые позволяют машине выполнять максимально возможную работу по проверке правильности моей программы и оптимизации ее скорости. К сожалению, javascript почти полностью оплачивает мои счета в настоящее время, и трава здесь определенно коричневая. Динамические типы, безудержная перегрузка, вариативность, непоследовательное и враждебное время выполнения и скользкая спецификация — все это работает против нас. Перед лицом таких проблем я считаю, что лучше всего признать язык таким, какой он есть, а не таким, каким мы хотели бы его видеть.

Javascript построен поверх C++, и, как и в случае со всеми абстракциями, он просачивается насквозь. Мы живем в среде выполнения, оптимизированной для объектов и мутаций, и мы, возможно, захотим дважды подумать, прежде чем поднимать флаги чистоты и отбрасывать десятилетия инженерных компромиссов и мудрости. В наших приложениях таится буги-ман производительности, поскольку пользователи и предприятия требуют более сложных и многочисленных функций, и мы изо всех сил пытаемся внедрить их в нашу работу.

Стремление к выразительности требует бдительности и честности в отношении наших вариантов. Надеюсь, я пролил здесь некоторый свет на итерацию Array, и, хотя вы можете не согласиться с моим выбором, я надеюсь, что это побудит вас задуматься о ваших собственных потребностях и компромиссах. Я включил в качестве дополнения ссылку на простой пакет, который вы можете установить и запустить, чтобы увидеть тесты для многих стилей, показанных в этом посте. Это задумано не как евангелие, а как простое, выполнимое подтверждение некоторых моих заявлений здесь.

https://github.com/stevekane/loop-benchmarks.git

Я с нетерпением жду ответа от вас здесь или в твиттере Стива Кейна. Я буду добавлять новые статьи в эту серию, так как чувствую, что могу сказать что-то интересное и обдуманное.