Skip to content

Instantly share code, notes, and snippets.

@ljharb
Last active March 20, 2024 13:40
Star You must be signed in to star a gist
Save ljharb/58faf1cfcb4e6808f74aae4ef7944cff to your computer and use it in GitHub Desktop.
Array iteration methods summarized

Array Iteration

https://gist.github.com/ljharb/58faf1cfcb4e6808f74aae4ef7944cff

While attempting to explain JavaScript's reduce method on arrays, conceptually, I came up with the following - hopefully it's helpful; happy to tweak it if anyone has suggestions.

Intro

JavaScript Arrays have lots of built in methods on their prototype. Some of them mutate - ie, they change the underlying array in-place. Luckily, most of them do not - they instead return an entirely distinct array. Since arrays are conceptually a contiguous list of items, it helps code clarity and maintainability a lot to be able to operate on them in a "functional" way. (I'll also insist on referring to an array as a "list" - although in some languages, List is a native data type, in JS and this post, I'm referring to the concept. Everywhere I use the word "list" you can assume I'm talking about a JS Array) This means, to perform a single operation on the list as a whole ("atomically"), and to return a new list - thus making it much simpler to think about both the old list and the new one, what they contain, and what happened during the operation.

Below are some of the methods that iterate - in other words, that operate on the entire list, one item at a time. When you call them, you provide a callback function - a single function that expects to operate on one item at a time. Based on the Array method you've chosen, the callback gets specific arguments, and may be expected to return a certain kind of value - and (except for forEach) the return value determines the final return value of the overarching array operation. Although most of the methods are guaranteed to execute for each item in the array - for all of them - some of the methods can stop iterating partway through; when applicable, this is indicated below.

All array methods iterate in what is traditionally called "left to right" - more accurately (and less ethnocentrically) from index 0, to index length - 1 - also called "start" to "end". reduceRight is an exception in that it iterates in reverse - from end to start.

Special Mention: Array.from

Array.from, introduced in ES6/ES2015, accepts any array, "arraylike", or "iterable". An "arraylike" object is anything with a length, which includes arrays, strings, and DOM NodeLists (as produced by document.querySelectorAll, eg). An "iterable" is anything that has a Symbol.iterator method, which includes arrays, strings, NodeLists, Maps, Sets, and potentially many more.

To efficiently convert any non-array into one for the purpose of using the below methods, you can utilize Array.from's "mapper" argument:

const tagNames = Array.from(document.querySelectorAll('.someClass'), (el) => el.tagName);

const codePointValues = Array.from('some string with 💩 emoji 💩!', (codePoint) => codePoint.codePointAt(0));

const mapValues = Array.from(map, ([key, value]) => value);

forEach:

  • callback answers: here’s an item. do something nutty with it, i don't care what.
  • callback gets these arguments: item, index, list
  • final return value: nothing - in other words, undefined
  • example use case:
[1, 2, 3].forEach(function (item, index) {
  console.log(item, index);
});

map:

  • callback answers: here’s an item. what should i put in the new list in its place?
  • callback gets these arguments: item, index, list
  • final return value: list of new items
  • example use case:
const three = [1, 2, 3];
const doubled = three.map(function (item) {
  return item * 2;
});
console.log(three === doubled, doubled); // false, [2, 4, 6]

flatMap:

  • callback answers: here’s an item. what thing, or array of things, should i put in the new list in its place?
  • callback gets these arguments: item, index, list
  • final return value: list of new items
  • example use case:
const three = [[1, 2], [3, 4], [5, 6]];
const flatDoubled = three.flatMap(function (items) {
  return items.map(function (item) { return item * 2; });
});
console.log(three === flatDoubled, flatDoubled); // false, [2, 4, 6, 8, 10, 12]

filter:

  • callback is a predicate - it should return a truthy or falsy value
  • callback answers: should i keep this item?
  • callback gets these arguments: item, index, list
  • final return value: list of kept items
  • example use case:
const ints = [1, 2, 3];
const evens = ints.filter(function (item) {
  return item % 2 === 0;
});
console.log(ints === evens, evens); // false, [2]

reduce:

  • callback answers: here’s the result from the previous iteration. what should i pass to the next iteration?
  • callback gets these arguments: result, item, index, list
  • final return value: result of last iteration
  • example use case:
// NOTE: `reduce` and `reduceRight` take an optional "initialValue" argument, after the reducer callback.
// if omitted, it will default to the first item.
const sum = [1, 2, 3].reduce(function (result, item) {
  return result + item;
}, 0); // if the `0` is omitted, `1` will be the first `result`, and `2` will be the first `item`

reduceRight: (same as reduce, but in reversed order: last-to-first)

some:

  • callback is a predicate - it should return a truthy or falsy value
  • callback answers: does this item meet your criteria?
  • callback gets these arguments: item, index, list
  • final return value: true after the first item that meets your criteria, else false
  • note: stops iterating once it receives a truthy value from your callback.
  • example use case:
const hasNegativeNumbers = [1, 2, 3, -1, 4].some(function (item) {
  return item < 0;
});
console.log(hasNegativeNumbers); // true

every:

  • callback is a predicate - it should return a truthy or falsy value
  • callback answers: does this item meet your criteria?
  • callback gets these arguments: item, index, list
  • final return value: false after the first item that failed to meet your criteria, else true
  • note: stops iterating once it receives a falsy value from your callback.
  • example use case:
const allPositiveNumbers = [1, 2, 3].every(function (item) {
  return item > 0;
});
console.log(allPositiveNumbers); // true

find:

  • callback is a predicate - it should return a truthy or falsy value
  • callback answers: is this item what you’re looking for?
  • callback gets these arguments: item, index, list
  • final return value: the item you’re looking for, or undefined
  • note: stops iterating once it receives a truthy value from your callback.
  • example use case:
const objects = [{ id: 1, name: 'a' }, { id: 2, name: 'b' }, { id: 3, name: 'b' }, { id: 4, name: 'c' }];
const found = objects.find(function (item) {
  return item.name === 'b';
});
console.log(found === objects[1]); // true

findLast:

  • callback is a predicate - it should return a truthy or falsy value
  • callback answers: is this item what you’re looking for?
  • callback gets these arguments: item, index, list
  • final return value: the item you’re looking for in reverse list order, or undefined
  • note: stops iterating once it receives a truthy value from your callback.
  • example use case:
const objects = [{ id: 1, name: 'a' }, { id: 2, name: 'b' }, { id: 3, name: 'b' }, { id: 4, name: 'c' }];
const found = objects.findLast(function (item) {
  return item.name === 'b';
});
console.log(found === objects[2]); // true

findIndex:

  • callback is a predicate - it should return a truthy or falsy value
  • callback answers: is this item what you’re looking for?
  • callback gets these arguments: item, index, list
  • final return value: the index of the item you’re looking for, or -1
  • note: stops iterating once it receives a truthy value from your callback.
  • example use case:
const objects = [{ id: 1, name: 'a' }, { id: 2, name: 'b' }, { id: 3, name: 'b' }, { id: 4, name: 'c' }];
const foundIndex = objects.findIndex(function (item) {
  return item.name === 'b';
});
console.log(foundIndex === 1); // true

findLastIndex:

  • callback is a predicate - it should return a truthy or falsy value
  • callback answers: is this item what you’re looking for?
  • callback gets these arguments: item, index, list
  • final return value: the index of the item you’re looking for in reverse list order, or -1
  • note: stops iterating once it receives a truthy value from your callback.
  • example use case:
const objects = [{ id: 1, name: 'a' }, { id: 2, name: 'b' }, { id: 3, name: 'b' }, { id: 4, name: 'c' }];
const foundIndex = objects.findIndex(function (item) {
  return item.name === 'b';
});
console.log(foundIndex === 2); // true
@ljharb
Copy link
Author

ljharb commented Jan 14, 2017

@ggauravr I intentionally omitted that one, because sort mutates.

@yogx4u
Copy link

yogx4u commented Jan 17, 2017

Very good explanation. Also I didn't knew of reverse iteration like in case of reduceRight. Thank you for that. 👍

@felisio
Copy link

felisio commented Jan 18, 2017

Hi, I'm forked and translate for Portugues-Brazil

https://gist.github.com/felisio/4eb4d427951b26e3d1616a411e93df87

@sAbakumoff
Copy link

@azl397985856
Copy link

This is great! Very easy to understand. Thank you

@bestwestern
Copy link

Thanks - very good.

  1. They could delete every or some, couldn't they? (By negating the criteria you could get the other)
  2. Are they speedy? (forEach vs for(var i=0;i<_.length;i++){})

@ddanielbee
Copy link

@Jiharb This is great. Something else that could help people wanting some more beef about the feature is a link to its MDN page.

@Bandito11
Copy link

This awesome, thanks!

@icanhazstring
Copy link

Thanks man. Really good explanation

@ljharb
Copy link
Author

ljharb commented Jan 19, 2017

@bestwestern yes, they are speedy, but performance is the least important thing to worry about when writing code - clarity is the most. Both every and some are needed to clearly express your intention, which is the whole point. You could implement all of these with reduce alone, but that wouldn't be as clear.

@eliortabeka
Copy link

This is awesome man, Thanks!

@NaiyaShah-BTC
Copy link

Superb!! Nice efforts

@mr-devboy
Copy link

Always used this methods. But not all supported by IE

@ljharb
Copy link
Author

ljharb commented Feb 14, 2017

@mr-devboy that's where the es5-shim and es6-shim come in. I recommend every site always use them.

@vladyn
Copy link

vladyn commented Apr 2, 2017

Very nifty. Thank you!

@OliverMensahDev
Copy link

Nice one

@andrey-smolko
Copy link

andrey-smolko commented May 4, 2017

Great work!
I think it is also worth to mention that there is a second optional parameter in addition to callback for map(), forEach() and several others:
(callback[, thisArg])
thisArg - this inside callback function.

@ljharb
Copy link
Author

ljharb commented May 14, 2017

@andrey-smolko I've intentionally omitted that; with ES6 arrow functions there's really zero point in using the thisArg ever again.

@derek-knox
Copy link

@ljharb I think it's worth delving into reduceRight() just like the other methods. Just mentioning that it's the opposite order of reduce() isn't helpful (especially when the reduce() example is a summation). Where's the value in summing in reverse order?

@ljharb
Copy link
Author

ljharb commented Feb 24, 2018

@derek-knox i mean, where's the value in summing in any order? "summing" isn't the only or the primary benefit of reduce. It really just depends on what data you're reducing, what order it's in, what you're reducing it to, and which order makes the most sense to do so.

Most examples that would demonstrate more advanced uses of reduce would be great, but they might be a bit distracting for this basic explainer.

@rrakso
Copy link

rrakso commented Jul 16, 2019

Great article!

@frankie9793
Copy link

Simple and effective way to understand. Thank you very much

@nancysellars
Copy link

Thanks so much! I appreciate your contribution and effort!!! Very helpful!

@Andrew-Cottrell
Copy link

Andrew-Cottrell commented Jun 8, 2020

filter:

  • callback is a predicate - it should return a truthy or falsy value
  • callback answers: should i keep this item?
  • callback gets these arguments: item, index, list
  • final return value: list of kept items

The idea of the filter method, or the callback, 'keeping' items seems slightly inaccurate and potentially confusing. Obviously no items are discarded from the source list, which is left unmodified, and it appears misleading to unintentionally imply all items are copied to the returned list and then some items are kept and others removed.

An alternative, and perhaps clearer, way to put it might be

filter:

  • callback is a predicate - it should return a truthy or falsy value
  • callback answers: does this item pass through? the filter (from the source list to the returned list)?
  • callback gets these arguments: item, index, list
  • final return value: list of items that passed through the filter

[edited, subsequent to the below comment, to omit repetition of the word "filter"]

@ljharb
Copy link
Author

ljharb commented Jun 8, 2020

@Andrew-Cottrell a water filter keeps the water, a red/blue/green filter keeps that color, an air filter keeps the air. I think phrasing the question as "what does it keep" is much clearer than repeating the word "filter", even with the "pass through" qualifier.

@Andrew-Cottrell
Copy link

Andrew-Cottrell commented Jun 8, 2020

I don't disagree with the perspective of those examples, but there is an alternative perspective: the coffee filter keeps the grounds, so we may drink the liquid; the red filter keeps the non-red light, and allows red light to pass through; the air filter keeps the dust, so we may breathe the clean air. I certainly don't mean to imply one perspective is better or worse than the other. Sorry to belabor the point; I won't say any more on this topic.

@djleonskennedy
Copy link

Best post about js arrays in the world! Thanks

@carloswm85
Copy link

Add a self URL reference to this gist. I downloaded to one of my college classes folders as raw. Adding h1 and and index at the beginning of the gist would be a nice addition. Good job. Thanks for your work.

@ljharb
Copy link
Author

ljharb commented Jul 16, 2021

@carloswm85 thanks, i've done so (i held off adding an index; it's not super long)

@natesire
Copy link

Thank you so much for sharing this. I never ever thought of categorizing the functions by mutating versus non mutating. That's going to be super helpful when programming in a functional style.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment