In this fifth post in the series of posts titled “The Anatomy of a JavaScript function”, we’ll revise all the different ways of defining functions in JS.

Anatomy of a JS function part 5 Image by CodingExercises

Earlier in this series of articles, we’ve learned that functions are sort of like pre-built machines that we build once and run as many times as we need.

There are several ways of defining functions in JavaScript - i.e. of building these “code machines”. Each way has its strengths and weaknesses.

  1. Function declarations (with the caveat of function declarations in conditionals)
  2. Function expressions and IIFEs (Immediately invoked function expressions)
  3. Function constructors
  4. Generator functions

Let’s look at each of these in detail. There are many additional gotchas and caveats to all these different types of functions, so for a full list of all the possible scenarios, scroll to the bottom of this article.

1. Function declarations

Here’s an example of a function declaration without params:

function exampleFunct1() {
    // a function declaration expecting no params
}

Here’s an example of a function declaration with params:

function exampleFunct2(param1, param2) {
    // a function declaration expecting two params
}

Function declarations are hoisted and they have a built-in array-like arguments collection. Hoisting makes it possible to call a function before it’s declared, like this:

exampleFunct1();
function exampleFunct1() {
    // this function can be called before it's declared
}

Hoisting is possible with function declarations because they are saved into memory “on the first pass” that JavaScript engine makes when it initially reads our code - in the above example, the exampleFunct1 function declaration.

This “first pass” is one of the two passes that the JavaScript engine makes when it deals with our code. The first time it reads our code, it saves into memory all the variables and function declarations it finds. This is what’s sometimes called the “creation phase” of setting up the execution context for a given function.

After this “first pass” (that is, after the “creation phase”), the JavaScript engine runs our JavaScript code line by line. This “second pass” is what’s called the “execution phase”.

Additionally, the arguments collection allows us to inspect the arguments that were passed into the function when we called it:

exampleFunct2(firstParam, secondParam) {
    console.log(argments.length);
    console.log(arguments[0]);
    console.log(arguments[1]);
}
exampleFunct2("1st parameter", "2nd parameter")

1a. Conditionals and function declarations

There are JavaScript engines that will throw an error if we define a function inside a code block, between the curly brackets, such as inside statements like if, for, and while.

What makes matters even worse is that this behavior is sometimes error-free in “sloppy” mode in JS, and only shows up when in strict mode. For example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
(function() {
    'use strict';

    if(true) {
        function isItTrue() {
            return 'evaluated to "true"'
        }
    } else {
        function isItTrue() {
            return 'evaluated to "false"';
        }
    }
    console.log(typeof isItTrue === 'function'); // returns: false
    console.log(isItTrue()); // Error!
})();

On line 14 of the above code, we are running itsTrue(), which results in this:

Uncaught ReferenceError: isItTrue is not defined.

This is happenning because we’ve defined the itsTrue function inside a conditional code block.

However, this is perfectly legit in sloppy mode:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
(function() {

    if(true) {
        function isItTrue() {
            return 'evaluated to "true"'
        }
    } else {
        function isItTrue() {
            return 'evaluated to "false"';
        }
    }
    console.log(typeof isItTrue === 'function'); // returns: true
    console.log(isItTrue()); // returns: 'evaluated to "true"'
})();

No error is produced by the above code.

A way to go around this issue is to use a function expression instead of a function declaration, like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
(function() {
    'use strict';
    let isItTrue;
    if(true) {
        isItTrue = function() {
            return 'evaluated to "true"'
        }
    } else {
        isItTrue = function() {
            return 'evaluated to "false"';
        }
    }
    console.log(typeof isItTrue === 'function'); // returns: true
    console.log(isItTrue()); // returns 'evaluated to "true"'
})();

In the code above, we cheated, kind of. How come? Well, we declared the isItTrue as a variable, outside of the if...else conditional statments. Then, inside each of the conditions we specified, we use a function expression, to assign an anonymous function to our pre-declared isItTrue variable. Problem solved!

2. Function expressions

To write a function expression, a function is assigned to a variable:

let exampleFunct3 = function() {
    // a function expression expecting no params
}

// or:
let exampleFunct4 = function(firstParam, secondParam) {
    // a function expression expecting two params
}

