Can’t Function Without You
There are two types of function in JavaScript. First is the classic ES5 and earlier version (let’s call these ES5 functions):
function average(a, b) {
return (a + b) / 2
}
// or
const average = function(a, b) {
return (a + b) / 2
}
The second way is using arrow functions, which were introduced in ES6:
const average = (a, b) => {
return (a + b) / 2
}
// or
const average = (a, b) => (a + b) / 2
There are two main reasons arrow functions were introduced. One was to provide a shorter syntax for writing a function, which makes passing functions as parameters more readable. Take the following examples:
pets.forEach(function(pet, index) {
setInterval(function() {
console.log(`You hear a ${pet.noise} from behind door number ${index + 1}.`);
}, 1000);
});
// versus
pets.forEach((pet, index) => {
setInterval(() => {
console.log(`You hear a ${pet.noise} from behind door number ${index + 1}.`);
}, 1000);
})
scratchBehindEar(pets.find(function(pet) {
return pet.type === 'dog';
}));
// versus
scratchBehindEar(pets.find(pet => pet.type === 'dog'));
The second reason was because of the confusing way the this
keyword works with ES5 functions. Take the following example from the Mozilla Developer Network JavaScript docs:
function Person() {
this.age = 0;
setInterval(function() {
this.age++;
}, 1000);
}
var p = new Person();
setTimeout(() => console.log(p.age), 5000);
> 0
The expected behavior is that it starts with the person’s age at 0 and increments every second. After 5 seconds, the age should be 5, and we should see that 5 get printed.
Instead, after 5 seconds, 0
is printed. This is because ES5 functions use execution context. Since the function that increments the age
is called at the global level, rather than in the context of the Person
constructor, when it tries to access this.age
, it is using the this
of the global context, not the this
of the Person
function.
If this is getting a bit confusing (ba dum tsh), you’re not alone. It’s bad enough with this simple context, but imagine debugging this issue in a large, complex web application.
Arrow Functions
The arrow functions handle this
differently. They use the lexical context, which means they use the context where they were written, which is what literally everybody expects, pretty much always.
That example becomes:
function Person() {
this.age = 0;
setInterval(() => {
this.age++;
}, 1000);
}
var p = new Person();
setTimeout(() => console.log(p.age), 5000);
And now after 5 seconds, it prints 5 as expected.
Honey, I Shrunk the Function
We can also shorten this down a bit as well. First of all, we can remove the braces from the setInterval
callback. This will add the side effect that it returns the age, but we don’t care what it returns, so that’s fine. The setInterval
call will become:
setInterval(() => this.age++, 1000);
Notice how we already made use of this shorthand in the setTimeout
call. There we return the result of console.log
. Since console.log
doesn’t return anything, it evaluates to undefined (not that it matters since we don’t care about the return value in this case).
Another shorthand I like to do is to write _
instead of ()
for functions that take no parameters. Technically, we’re declaring a function that takes one unused parameter whose name is _
, but I just find it quicker to read without all the extra parentheses. Plus, some IDEs ignore the unused parameter warning when it’s called _
. I also like to use _
when there are multiple parameters and one is unused.
(e.g. pets.forEach((_, index) => console.log(index));
)
In TypeScript, the parameters for a function parameter have to match the expected input, so if it is expecting a function that takes no parameters, we have to write
()
, but if takes 1 optional parameter, we can write_
.
This leaves us with a more concise but functionally identical version:
function Person() {
this.age = 0;
setInterval(_ => this.age++, 1000);
}
var p = new Person();
setTimeout(_ => console.log(p.age), 5000);
I Need Closure
When an arrow function is declared, a closure is created around the lexical context. This simply means that, along with the function itself, the lexical context is also stored so it can be referenced by the function.
In that last example, the context that the ES5 function incremented in wasn’t within our code due to setInterval, so let’s take a look at another example where the interval is another context we control:
function Pet(name) {
this.name = name;
this.identify = function identify() {
console.log('I am a free agent');
}
}
function Person(name) {
this.name = name;
this.adopt = function adopt(pet) {
pet.identify = function () {
console.log('I am the pet of', this.name);
}
}
}
let dog = new Pet('Mr. Tibbles');
let lindsay = new Person('Lindsay Jenkins');
lindsay.adopt(dog);
dog.identify();
> I am the pet of Mr. Tibbles
In the above example, we have two classes, Person
and Pet
. The class Pet
has an identify
function that prints out its adoption status. In the Person.adopt
function, we assign an ES5 function to pet.identify
that prints out who the pet belongs to. We create a dog, Mr. Tibbles, and a person, Lindsay Jenkins. When we run the code, the dog identifies itself as belonging to Mr. Tibbles
.
This is because the this.name
the function refers to uses its execution context, i.e. Pet
. Let’s try replacing this with an arrow function.
function Pet(name) {
this.name = name;
this.identify = function identify() {
console.log('I am a free agent');
}
}
function Person(name) {
this.name = name;
this.adopt = function adopt(pet) {
pet.identify = () => {
console.log('I am the pet of', this.name);
}
}
}
let dog = new Pet('Mr. Tibbles');
let lindsay = new Person('Lindsay Jenkins');
lindsay.adopt(dog);
dog.identify();
> I am the pet of Lindsay Jenkins
The only thing we changed here is assigning an arrow function to Pet.identify
inside of Person.adopt
instead of an ES5 function. When we call dog.identify()
, the dog correctly identifies itself as belonging to Lindsay Jenkins
.
Note that while in this case, we wanted an arrow function, and this is usually the case, had we instead tried to use the dog’s name in the function, perhaps to print `I am ${this.name}!`
, we would have wanted the ES5 function. Of course we could get both in the arrow function by saying `I am ${pet.name}, the pet of ${this.name}!`
Functions: Just Another Type
As we’ve seen many times already, JavaScript treats functions like any other value. This allows us to assign a function to a variable, pass a function as a parameter to another function, and return a function from a function.
Example:
const thisVariableIsAFunction = () => console.log('hi');
const thisFunctionTakesAFunction = (fn) => {
console.log('calling the callback');
fn();
};
const thisFunctionReturnsAFunction = () => {
console.log('returning a function');
return () => {
console.log('I was returned from a function');
thisFunctionTakesAFunction(thisVariableIsAFunction);
};
};
// Notice how we call the function,
// then call the value that was returned by the function
thisFunctionReturnsAFunction()();
> returning a function
I was returned from a function
calling the callback
hi
This can be a really powerful feature, but it can also be very confusing to understand, especially when things start getting nested.
For example, suppose we want to be able to sort a string of objects by any property.
JavaScript arrays have a sort
function thatan optional parameter of a compare function to use (by default it sorts using the <
operator). This compare function takes two parameters, lhs
and rhs
and returns:
-1
iflhs
<rhs
0
iflhs
=rhs
1
iflhs
>rhs
We could define a function like this for each property we want to sort by, but what if we had hundreds of properties? What if we’re getting a response back from a server and we don’t know what the properties will be?.
const compareByProperty = property => {
return (lhs, rhs) => {
if (lhs[property] < rhs[property]) {
return -1;
}
if (lhs[property] > rhs[property]) {
return 1;
}
return 0;
};
};
const list = [{ a: 5, b: 1 }, { a: 8, b: 2 }, { a: 2, b: 7 }, { a: 9, b: 1 }];
console.log('Sort by a:', list.sort(compareByProperty('a')));
console.log('Sort by b:', list.sort(compareByProperty('b')));
> Sort by a: [{ a: 2, b: 7 }, { a: 5, b: 1 }, { a: 8, b: 2 }, { a: 9, b: 1 }]
Sort by b: [{ a: 5, b: 1 }, { a: 9, b: 1 }, { a: 8, b: 2 }, { a: 2, b: 7 }]
We solve this by creating a function that returns a function. We create the function compareByProperty
, which takes a single parameter: the property name. This returns another function, which compares by that property.
We take that resulting function and pass it as a parameter to Array.sort()
, and we can see that the array is correctly sorted by each property.
Properties of a Function
One interesting thing about a function
in JavaScript is that it has a lot of the traits of an object
, including having properties. One such property is the length
property. It contains the number of arguments the function expects (the number written between parentheses when the function was defined). For example, take this example, loosely based around Express’s Application.use
function:
class Application {
use(fn) {
console.log(fn.length);
const err = new Error('oh no!');
const req = 'THIS IS REQ';
const res = 'THIS IS RES';
const next = 'THIS IS NEXT';
if (fn.length > 3) {
console.log('fn has at least four params');
fn(err, req, res, next);
} else {
console.log('fn has three params or fewer');
fn(req, res, next);
}
}
}
const app = new Application();
app.use((req, res, next) => {
console.log(`the 2nd param is ${res}`);
console.log(`the 3rd param is ${next}`);
});
console.log('---------------');
app.use((error, req, res, next) => {
console.log(`the 2nd param is ${req}`);
console.log(`the 3rd param is ${res}`);
});
> 3
fn has three params or fewer
the 2nd param is THIS IS RES
the 3rd param is THIS IS NEXT
---------------
4
fn has at least four params
the 2nd param is THIS IS REQ
the 3rd param is THIS IS RES
We can see that using fn.length
, we can see how many parameters fn
takes. Even though JavaScript allows us to pass any number of parameters to the function (ignoring extras or using undefined for missing parameters), we are able to check how many the function is “supposed” to take