DEV Community

Douglas Modena
Douglas Modena

Posted on

JavaScript beyond Simple Functions

Wether you are new to programming or not, functions are at the core of many programming languages, especially JavaScript. Simple functions encapsulate logic, execute actions according to parameters and allow for returning some result or not. But JavaScript enables you to expand the trivial use of functions with some features.

In this article we will explore how to go beyond the use of simple functions, understanding the concept and usage of higher order functions, closures and generators.

Simple Functions

A function is a "set of statements that perform a task or calculates a value"¹. We can see below an example of a function in JavaScript:

function multiply(numberA, numberB, numberC) {
    return numberA * numberB * numberC;
}
Enter fullscreen mode Exit fullscreen mode

This function receives three parameters: numberA, numberB and numberC. It multiplies them and returns the result. This is the standard syntax for creating functions in JavaScript, but we could also write using the Arrow function syntax:

const multiply = (numberA, numberB, numberC) => numberA * numberB * numberC;
Enter fullscreen mode Exit fullscreen mode

Arrow functions provide a cleaner syntax, which in some cases may improve readability. Just keep in mind that this syntax has some limitations².

Pure Functions and Side Effects

You may have already heard the term "Pure Function", but what is that exactly?

Due to the flexibility that functions provide, nothing would prevent us from creating the following function:

let someValue = 7;

function sum(a, b) {
    someValue = a;
    return a + b;
}
Enter fullscreen mode Exit fullscreen mode

By looking at the function name, you would expect it to return the sum of a and b, but it is also changing a value from a variable that was not passed as parameter. We refer to this as a side effect of the function.

In order to be considered pure³, a function must comply with the following rules:

  1. Given the same inputs, the function must always return the same output;
  2. The function must not have any side effects.

We've already seen an example of a pure function, the one used to multiply three values. That function does not act on anything outside of it's scope and always returns the same values for the same input (for example, given the values 1, 2, and 3, it will always return 6).

It is easier to reason about and debug pure functions, but sometimes we actually want to have impure functions, such as one that returns a different value every time it is called, as we will see later in the article.

Higher order functions

Functions may receive different types of parameters. In the examples we've seen so far, we are always passing numbers as parameters. But functions can receive several other types, such as strings, arrays, booleans, objects and even functions.

You read that right: you can pass a function as a parameter to another function. Let's see how that is done:

function add2(number) {
    return number + 2;
}

function subtract2(number) {
    return number - 2;
}

function calculate(a, b, fn) {
    let c = a + b;
    return fn(c);
}

calculate(3, 4, add2); // 9
calculate(3, 4, subtract2); // 5
Enter fullscreen mode Exit fullscreen mode

The add2 function simply adds 2 to any number it receives, while the subtract2 subtracts 2 from its parameter.

The calculate function does 2 things: first it sums the values of a and b. Then the result of this sum (c) is passed to the function it receives as fn, to be returned at the end. This provides the flexibility of performing different types of calculations according to the provided function.

After the function definitions we execute calculate twice, with the same numbers: 3 and 4. The first time we are passing the add2 function as parameter, and the result is 3 + 4 = 7 => 7 + 2 = 9, therefore it returns 9.

The second time we pass the subtract2 function as parameter, and the result is 3 + 4 = 7 => 7 - 2 = 5, therefore it returns 5.

A function that receives another function as parameter is called a Higher Order Function, and it is a very useful feature of JavaScript. One popular higher order function in JavaScript is the sort() function.

const users = [
    { id: 1, age: 21, name: 'John Doe' },
    { id: 2, age: 26, name: 'Mark Johnson' },
    { id: 3, age: 19, name: 'Jack Smith' },
    { id: 4, age: 25, name: 'Mary Doe' },
    { id: 5, age: 30, name: 'Abigail David' },
    { id: 6, age: 29, name: 'Stuart Fox' }
];

const compareByAge = (a, b) => {
    if (a.age < b.age)
        return -1;

    if (a.age > b.age)
        return 1;

    return 0;
}

const compareByLastName = (a, b) => {
    const lastNameA = a.name.split(' ')[1];
    const lastNameB = b.name.split(' ')[1];

    return lastNameA.localeCompare(lastNameB);
}

users.sort((a, b) => a.id > b.id ? -1 : 1); // users ordered by descending id
users.sort(compareByAge); // users ordered by age 
users.sort(compareByLastName); // users ordered by last name
Enter fullscreen mode Exit fullscreen mode

The Array.prototype.sort() function is a higher order function that can have a comparer function passed as parameter. It can sort an array in different ways, depending on the function it receives. In our example, the first time we called sort() we passed a simple arrow function to sort the items by id in descending order. The second time we passed a function that compares the ages, and sorts by age in ascending order. The third time we compared last names, using the built-in string function localCompare().