As we can conclude from the above two examples of function expressions, one major syntactic difference between function expressions and function declarations is: function expressions don’t have to be named.

However, it is possible to convert these anonymous, unnamed functions, into named functions:

let exampleFunct3 = function exampleFunct3() {
    // a named function expression without params
}
let exampleFunct4 = function exampleFunct4() {
    // a named function expression with params
}

There is a bit of a gray area regarding the above explanations, however. To understand why, let’s discuss the naming of functions in a bit more detail.

Each function object comes with the name method, and thus we can check the value of a function’s name. Anonymous functions don’t have a name, so the value returned from the name method in this case is an empty string: "".

To prove that an anonymous function expression’s name method returns an empty string, let’s run this code:

(
    function() { console.log("whatever") }
).name; // returns: ""

Let’s now try running the name method on the exampleFunct3 from a couple of code snippets ago. Actually, let’s re-declare it, so that all our code is in one place; then we’ll run the name method on the function object:

let exampleFunction3 = function() {
    console.log("This is the exampleFunction3");
}
exampleFunction3.name; // returns: "exampleFunction3"

The above code proves that the exampleFunction3 is not really an anonymous function. This is because the JS engine can deduce the name of exampleFunction3 because we’ve assigned it to a variable.

For all practical intents and purposes, the only truly anonymous function expression in JS is an anonymous IIFE (immediately invoked function expression).

2a. Function expressions as IIFEs

These are useful when we want to define and run the function at the same time, and we don’t need to re-use it:

(function(firstParam) {
    console.log(firstParam);
})('First param here');

The example above is using an anonymous IIFE. It’s ok that IIFEs are usually anonymous, because the way they are written serves one purpose: to not allow scope bleed, i.e. to keep the code inside the IIFE as self-contained as possible. Entire JavaScript libraries are written using the IIFE pattern.

As a side note, IIFEs were extremely popular before using JavaScript modules became commonplace. However, even that being the case, IIFEs are still a valuable tool in your arsenal.

Additionally, since they are self contained, making IIFEs named rather than anonymous might seem to not have any additional benefits. However, that is not entirely true. Consider this example:

(function factorial(n) {
    if(!n) {
        return 1;
    }
    return n * factorial(n-1);
} ()
)

Now we have an IIFE function that returns a 1. However, we can easily make that more versatile:

(function factorial(n) {
    if(!n || !Number.isNaN(n)) {
        return 1;
    }
    return n * factorial(n-1);
} (prompt("Please enter a number"))
)

Here are a few important facts about function expressions:

  1. A named function expression is useful when we debug, because the names appear in stack traces.
  2. As a corollary to the above point, using named function expressions decreases the number of anonymous functions in stack traces.
  3. Since the function is named, you can easily run recursion with it or detach event listeners
  4. Function expressions are not hoisted, and thus we can’t call them before we’ve defined them.

This is a good time to mention the one major difference between function declarations and function expressions.

2b. Function expressions as object methods

Another way to build a function expression is to define it as a method of an object:

const car = {
    lights: "off",
    lightsOn: function() {
        this.lights = "on";
    },
    lightsOff: function() {
        this.lights = "off";
    },
    lightsStatus: function() {
        console.log(`The lights are ${this.lights}`);
    }
}

Now we can work with our car object like this:

car.lightsStatus();
car.lightsOn();
car.lightsStatus();
car.lightsOff();
car.lightsStatus();

Function expressions as object methods are a very common use case in JavaScript.

2c. Function expressions as shorthand methods in ES6 object literals

Shorthand object methods are a way to define function expressions inside object literals, using ES6 syntax. Here’s our car object from the above example, only this time re-written with shorthand methods:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const car = {
    lights: "off",
    lightsOn() {
        this.lights = "on";
    },
    lightsOff() {
        this.lights = "off";
    },
    lightsStatus() {
        console.log(`The lights are ${this.lights}`);
    }
}
car.lightsStatus();
car.lightsOn();
car.lightsStatus();
car.lightsOff();
car.lightsStatus();

The shorthand syntax looks almost identical, except for the fact that we omit the colon character, i.e. the :, and the function keyword in shorthand methods.

