Перебирающие методы массивов изнутри
Методы массивов forEach
, map
, filter
, some
, every
и reduce
являются неотъемлимой частью функционального программирования на JavaScript. В этой статье подробно рассмотрим применение каждого из них, а также их реализацию в виде функций.
forEach
Самый простой способ разобраться, как работают функции высшего порядка, принимающие callback’и, — самостоятельно переписать несколько нативных методов. Начнём с самого простого метода Array.prototype.forEach
. Метод массивов forEach
принимает два аргумента: первый (обязательный) — callback
функция, которая будет выполнена для каждого элемента массива один раз и второй (необязательный) — значение, которое будет использовано в качестве this
при вызове функции callback
. Работает это следующим образом:
В callback
функцию передаётся три аргумента: item
, соответствующий элементу массива, index
, равный номеру элемента в массиве, и arr
— ссылка на массив. Таким образом, справедливо следующее выражение arr[index] === item
. Аргументам передаваемым в callback
функцию можно давать разные названия в зависимости от контекста выполнения — выполнение callback
функции от этого не изменится:
Как я писал выше, помимо callback
функции метод forEach
принимает ещё один аргумент — значение, которое будет использовано в качестве this
при вызове функции callback
. Зачем это может понадобиться? По умолчанию this
не определён, то есть равен undefined
. В некоторых ситуациях, особенно при работе с функциями конструкторами, необходимо задать контекст выполнения callback
функции:
Подобный код, на первый взгляд, может оказаться полностью рабочим. Тем не менее, после его выполнения всё, что мы увидим будет ошибка Cannot read property 'say' of undefined
. Данная ошибка показывает, что при выполнении метода forEach
на передаваемом в метод mumble
массиве phrases
не задаётся контекст испольнения callback
функции. Проще говоря, callback
функция пытается выполнить подобный код undefined[say](words)
. Решается данная проблема элементарно — передётся второй аргумент в метод forEach
, который и указывает ему, что брать за this
:
forEach своими руками
Теперь, когда вы знаете, как работает forEach
, настало время написать функцию, которая делает всё тоже самое. При создании функции each
будем руководствоваться тремя правилами:
- Функция принимает три аргумента: массив, по которому будет происходить итерация,
callback
фунцию, которая будет выполнена для каждого элемента массива, и значение, которое будет использованоcallback
функцией в качествеthis
. callback
функция, в свою очередь, также работает с тремя аргументами: текущий элемент массива, индекс элемента, ссылка на сам массив.- Функция ничего не возвращает.
Самое очевидное решение: используя цикл for
перебрать каждый элемент массива и выполнить передаваемую callback
функцию с каждым. Сделать это очень просто:
Функция each
будет отлично работать, за исключением того, что не соблюдается часть первого правила — для callback
функции нельзя задать контекст, то есть нельзя присвоить значение this
:
Чтобы можно было осуществлять передачу ключевого слова this
в функцию each
достаточно воспользоваться методом функций call
, который позволяет вызвать функцию и явно указать, на что будет указывать ключевое слово this
.
Теперь функция each
работает с массивами в точности так, как и метод forEach
. Чтобы её протестировать, можно запустить её вместе с кодом из предыдущего примера.
Несмотря на кажущуюся одинаковость метода forEach
и написанной нами выше функции each
, между ними есть одно очень важное различие: метод forEach
работает только с массивами, в то время как функция each
может также успешно работать с любыми коллекциями.
Скорее всего, вы уже видели ранее подобную конструкцию, которую используют многие JavaScript разработчики для итерации по всем элементам из DOM коллекции:
Подобная конструкция может ввести в замешательство практически любого программиста, который раньше не использовал её сам. Подробное объяснение, как именно она работает можно найти в этом вопросе на Stackoverflow. Нужна она лишь для того, чтобы провести итерацию по всем элементам выбранной DOM коллекции и выполнить callback
функцию для каждого из них. При использовании написанной нами функции each
надобности в такой хитрой конструкции нет, так как она поддерживает работу не только с массивами, но и с любыми коллекциями, в том числе и коллекциями DOM элементов:
Когда появится широкая поддежка ES6 метод forEach
можно будет использовать с DOM коллекциями более простым способом с помощью оператора Spread:
map
Метод массивов map
похож по своей функциональности на forEach
, но результат выполнения callback
функции добавляется в новый массив, который возвращается после последней итерации. Другими словами, результатом метода map
всегда является новый массив с результатами выполнения функции callback
на исходном массиве.
Также, как и в случае с forEach
map
помимо callback
функции принимает второй параметр, который позволяет задать контекст и явно указать this
:
Как вы могли заметить, при использовании map
и forEach
всегда использовалась анонимная функция в качестве callback
. Это совсем не обязательно. Вы можете объвить функцию, которую хотите использовать в качестве callback
заранее, а затем просто передать её в качестве параметра.
В некоторых ситуациях можно вообще не создавать отдельную функцию и пользоваться нативными методами. Пример выше, в котором каждое число в массиве возводилось в степень, соответствующую своему индексу, можно значительно сократить:
Передвая callback
функцию подобным образом вы не теряете возможность явно указать this
. Таким образом, метод grabPhrases
из функции конструктора Person
также можно немного сократить:
В отличие от forEach
при использовании map
вам становится доступен chaining. Это значит, что вы можете последовательно применить метод на возвращенный после map
массив.
map своими руками
Как и при создании аналога forEach
напишем небольшие правила, которыми будем руководствоваться при создании функции map
:
- Функция возвращает новый массив, оставляя исходный без изменений.
- Всё, что возвращает
callback
функция добавляется в новый массив. - Функция принимает три аргумента: массив, по которому будет происходить итерация,
callback
фунцию, которая будет выполнена для каждого элемента массива, и значение, которое будет использованоcallback
функцией в качествеthis
. callback
функция, в свою очередь, также работает с тремя аргументами: текущий элемент массива, индекс элемента, ссылка на сам массив.
Правил стало больше, но последними двумя мы уже пользовались при создании функции each
, а, значит, вы уже знаете, как с ними справиться.
Менять исходный массив нельзя, поэтому нужно создать новый массив в самом начале выполнения функции map
. Назовём его results
. В созданный нами массив results
при кажом выполнении будем добавлять результат выполнения функции callback
с помощью метода push
. После завершения последней итерации всё, что остаётся сделать — вернуть массив results
.
Написанная нами функция map
работает точно так же, как и метод массивов map
, но, как и each
, может принимать в качестве аргументов любый другие коллекции.
Небольшой совет: не используйте метод map
для манипуляций с коллекциями DOM элементов, например, чтобы добавить класс всем элементам коллекции.
Данный код сработает и всем ссылкам будет добавлен класс link
, но пимимо этого будет создан дополнительный пустой массив, что скажется на производительности при большом объеме итерируемой коллекции. В подобных случаях следует использовать исключительно forEach
.
Тем не менее, map
отлично подходит для получения данных из DOM коллекций. Например, получение всех href
атрибутов будет выглядеть следующим образом:
filter
Метод filter
, как и следует из названия, служит для фильтрации массива по правилам, заданным в callback
функции. Так же, как в случае с map
создаётся новый массив, куда добавляются все элементы прошедшие провеку колбэком.
При использовании метода filter
результатом выполнения может быть любое значение, но данные из исходного массива будут добавлены только в том случае, если значение является правдивым. Напомню, что правдивыми значениями являются все, кроме:
- пустой строки
''
- числа ноль
0
false
undefined
null
Тем не менее, пользоваться подобным способом фильтрации массива не стоит в силу его неочевидности. Поэтому рекомендуется создавать callback
функцию таким образом, чтобы она всегда возвращала либо true
, либо false
.
Немного более сложный пример использования метода filter
. Допустим, что мы получаем JSON файл с сервера с подобным содержимым:
После получения данных (с помощью AJAX или JSONP, например) мы хотим их отфильтровать, узнав из свойства isCustomer
, является ли данных человек нашим клиентом или нет. Вот здесь и пригодится метод filter
:
Или же можно руководствоваться немного другим, более сложным принципом при выборе цели. Например, мы решили, что если email клиента не начинается с буквы j
, то он определённо нам не подходит.
filter своими руками
Как и для прошлых функций map
и forEach
напишем небольшой свод правил:
- Функция возвращает новый массив, оставляя исходный без изменений.
- Данные исходного массива передаются в
callback
функцию. Результат выполненияcallback
функции решает будет ли добавлен данный элемент в новый массив. - Функция принимает три аргумента: массив, по которому будет происходить итерация,
callback
фунцию, которая будет выполнена для каждого элемента массива, и значение, которое будет использованоcallback
функцией в качествеthis
. callback
функция, в свою очередь, также работает с тремя аргументами: текущий элемент массива, индекс элемента, ссылка на сам массив.
С помощью всё того же метода функций call
мы вызываем callback
функцию, но на этот раз всё, что нас будет интересовать — вернула ли функция правдивое значение. Если результат содержит правдивое значение, то данные будут добавлены в массив, если же нет, то просто проигнорированы.
Удостоверимся, что функция filter
работает, как мы её и задумывали:
Небольшой хак: чтобы отфильтровать все ложные значения из массива можно воспользоваться конструктором Boolean
:
some и every
Методы some
и every
во многом похожи друг на друга. Оба метода возвращают true
или false
. some
возвращает true
тогда, когда хотя бы один элемент массива отвечает переданным в callback
функцию условиям. every
вернёт true
, когда все элементы массива отвечают данным условиям. Звучит грозно, но, на самом деле всё очень просто.
Методы some
и every
очень удобно использовать вместе с методом filter
для вложенных массивов. Чтобы понять, как это работает, опять представим, что мы получили данные с сервера в виде JSON файла, который содержит массив объектов (наших покупателей). У каждого покупателя есть свойство purchases
, которое представляет собой список приобретённых покупателем товаров в нашем магазине ранее.
Чтобы понять, какие клиенты нам принесли больше всего прибыли, мы хотим их отфильтровать и посмотреть, кто из них покупал у нас технику от Apple (iPhone, Macbook или iMac). Метод some
поможет сделать это с помощью всего нескольких строк кода.
some и every своими руками
Правила:
- Функция возвращает только
true
илиfalse
- Каждое значение передаётся в
callback
функцию и на результате её выполнения для всех элементов массива решается, какой будет результат. - Функция принимает три аргумента: массив, по которому будет происходить итерация,
callback
фунцию, которая будет выполнена для каждого элемента массива, и значение, которое будет использованоcallback
функцией в качествеthis
. callback
функция, в свою очередь, также работает с тремя аргументами: текущий элемент массива, индекс элемента, ссылка на сам массив.
Функция some
при каждой итерации проверяет, является ли результат выполнения callback
функции правдивым. Если она находит хотя бы один правдивый результат, то прерывает своё выполнение и сразу возвращает true
.
Функция every
построена по противоположному принципу. Если хотя бы одно значение не является верным, то сразу же возвращается false
без дальнейшего перебирания массива.
Функции every
и some
работают идентично соответствующим им методам и будут давать одинаковые резутаты. Тем не менее, написанные нами функции работают лучше нативных методов. Почему? Используя методы массивов some
и every
вы подразумеваете, что callback
функция будет выполнена для всех элементов без исключения. Но, может оказаться так, что первый элемент в массиве уже содержит нужные нам данные и итерация по всем остальным будет абсолютно бесполезной. В написанных нами функциях таких итераций не будет — когда будет найдено искомое значение функция сразу же прекратит своё выполнение. Подобный подход может дать достаточно ощутимый прирост производительности при работе с большими объемами данных, например, с JSON файлами содержащими несколько тысяч объектов.
reduce
callback
функция всех рассмотренных выше методов массивов работает с одинаковым набором данных: значением, индексом и массивом. Метод reduce
не такой, как все. Принцип его работы немного отличается от всех остальных методов. Начнём сразу с примера:
Метод reduce
принимает два аргумента callback
функцию и начальное значение, которое будет присвоено аргументу result
в примере выше при первой итерации. callback
функция принимает целых 4 аргумента: промежуточное значение (аргумент result
в примере выше), элемент массива, индекс элемента и сам массив. После каждой итерации в промежуточное значение записываются новые данные, которые берутся из результата выполнения функции callback
при прошлой итерации:
Разумеется, reduce
может работать с любыми типами данных, не только с числами. Пример со строками (в данном случае в качестве начального значения стоит передавать пустую строку):
Пример с многомерным массивом (начинаем с пустого массива):
reduce своими руками
Вы уже знаете — у нас есть правила:
- Функция принимает три аргумента: массив,
callback
функцию, начальное значение. - После каждой итерации в промежуточное значение перезаписывается значением, полученным в результате выполнения
callback
функции. 3.callback
функция принимает четыре аргумента: промежуточное значение, текущий элемент массива, индекс элемента, ссылка на сам массив. - Явно указать значение
this
нельзя.
В нативном методе указывать значение this
нельзя, поэтому вместо введения в функцию ещё одного аргумента thisArg
мы просто передаём null
в вызов функции.
Протестируем написанную нами функцию reduce
на предыдущих примерах, чтобы убедиться, что всё работает, как мы и ожидаем.
Комментарии