JavaScript Shinies: Array Functions

Though they’re not part of the ES6 spec, array functions are commonly underused and not necessarily well understood. We’ll go through them here and take a look.

Matt Burgess

--

Javascript has some rich functionality built right into the array prototype, which makes handling array or objects (bear with me on that one, we’ll get to it) a hell of a lot easier than less dedicated methods.

The Problem We Solve With It

There are a lot of different ways to do simple actions on an array in JavaScript. It’s still surprisingly common to see the following.

var total = 0;for (var i = 0; i <= orders.length; i++) {
total = total + orders[i].value;
}
console.log(total);

This solution works, for a certain value of “works”. It’s very imperative, though, explicitly stepping through the indexes of the array. It also doesn’t particularly clearly describe the intent. The loop being used here isn’t really what we want to do, it’s just a means to an end. If you thought about how you’d describe what you wanted, you probably wouldn’t say “count the number of orders and then start a counter at 0 and keep adding one to it and then get the order at that index and add the value…”. You’d say something like “Get the value of each order and add them up”.

It’s also quite error prone as the index is 0 based it’s easy to create off-by-one errors. There’s one above. An array of 5 items will have a last index of 4, but a length of 5, so that should actually be < not <= up there.

Enter JavaScript’s Array Functionality

So to make more clear, idiomatic array functionality we’ll have a look at four related JavaScript array functions: these are forEach, map, filter, and reduce. This is not the comprehensive documentation of functionality for arrays within JavaScript, but they’re enough to be getting on with.

Using forEach

We’ll start with forEach, because it’s the most like the above example, and is probably the most comprehensible. Like some other languages, like PHP, the forEach function just loops through an array.

The forEach function differs from the other functions here in that it doesn’t actually return anything useful. It just “does a thing” with each item.

let total = 0;orders.forEach(order => total += order.value);console.log(total);

There’s actually a better way to do this with reduce but we’ll get to that in time and you can already see we’ve got much clearer and simpler code.

Foreach is useful when you want to do something sort of arbitrary to each item. Maybe it’s appending chunks of it to a string, or… it’s honestly hard to tell. In truth, forEach is significantly less useful than filter, map, or reduce. But is a handy fallback where those more specific functions aren’t appropriate.

Map Function

The map function is probably the most generally useful of these array functions. It creates a new array, with an entry for every item in the original array. It’s a great way to populate or reshape the values in an array.

let ordersWithShipping = orders.map(order => {
order.shipping = calculateShipping(order);
order.total = order.value + order.shipping;
return order;
});

This will add two new properties to your order object, shipping cost and the total. Note that you have to return the order for this to do anything, and also that it doesn’t affect the original orders array in any way. It’s a totally new array.

It’s important to be aware that the entry in the array will be whatever you return from the map function. In the above example we return an order object, so that’s what is in the array. If we didn’t return anything (or returned conditionally) we’d have an empty entry in the array. It wouldn’t just skip it.

It’s not all added complexity, though. Map is a good way to pull things out of complex data arrays.

let orderIds = orders.map(order => { 
return order.id;
});
// or fancier and betterlet orderIds = orders.map(({id}) => id);

This gives you an array of all the order ids from the orders array. Using map is a really nice way to move around and reshape your data.

The Filter Function

Filter is a typically-simple function that does what it says on the tin. Like map, filter will return a new array. The function simply needs to return a boolean of some kind. Trying to return an object or value like you do in a map is just going to be seen as “true”.

Again, to clarify, the filter function doesn’t change the values in the array in any way, it’s just a filter, a gatekeeper.

Also to clarify, despite the wording the boolean returned from the filter doesn’t filter stuff out, but rather filters it in.

let people = [
{id: 1, name: "Ash", age: 10},
{id: 2, name: "Sam", age: 23},
{id: 3, name: "Shannon", age: 46},
{id: 4, name: "Kim", age: 18}
];
let adults = people.filter(({age}) => age > 18);// [ {id: 2, name: "Sam", age: 23},
// {id: 3, name: "Shannon", age: 46}]

You might notice Kim isn’t in there. Our filter was > and simply didn’t match the boolean. We could easily fix that with >= instead. Something else to point out is that though these functions aren’t ES6 features, they play very well with them. Filters, especially, become nice and readable when they’re a quick inline fat arrow with a conditional rather than an explicit function on multiple lines, etc.