Closures

Functions have scopes for accessing variables. In one of our examples, we were able to change a value that was defined outside of our function. Closures follow the same idea, which means that it provides access to variables within its scope.

According to MDN, a closure is "the combination of a function and the lexical environment within which that function was declared". Let's see how we could make use of a Closure with an example.

function sumAllMaker() {
    let total = 0;

    function sumAll(a, b) {
        total += (a + b);
        return total;
    }

    return sumAll;
}

const totalSum = sumAllMaker();

totalSum(1, 2); // 3
totalSum(1, 2); // 6
totalSum(3, 1); // 10
Enter fullscreen mode Exit fullscreen mode

To understand what is going on with our example, let's start by looking at the inner function, called sumAll(). This function receives 2 parameters, and sums them. The result is then aggregated to total, which is a variable outside of sumAll(). The variable was declared in the outer function, but just like we could previously access global variables, here we have access to variables in the outer function. So, subsequent calls to this function will always update the value stored in total.

Now let's look at the outer function: sumAllMaker(). It's starts by assigning 0 to our variable total. Then it defines the function sumAll() which is where the logic to update the total resides, and at the end it returns a reference to the function sumAll. That is important because now we can declare a variable, and by assigning sumAllMaker() to it we get access to the sumAll function.

In other words, now that we declared const totalSum = sumAllMaker(), calling totalSum() is the same as calling the sumAll() function.

Finally we call the function 3 times. Notice that the first time we call it with the values 1 and 2, it returns 3. The second time we call it with the same values, it returns 6. That is because the total is being kept and updated every time we call the function.

That is our closure in action! We called it once more, and it returned 10. Any subsequent calls will continue to update total and return it appropriately. Notice that this closure cannot be considered a pure function, because calling the same function twice with the same parameters return different results.

Generator Functions

Imagine that you are creating objects and you want each of them to have a different id. You don't want to manually assign ids, instead you would like to create a function to generate them.

One way to solve it would be to create a random number generator.

function getRandomInteger(min, max) {
    return Math.floor(Math.random() * (max - min) + min);
}

getRandomInteger(100, 999); // 926
getRandomInteger(100, 999); // 508
Enter fullscreen mode Exit fullscreen mode

Just as an exercise, would you be able to explain wether getRandomInteger() is a pure function, and why?

Using this function would not work for our scenario because nothing guarantees that the function would return unique numbers. At some point I could get the same id again, since it's random, there is no control on which number is returned.

As we've seen previously, we could use a closure to solve that.

function idMaker() {
    let id = 100;
    return function getId() { return ++id; }
}

const getId = idMaker();
getId(); // 101
getId(); // 102
getId(); // 103
Enter fullscreen mode Exit fullscreen mode

That's awesome! Now we know how to use a closure to generate ids. But there is another way to achieve this, by using a generator.

A generator function uses a special notation, including an asterisk after the function keyword: function*. This type of function returns an object that can be iterated upon. Think of it as a function that can be paused and resumed. Within the function, the yield statement is what "pauses" the function, returning some value. Subsequent calls to the generator will resume from the last yield until the next one, and so on.

Let's see an example using a simple generator that returns the vowels.

function* generateVowels() {
    yield 'a';
    yield 'e';
    yield 'i';
    yield 'o';
    yield 'u';
}

const vowels = generateVowels();
vowels.next().value; // 'a'
vowels.next().value; // 'e'
vowels.next().value; // 'i'
vowels.next().value; // 'o'
vowels.next().value; // 'u'
vowels.next().value; // undefined
Enter fullscreen mode Exit fullscreen mode

As you can see, every time we call the next() function and get the value, a subsequent yield is executed, until the iterator completes. After that, any further execution returns undefined.

Considering what we've seen so far, would you be able to create a generator that returns ids from 101 until 999? If you feel like it, pause reading and go try it for yourself! Otherwise, the answer follows below.

function* generateIds() {
    let id = 100;
    while (id < 999) { yield ++id; }
}

const ids = generateIds();
ids.next().value; // 101
ids.next().value; // 102
ids.next().value; // 103
Enter fullscreen mode Exit fullscreen mode

Conclusion

JavaScript is a powerful language, and it provides flexible ways of working with functions. Try to find opportunities in your routine to integrate some of these options, and go beyond the use of simple functions.


References

  1. Functions - MDN web docs
  2. Arrow function expressions - MDN web docs
  3. What is a Pure Function in JavaScript - freeCodeCamp
  4. Higher Order Functions in JavaScript – Explained with Practical Examples - freeCodeCamp
  5. Array.prototype.sort() - MDN web docs
  6. String.prototype.localeCompare() - MDN web docs
  7. Closures - MDN web docs
  8. Math.random() - MDN web docs
  9. function* - MDN web docs

Top comments (0)