The number one benefit of shorthand methods is the fact that unlike ES5 function expressions in objects, the shorthand methods are named functions, which makes them easier to debug. That’s probably one of the reasons why the Vue framework very often relies on shorthand methods.

Actually, shorthand methods are so great that they’ve got another trick up the sleeve: computed properties syntax.

2d. Function expressions as shorthand methods with the computed properties syntax

To understand how shorthand ES6 methods work with the computed properties syntax, we’ll work with our car object again. This time, there’s a twist: we’ll return it from a function.

Note that while the below function is a function declaration (not a function expression!), we’ll build up to having shorthand computed properties methods inside of it.

But let’s not get ahead of ourselves, and instead, let’s take things one step at a time.

First, we’ll write a function declaration, and call it getACar. This function will return an object:

function getACar() {
    return {
        lights: "off",
        lightsOn() {
            this.lights = "on";
        },
        lightsOff() {
            this.lights = "off";
        },
        lightsStatus() {
            console.log(`The lights are ${this.lights}`);
        }
    }
}

So far so good, we’re just returning our car object, which is exactly the same object like we had before. We can now call the getACar function, and it will return our car object.

getACar();

We can even call a method on our returned object, like this:

getACar().lightsStatus(); // returns: "The lights are off";

Now let’s extend the returned car object, like this:

function getACar(carType, color, year) {
    return {
        carType: carType,
        color: color,
        year: year,
        lights: "off",
        lightsOn() {
            this.lights = "on";
        },
        lightsOff() {
            this.lights = "off";
        },
        lightsStatus() {
            console.log(`The lights are ${this.lights}`);
        },
        getType() {
            return this.carType
        },
        getColor() {
            return this.color
        },
        getYear() {
            return this.year
        }        
    }
}
getACar("sports car", "red", "2020").getType(); // returns: "sports car"

Now comes the fun part: to access a property in an object, we can either use the dot notation, or the brackets notation.

This means that the following syntax is completely legitimate:

let aCarObj = {
    [year]: "2020",
    [color]: "red",
    [carType]: "sports car"
}
aCarObj.carType; // returns: Uncaught ReferenceError: year is not defined

The returned error is completely expected, since we haven’t declared the year variable anywhere. Let’s fix the error, like this:

let year = "2020";
let color = "red";
let carType = "sports car";

let aCarObj = {
    [year]: "2020",
    [color]: "red",
    [carType]: "sports car"
}
aCarObj.carType; // returns: undefined

Now, trying to access the carType property returns undefined instead of “sports car”. Just why that is so, can be seen if we just inspect the entire object:

aCarObj;

The returned object looks like this:

{2020: "2020", red: "red", sports car: "sports car"}

This brings us to an a-ha moment: the brackets notation makes it possible to run expressions inside of them. Actually, this is the regular behavior of the brackets notation.

What that means is that the values of the computed properties are indeed computed, which means that the values of the variables are evaluated inside brackets and used as object properties’ keys.

Let’s rewrite the aCarObj:

let a = "year";
let b = "color";
let c = "carType";

let aCarObj = {
    [a]: "2020",
    [b]: "red",
    [c]: "sports car"
}
aCarObj.carType; // returns: "sports car"

In the above code, the value of [a] was evaluated to year; the value of [b] was evaluated to color, and the value of [c] was evaluated to carType.

This brings us to the point of this section. Just like we can use computed properties in the above use case, we can use them on shorthand object methods, like this:

let lightsOnMethod = "lightsOn";
let lightsOffMethod = "lightsOff";
let lightsStatusMethod = "lightsStatus";
let getTypeMethod = "getType";
let getColorMethod = "getColor";
let getYearMethod = "getYear";

function getACar(carType, color, year) {
    return {
        carType: carType,
        color: color,
        year: year,
        lights: "off",
        [lightsOnMethod]() {
            this.lights = "on";
        },
        [lightsOffMethod]() {
            this.lights = "off";
        },
        [lightsStatusMethod]() {
            console.log(`The lights are ${this.lights}`);
        },
        [getTypeMethod]() {
            return this.carType
        },
        [getColorMethod]() {
            return this.color
        },
        [getYearMethod]() {
            return this.year
        }        
    }
}
getACar("sports car", "red", "2020").getType(); // returns: "sports car"