A common use for filters is removing empty entries. A quick way to do that is to make the callback function the constructor for the boolean type itself.

let valid = people.filter(Boolean);

Where these functions become particularly handy is the fact that they return arrays. This means that they can be quite happily chained.

let adultNames = people.filter(({age}) => age > 18)
.map(({name}) => name);

The Hard One: Reduce

Reduce is probably the most powerful and versatile function. It’s definitely the hardest to explain.

Reduce is used to collapse an array down to a single value by iterating it. Unlike all the previous examples, the callback for reduce doesn’t just have one argument (the current item in the iteration) but two. These are the “accumulator” and the current item, respectively.

A simple demonstration is our previous summing of value.

let orderTotal = orders.reduce((total, {value}) => total += value);

This is slightly simpler than the previous version, and doesn’t require creating a temporary variable. Well, it sort of does, but it’s internal to the function. It turns the whole thing into a single line.

This is the simplest form of reduce, but it’s not the most useful or powerful. Reduce has a second argument, after the callback. To be clear, I don’t mean the argument in the callback, I mean after it.

array.reduce(callback, startingValue);

The default is usually just zero or an empty string or whatever JS wants it to be, and then gets clobbered by the first iteration. Nevertheless, it is possible to set it to something.

let orderTotal = orders.reduce((total, {value}) => { 
return total += value;
}, shippingCost);

The starting value could be an array though, and then your reduce could conditionally push data to that array. Which you can then filter or map or reduce again, or whatever you like.

More particularly, though, this starting value can be an empty object.

It’s here that reduce gets particularly powerful. Let me show you an example. We’ll take our peeps from earlier.

let people = [
{id: 1, name: "Ash", age: 10},
{id: 2, name: "Sam", age: 23},
{id: 3, name: "Shannon", age: 46},
{id: 4, name: "Kim", age: 18}
];

That’s nice and everything, and we could happily filter that to get a particular name, etc as we did before. But what if we wanted that as an object, instead of an array? That way it would be faster and more consistent to access it.

We could reduce down our array to a single object, keyed by their id. And discarding their age, we’ll just have key/value pairs with their name. Also, I’ve done all the previous examples with inline anonymous functions, but it’s worth noting that isn’t required. So with this one we’ll actually make a named function here and use that.

const personReducer = (peopleObj, person) => {
peopleObj[person.id] = person.name;
return peopleObj;
};
const peopleById = people.reduce(personReducer, {});// {1: "Ash", 2: "Sam", 3: "Shannon", 4: "Kim"}

A few things worth noting are that we could have destructured person, like this.

const personReducer = (peopleObj, {id, name}) => {
peopleObj[id] = name;
return peopleObj;
};

Sometimes it’s clearer and more readable not to. It’s certainly not mandatory, so you should use your own judgement.

Secondly, note that you have to return the accumulator. That will essentially set the object’s current state as the new accumulator, and continue rolling new properties into it.

Combining Complex Array Functions

I do a lot of work with blockchain code, and there is something called an AST, which is essentially a complex object that defines input and outputs, types, property access rules, etc. It’s intended for use by compilers and the like, but I wanted to be able to transform it so that the interface names and types are available in JS.

Here’s a single function definition ripped from the AST.

