So many closures
TL;DR When only a small portion of your function needs to change, try to encapsulate it one level of abstraction above.
const getNavTo = index => () => {
navigateTo(index)
}
document.querySelectorAll('.tab').forEach(($el, index) => {
$el.addEventListener('click', getNavTo(index))
})
Closures are functions that return a function. This is a simple yet handy pattern that allows you to:
- create a scope in which values are retained, in the wrapper function
- build a function based on arguments, this is the returned function
function wrapperFunction (...scopeArgs) {
// this code is meant to run only once
// but it creates a scope for the returned function
function returnedFunction (...funcArgs) {
// this code will be run many times
}
return returnedFunction
}
Example 1: Basic scope understanding
function closure (verb) {
const prefix = 'Simon says'
return adjective => `${prefix} ${verb} ${adjective}`
}
const jump = closure('jump')
const run = closure('run')
jump('higher') // Simon says jump higher
run('faster') // Simon says run faster
Here you can see that verb
remains accessible in the returned function, and that the prefix
we defined internally is also available in the function’s scope. We just built a function from parameters and created a scope.
In practice, there are many use cases for closures: initialize a , factory functions, currying…
Example 2: Easily generate a series of similar functions.
const healthPointUpdate = (points) => () => {
state.health += points
}
}
const hit = healthPointUpdate(-30)
const fall = healthPointUpdate(-10)
const potion = healthPointUpdate(100)
const state = { health: 100 }
hit() // state.health === 70
fall() // state.health === 60
hit() // state.health === 30
potion() // state.health === 130
Example 3: Lay the groundwork for the returned function
We can consider the wrapper function as a way to “prepare” the use of the returned function. For example, some API calls can require some quite complicated header setup.
const createApiCall = (url, token) => {
const options = {
header: new Headers({ Authorization: token}),
method: 'GET'
}
return async (endpoint) => await fetch(`${url}/${endpoint}`, options)
}
const apiCall = createApiCall('site.com/api', 'HE$V539BS_O')
const moviesFrom2011 = await apiCall('/movies/2011')
const castFromFriends = await apiCall('/actors/series/Friends')
Example 4: Tailor made functions
Say you generate an array of DOM nodes based on data, and you need some event listeners to go with it, and this listener should be able to access all of the array’s items data. Here’s some example data:
const userData = [
{ id: 1, name: 'Julia', phone: '123098123' },
{ id: 2, name: 'Morty', phone: '000777444' },
]
There are several ways to go about this. Let’s start with an anti-pattern: a god object.
window.users = userData
function listener(event) {
console.log(window.users[event.target.dataset.id])
// { id: 1, name: 'Julia', phone: '123098123' }
}
window.users.forEach(user => {
const $node = document.createElement('div')
$node.innerText = user.name
$node.dataset.id = user.id
$node.addEventListener('click', listener)
document.body.appendChild($node)
})
Another way to communicate data to the listener, since it receives the DOM node, would be to have the actual node carry it:
function listener(event) {
console.log(event.target.dataset)
// DOMStringMap {id: "1", name: "Julia", phone: "123098123"}
}
userData.forEach(user => {
const $node = document.createElement('div')
$node.innerText = user.name
Object.keys(user).forEach(key => $node.dataset[key] = user[key])
$node.addEventListener('click', listener)
document.body.appendChild($node)
})
Now with a closure, we can make this process much cleaner!
function getListener(user) {
return (event) => {
console.log(user)
// { id: 1, name: 'Julia', phone: '123098123' }
}
}
userData.forEach(user => {
const $node = document.createElement('div')
$node.innerText = user.name
$node.addEventListener('click', getListener(user))
document.body.appendChild($node)
})