2e. Function expressions as callbacks

A function expression can be used as a callback. For example:

[1,2,3].map( function(x) {
    return x*x
});

In the above example, we’re using an ES5 function expression. However, very often, the callback function comes in the form of an arrow function. Here’s the above example, re-written in ES6 syntax:

[1,2,3].map( x => x*x )

Let’s now discuss why the above callback function is considered a function expression.

2f. Function expressions as arrow functions

Arrow functions can also be used inside function expressions, as follows:

let anExampleFunctA = () => { return true };

let anExampleFunctB = x => x*x;

let anExampleFunctC = (x, y) => x*y;

There are a few gotchas for arrow functions:

  1. Arrow functions are anonymous, but the JS engine can deduce the name from the variable name (the left side of the assignment)
  2. The arguments collection is not available for arrow functions, but we can use the ... rest operator
  3. Arrow functions don’t have their own execution context (which is different from other function expressions and function declarations, which have their own this when they’re ran)

2f. How does the JavaScript engine load functions into the execution context?

The engine loads function declarations before any code is executed. This is what’s commonly known as hoisting. It happens before the code is executed.

Function expressions are loaded at run-time, once the engine reaches that specific line of code (after the execution context’s creation phases has completed).

That’s why you can call function declarations in any part of your code, even at the very top of the file, while function expressions can only be called once they’ve been loaded into memory, at run-time, which practically means that function expressions are always called after they’re declared, or they’d throw an error otherwise.

How to tell if a JavaScript function is a function declaration or a function expression? Here’s a simple rule of thumb: if it begins with function, it’s a function declaration. In all other cases, it’s a function expressions.

3. Function constructors

Each and every single function in JavaScript is a Function object. Here’s the proof:

(function(){}).constructor === Function

Since a JavaScript function is a Function object, to build a new function, we can just call the Function object’s constructor, using the new keyword:

const exampleFunct5 = new Function();

Inside the call to the Function object’s constructor, we list the parameteres, as strings.

Interestingly, we even pass the body of the function into this list of “parameters as strings”:

const exampleFunct5 = new Function("firstParam", "secondParam", "console.log(firstParam + secondParam));

Functions built using the Function constructors have the same behavior as function expressions:

  1. They are not hoisted
  2. Their this is runtime-bound
  3. Their bind method is the same
  4. They also have the arguments object

In most cases, it’s best not to use the function constructor syntax. There are several reasons why, all stemming from the fact that the way it works is similar to the eval() method. For function constructors the function body is evaluated at runtime, which leads to a number of issues:

  1. function declarations and function expressions are more efficient
  2. function constructors are parsed each time the constructor is called
  3. it’s a security risk
  4. there are no code optimizations
  5. it’s harded to debug
  6. editos can’t auto-complete functions defined using function constructor syntax

A possible situation in which a function constructor might be used is to return the global object, be it window or global:

const globalObj = new Function('return this');

Why does this work? It works because functions built with the function constructor are always added to the global scope.

4. Generator functions

In JavaScript, functions traditionally have the run-to-completion implementation, meaning, once a function is ran, it cannot be “paused” - it runs until all the code in the function body is executed.

This traditional behavior was changed with the introduction of generator functions.

In JS, a generator function can be stopped at any point during its execution, then, at a later time, you can call it back and it continues executing from where it left off.

A generator function is defined like this:

function * exampleGeneratorFunct() {
    // code here...
}

The above example shows that a generator function is the same as a function declaration, save for the additional * right after the function keyword.

Here’s the full list of all the ways we can define functions in JavaScript:

  1. “Regular” function declarations
  2. Function declaration caveats in block statements (if, for, etc)
  3. Anonymous function expressions
  4. Named function expressions
  5. Immediately invoked function expressions
  6. Function expressions as object methods
  7. Function expressions as shorthand methods in ES6 object literals
  8. Function expressions as shorthand methods with the computed properties syntax
  9. Function expressions as callbacks
  10. Function expressions as arrow functions
  11. Function contstructors
  12. Generator functions

This completes part 5 of The Anatomy of a JavaScript function article series.