beyond the for loop
@tmcw / Tom MacWright / Mapbox this is a topic that’s close to my heart because it’s fundamental, difficult, and subtle: something as simple as a loop can expressed in vastly different ways. knowing which route to take for every situation will help you write straightforward code that reflects your ideas.
loops are one of the bits of fundamental magic in programming
if { } else { }
computers can make decisions
for (;;) { }
computers can do work
sparknotes arrays
arrays are collections of data
var numbers = [1, 2, 3];
arrays have a length property
numbers.length; // 3
arrays have methods
numbers.push(4);
// puts 4 into numbers
arrays contain data
var four = numbers;
problem one: make some animals rock
var animals = ['cats', 'dogs'];
our desire
['cats rock', 'dogs rock'];
using for
var animals = ['cats', 'dogs'];

for (var i = 0; i < animals.length; i++) {
animals[i] = animals[i] + ' rock';
}
for is a way of saying do this repeatedly for is a classic example of the concept of 'imperative programming': a style that emphasizes telling computers what to do. there are other styles, like declarative programming, which is telling computers what you want - a big example of that is SQL.
equivalent to while() { } and do { } while() and even goto
for loops have three basic elements
where we usually make the loop counter
for (var i = 0; i < animals.length; i++) {
where we usually check if we're done
for (var i = 0; i < animals.length; i++) {
where we usually increment the loop counter
for (var i = 0; i < animals.length; i++) {
the loop is counting from 0 to 1
is that what i'm doing?
 in out cats dogs cats rock dogs rock
i'm not counting from 0 to 1, i'm transforming data
function theyRock(creatures) {
return creatures + ' rock';
}
theyRock('cats'); // cats rock
function theyRock(creatures) {
return creatures + ' rock';
}
// feed ['cats', 'dogs'] into theyRock
daring thesis: we should do that
let's meet the family
map, reduce, filter
there are others but these are the hits some of the others are .some(), which tells you if any element of an array passes a test, .every(), which says if ever element does
they live on arrays
[].filter; // there it is
for IE8 and lower use es5-shim and weep legacy tears some people use underscore or lodash for this. that's fine too, but it's best to use tools that do exactly what you want: the mission of underscore is to bring other functional programming goodies to javascript, not to serve as a compatibility layer.
so what do they do?
map
creates a new array by calling a function on every element of an old array
that thing from earlier but with map
var animals = ['cats', 'dogs'];
function theyRock(creatures) {
return creatures + ' rock';
}
var rockinAnimals = animals.map(theyRock);
this says what it does: we are turning an array of animals into an array or rockin animals with a named function it also does not change the original array, but creates a new one
aka 'mutation', changing arrays in place breeds confusion
var animals = ['cats', 'dogs'];
function makeEmRock(animals) {
for (var i = 0; i < animals.length; i++)
animals[i] = animals[i] + ' rock';
return animals;
}
var rockinAnimals = makeEmRock(animals);
// rockinAnimals = [cats rock, dogs rock]
// animals = [cats rock, dogs rock] NOOOOOOO
var animals = ['cats', 'dogs'];
function makeEmRock(animals) {
return animals.map(function(animal) {
return animal + ' rock';
});
}
var rockinAnimals = makeEmRock(animals);
// rockinAnimals = [cats rock, dogs rock]
// animals = [cats, dogs]
filter
create a new array with only the elements that pass a test
var animals = ['cats', 'dogs'];
var catsOnly = [];
for (var i = 0; i < animals.length; i++)
if (animals[i] === 'cats')
catsOnly.push(animals[i]);

// catsOnly = ['cats']
but we'd want to abstract the comparison because of those cats that look like dogs because of their crazy cat show owners doing weird haircuts or grooming or whatever
not judging, keep rocking cat show people
this is a function that returns true or false. comparisons are values, liberate them from if statements!
function thatsACat(animal) {
return animal === 'cats';
}
var animals = ['cats', 'dogs'];
var catsOnly = [];
function thatsACat(animal) {
return animal === 'cats';
}
for (var i = 0; i < animals.length; i++)
if (thatsACat(animals[i]))
catsOnly.push(animals[i]);

// catsOnly = ['cats']
filter to the rescue
var animals = ['cats', 'dogs'];
function thatsACat(animal) {
return animal === 'cats';
}
var catsOnly = animals.filter(thatsACat);
// catsOnly = ['cats'] like map, filter creates a new array and doesn't mess with your old one this concept, called 'immutability', is really deep. see immutable-js for an example of it taken to the extreme. the benefits of immutability, besides simpler code, extend into performance and the ability to undo changes to data.
map
array ⇢ array of transformed values
filter
array ⇢ array of filtered values
one last kind of loop: aggregation
array ⇢ sum
reduce
given an array and a starting value, get a result by calling a function with each element of the array and the value.
um, what?
reduce is more general than the others. let's look at it two ways.
compute the sum of an array of numbers
var numbers = [4, 8, 15, 16, 23];
var sum = 0;
for (var i = 0; i < numbers.length; i++) {
sum += numbers[i];
}
// sum = 66
the gist: start off with a sum of 0, add each number to it
1. starting value = 0 2. add each number to current sum 3. voila!
here's the same thing with reduce!
var numbers = [4, 8, 15, 16, 23];
var sum = numbers.reduce(function(currentSum, value) {
return currentSum + value;
}, 0);
first notice the starting value of 0, given as the second argument
var numbers = [4, 8, 15, 16, 23];
var sum = numbers.reduce(function(currentSum, value) {
return currentSum + value;
}, 0);
this enters the function as currentSum
var numbers = [4, 8, 15, 16, 23];
var sum = numbers.reduce(function(currentSum, value) {
return currentSum + value;
}, 0);
each loop returns a new value that is the new currentSum
var numbers = [4, 8, 15, 16, 23];
var sum = numbers.reduce(function(currentSum, value) {
return currentSum + value;
}, 0);
so, for the first iteration it looks like
var numbers = [4, 8, 15, 16, 23];
var sum = numbers.reduce(function(currentSum=0, value=4) {
return 0 + 4;
}, 0);
and then the second
var numbers = [4, 8, 15, 16, 23];
var sum = numbers.reduce(function(currentSum=4, value=8) {
return 4 + 8;
}, 0);
and third
var numbers = [4, 8, 15, 16, 23];
var sum = numbers.reduce(function(currentSum=12, value=15) {
return 12 + 15;
}, 0);
here's another way of putting it reduce is worth learning.
you may know reduce as fold or foldl from other languages.
it can do all the other ones
reduce's initial value doesn't have to be a number. what if it's an array?
remember this?
var animals = ['cats', 'dogs'];

function theyRock(list, creatures) {
list.push(creatures + ' rock');
return list;
}

var rockinAnimals = animals.reduce(theyRock, []);
and this?
var animals = ['cats', 'dogs'];
function thatsACat(list, animal) {
if (animal === 'cats') {
list.push(animal);
}
return list;
}
var catsOnly = animals.reduce(thatsACat, []);
we can even write a general 'filter' function using reduce.
function filter(array, fn) {
return array.reduce(function(memo, value) {
return fn(value) ?
memo.concat([value]) : memo;
}, []);
}
so, use reduce always? for what it's worth, array.splice() is basically the reduce of mutable array functions: you can use it instead of shift, unshift, pop, push, and slice. but nobody ever remembers the order of arguments and the name sounds weird so they don't.
no
use the right tool for the job j.mp/js-loops
use map for transforming values use filter for filtering values use reduce for aggregating values use for loops for performance hotspots make sure they're hotspots first.
chaining
made famous by \$
\$('div')
.css('background', 'Gainsboro')
.slideIn()
.doBarrelRoll();
this is a real color
\$('div')
.css('background', 'Gainsboro')
.slideIn()
.doBarrelRoll();
it's grayish
anyway: chaining is useful for doing multiple steps quickly without intermediate variables
thing
.function()
.function()
.function();
map and filter return arrays and are functions on arrays
var rockin = ['cats', 'dogs']
.map(theyRock);

var catsOnly = ['cats', 'dogs']
.filter(thatsACat);
THEREFORE
var rockinCatsOnly = ['cats', 'dogs']
.filter(thatsACat)
.map(theyRock);
// ['cats rock']
the things that used to be in loops are now reusable and combinable
var rockinCatsOnly = ['cats', 'dogs']
.filter(thatsACat)
.map(theyRock);
// ['cats rock']
✌ thanks | @tmcw | Tom
let's talk blocks vs functions real quick
for loops are one kind of block:
for (var i = 0; i < 3; i++) {
}
blocks are in for loops, switch statements, if / else
when you see { } but no function, you're looking at a block.
blocks are chunks of work: the code within them might run lots of times
for (var i = 0; i < 3; i++) {
}
or it might never run
if (false) {
}
blocks and functions both have { } but have a big difference: scope
scope is containingness
variables are contained in scopes
this saves us from conflict. without scope, every time you named a variable you'd have to be sure it was the only thing named that everywhere. that's crazy.
for instance,
function divide(a, b) {
var divided = a / b;
divided only exists here!
return divide;
}
var myResult = divide(1, 3);
divided doesnt exist here!
so variables are contained by scopes and only functions give you scopes
but, Tom, we were just talking about loops, why does this matter?
for loops are blocks, map, filter, reduce are functions.
example of disaster
var numbers = [1,2,3];
var sum = 0;
// i will leave my
// important business here, yes
var importantBusiness = 42;

// copy & pasted this - me
for (var i = 0; i < numbers.length; i++) {
sum = i += numbers[i];
var importantBusiness = sum + 1;
}
importantBusiness is now equal to 6!?
o no!
variables leak out & in for loops. another variable leaked out of that one:
var numbers = [1,2,3];
var sum = 0;
// i will leave my
// important business here, yes
var importantBusiness = 42;

// copy & pasted this - me
for (var i = 0; i < numbers.length; i++) {
sum = i += numbers[i];
var importantBusiness = sum + 1;
}
// i = 16
now see the functional version
// i will leave my
// important business here, yes
var importantBusiness = 42;

var numbers = [1,2,3];
var sum = numbers.reduce(function(current, value) {
var importantBusiness = current + 1;
return current + value;
}, 0);

importantBusiness = 42; still! phew.
leak-free! the variables current and value are contained within the function, and when we talk about importantBusiness, it refers to the variable in function scope, so the other one isn't affected.
having scopes also fixes this problem:
for (var i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i);
}, 10);
}
logs 3, 3, 3
voila!
[0, 1, 2].map(function(i) {
setTimeout(function() {
console.log(i);
}, 10);
});
logs 1, 2, 3
the value is in the function's scope: it doesn't change after being passed to the function.
[0, 1, 2].map(function(i) {
setTimeout(function() {
console.log(i);
}, 10);
});
logs 1, 2, 3