{
"body": {
"id": 146,
"nodeType": "Block",
"src": "1629:39:0",
"statements": [
{
"expression": {
"argumentTypes": null,
"id": 144,
"isConstant": false,
"isLValue": false,
"isPure": false,
"lValueRequested": false,
"leftHandSide": {
"argumentTypes": null,
"id": 142,
"name": "_seatPrice",
"nodeType": "Identifier",
"overloadedDeclarations": [],
"referencedDeclaration": 69,
"src": "1639:10:0",
"typeDescriptions": {
"typeIdentifier": "t_uint256",
"typeString": "uint256"
}
},
"nodeType": "Assignment",
"operator": "=",
"rightHandSide": {
"argumentTypes": null,
"id": 143,
"name": "seatPrice",
"nodeType": "Identifier",
"overloadedDeclarations": [],
"referencedDeclaration": 137,
"src": "1652:9:0",
"typeDescriptions": {
"typeIdentifier": "t_uint256",
"typeString": "uint256"
}
},
"src": "1639:22:0",
"typeDescriptions": {
"typeIdentifier": "t_uint256",
"typeString": "uint256"
}
},
"id": 145,
"nodeType": "ExpressionStatement",
"src": "1639:22:0"
}
]
},
"documentation": null,
"id": 147,
"implemented": true,
"isConstructor": false,
"isDeclaredConst": false,
"modifiers": [
{
"arguments": null,
"id": 140,
"modifierName": {
"argumentTypes": null,
"id": 139,
"name": "onlyOwner",
"nodeType": "Identifier",
"overloadedDeclarations": [],
"referencedDeclaration": 1258,
"src": "1619:9:0",
"typeDescriptions": {
"typeIdentifier": "t_modifier$__$",
"typeString": "modifier ()"
}
},
"nodeType": "ModifierInvocation",
"src": "1619:9:0"
}
],
"name": "setSeatPrice",
"nodeType": "FunctionDefinition",
"parameters": {
"id": 138,
"nodeType": "ParameterList",
"parameters": [
{
"constant": false,
"id": 137,
"name": "seatPrice",
"nodeType": "VariableDeclaration",
"scope": 147,
"src": "1596:14:0",
"stateVariable": false,
"storageLocation": "default",
"typeDescriptions": {
"typeIdentifier": "t_uint256",
"typeString": "uint256"
},
"typeName": {
"id": 136,
"name": "uint",
"nodeType": "ElementaryTypeName",
"src": "1596:4:0",
"typeDescriptions": {
"typeIdentifier": "t_uint256",
"typeString": "uint256"
}
},
"value": null,
"visibility": "internal"
}
],
"src": "1595:16:0"
},
"payable": false,
"returnParameters": {
"id": 141,
"nodeType": "ParameterList",
"parameters": [],
"src": "1629:0:0"
},
"scope": 1160,
"src": "1574:94:0",
"stateMutability": "nonpayable",
"superFunction": null,
"visibility": "public"
},

Elegant, no? I might have missed a bracket there, it’s hard to see. This stuff isn’t particularly human readable. I should also add that this is just one of a huge array of elements and properties. In fact, there are ten thousand lines of this crud. Obviously this isn’t what I want to actually read with JavaScript, so the object needs to be reformatted.

Here is the code that does that.

const mapParameters = parameters => {
return parameters.map(
({name, typeString:type}) => ({ name, type })
);
};
const interfaceFromAST = ast => {
return ast.nodes
.filter(node => nodeType === 'FunctionDefinition')
.reduce((functions, {name, parameters, returnParameters}) => {
parameters = mapParameters(parameters.parameters);
returnParameters = mapParameters(returnParameters.parameters);
functions[name] = { name, parameters, returnParameters };
return functions;
}, {});
};

Something that is worth pointing out is one of the lines above where we do the map, the syntax looks slightly strange. In particular there are brackets around the returned object. This is a common mistake when using a fat arrow function to return an object directly. Because the curly brackets are used to define the function boundary, but also the object literal. So there’s some syntax ambiguity to it that isn’t obvious without the brackets.

In any case, this gives me a much more useful format for my data.

{
setSeatPrice: {
parameters: [{name: "price", type: "uint256"}],
returnParameters: []
},
getSeatByUuid: {
parameters: [{name: "uuid", type: "bytes32"}],
returnParameters: [
{name: "index", type: "uint256"},
{name: "uuid", type: "bytes32"},
{name: "owner", type: "address"},
{name: "passenger", type: "address"},
{name: "price", type: "uint256"}
]
}
}

Conclusion

JavaScript’s array methods aren’t that new. But there are a lot of people who are either not using them, using them wrong, or drastically under-using them. I should also admit that there are others that I’m not even talking about here, like array.find() or array.flatMap() but there’s no intent here to comprehensively detail the entire array prototype.

Again, these are not ES6 features. They don’t need to be installed, they don’t need to be shimmed, and they should be completely supported. An exception to this (and the reason I don’t use it) is array.find(callback), which is not supported by many versions of IE, so you need to use array.ilter(callback)[0] to do the same thing.

By chaining and combining these features, arrays and objects can become a lot easier to work with.

--

--

Matt Burgess

Senior Web Developer based in Bangkok, Thailand. Javascript, Web and Blockchain Developer.