Different ways of defining a function in JavaScript

At first glance, JavaScript may seem like a very simple language. It has pretty straight-forward syntax, easy-to-use standard objects, APIs and all that kind of stuff. That's why it's so "beginner-friendly", right? Well, utilizing JS at the beginner level can be compared to something like seeing only the top of the iceberg. When you'll finally start using it for some more serious work and projects, you'll quickly notice that not everything is as easy as it may seem. JS is a very versatile language - it allows its user to utilize multiple programming concepts and constructs in the same piece of code, thus bringing it to the next level. That's also a reason why it provides multiple ways of achieving the same result. So, in today's post, we're going to explore one of JavaScript's most versatile functionalities - function declaration!

More than two there are...

Do you know how many ways are there to define a function in JS? Well, considering the addition of arrow functions in ES6, it's not 2 or 3, but as many as 4! And I'm not even counting the ES6 generator functions or object method shortcut! Interesting, huh?

Now, in today's article, we'll explore each of these 4 approaches in-depth, together with their similarities and differences. Believe me - even though all these methods lead to the same or quite a similar result (depending on your point of view), they certainly differ in syntax and with some other exclusive properties. Anyway, we'll cover all of that!

Function declaration

Let's start our analysis with something that's very well-known and stable - the function keyword and the most basic syntax that's been in JS from its very beginnings:

function example(param) {
	// ...
}

The syntax above is something that you've surely seen before. It's usually referenced to as a function declaration - a very broad term, yet mostly associated with the syntax above. Functions created in such a way, beyond being usual functions, have some interesting properties...

Hoisting

Consider the code below:

example();
function example(param) {
	// ...
}

See what I did there? I called our supposed function before even defining it. Surprisingly, the code snippet can be executed without any errors. That's an example of exclusive property of function declaration called hoisting. It's related to the way that JS engines process code - first they analyze function declarations and then everything else. It's a nice trick, however, it's considered an anti-pattern - code that uses this feature extensively can get quite messy pretty quickly.

Arguments object

What other interesting functionalities do function declarations possess? Well, inside each function's body declared in such a way, you get access to the arguments array-like object. As the name indicates, it provides you with the list of arguments passed to the function call. It's similar to an array in a way that you can access its elements (function's arguments) by their respective indices. Apart from that, it also has the length property indicating the number of passed arguments. Any other properties and methods that you come to expect from a usual array (like .push(), and etc.) aren't present.

function example(param) {
	console.log(
		arguments.length,
		arguments[0],
		arguments[1]
	);
}
example("param1", "param2"); // 2 "param1" "param2"

arguments list doesn't limit its length to the number of parameters specified in the function definition. Because of that, before ES6, it was the only way to achieve an effect similar to what rest parameters can do today.

// ES5
function example(param) {
	// Convert arguments to a usual array.
	const argsArray = Array.prototype.slice.call(arguments);
	// Retrieve the rest parameters by cutting off the listed params.
	const restParams = argsArray.slice(1);
	// ...
}

// ES6
function example(param, ...restParams) {
	// ...
}

Lastly, arguments object contains the callee property, which holds the reference to the called function. It can be used for recursion, but it's not recommended. The sole fact that this feature isn't available in Strict Mode, should speak for itself. Unless you've got some really important reason, it'll be better for you to simply reference the function by its name (especially if you're using function declaration).

function example(param) {
	if(param) {
		arguments.callee(); // or simply example();
	}
}

This property

The last important feature of the function declaration is the value of this. JavaScript's this has been the subject of many controversies and misinformation. No one can be really sure of the current value of this is. Sadly, the same can be said about function declarations. Here, the value of this is determined by the time the function is called - aka runtime binding. Putting this theory into practice, let's analyze the code snippet below.

function example() {
	console.log(this);
}
example(); // Window
example.bind({example: true})(); // {example: true}

We inspect the value of this only to discover that it's equal to the global Window object. That's intended, as our function is defined in the very upper scope of our code.

The value of this cannot be assigned or changed in any way. If we want to get a function with a different value of this, we have to use the .bind() method. It allows us to set the value of this in the newly-created copy of the base function. The returned function can be used just like the input one, with the only difference being the value of this.

Now, there's a lot of stuff happening behind the scenes with this and .bind(). Because our goal is to simply analyze the most significant differences between each method for defining a function, we won't be going into all that. Just remember that functions defined using function declarations have the value of this determined at runtime.

Function expression

Aside from the basic function declaration, functions can also be defined with function expressions.

const example = function(param) {
	// ...
}
example();

In the snippet above, we're assigning our function expression to a variable to be able to call it later. Now, the only difference between a function expression and a function declaration is hoisting. Function expressions cannot be called before they're defined in the code. Any such attempt will result in an error.

example(); // ReferenceError
example2(); // ok
const example = function() {
	// ...
}
function example2() {
	// ...
}

In real-world JS code, function expressions are usually preferred over function declarations. The lack of hoisting makes calling such functions before their definitions impossible, while at the same time brute-forcing order into the code.

Named function expression

Apart from assigning them to variables, function expression can also be used as object methods or IIFEs (Immediately Invoked Function Expression).

// IIFE
(function() {
	// ...
})()
// Object method
const object = {
	example: function() {
		// ...
	}
}

Sadly, the lack of hoisting can result in recursion being a bit harder to pull off. Sometimes, the function might be impossible to reference (e.g. inside an IIFE). In this case, the arguments.callee might be an option, but we know it's not the best practice. Instead, we can use what's called a named function expression.

(function example(param) {
	if(param) {
		example();
	}
})(true);

As you can see, named function expressions even closer resemble function declarations. That's because the only real difference between them indeed comes down to hoisting.

Arrow function

Arrow functions - one of the most popular features of ES6 - has certainly gained a lot of traction among web developers. They mainly serve as a syntactic sugar and a shorter alternative to usual function expressions.

const example = () => {
	// ...
}
example();

But, naturally, aside from their nice syntax, arrow functions have some very interesting features when compared to usual function expressions. First and foremost, the arrow functions feature a lexically-bonded value of this. What it means is that arrow functions don't have their own this and instead rely upon the value from the enclosing scope. I think it can be explained more clearly with a piece of code.

const example = {
	property: "value",
	funcExpr: function() {
		console.log(this); // {property: "value", /* ... */}
	},
	arrowFunc: () => {
		console.log(this); // Window
	}
}

Such behavior makes arrow functions less unpredictable that their older counterparts, but it also excludes them from the roles of constructors (impossible) and methods (not suited).

The lack of own value of this also doesn't allow arrow functions to use the .bind() method. They also don't have the arguments object, but, as they're part of ES6 (together with the rest parameters), it shouldn't be a problem.

Function constructor

Finally, we got to the last item on this list. The way of defining functions that I guarantee you won't see very often.

You know that in JS there's a rule that "everything is an object", right? Well, this applies to functions too! Furthermore, they even have their own constructor - Function. And it also allows you to define a function.

const example = new Function("param", "console.log(param)");
example();

As you can see, the use of the Function constructor closely resembles the eval() function. And, as you might already know, it's not a function that experienced programmers would recommend you to use - it has poor performance, it's not secure, etc. But, while the Function constructor shares similar syntax and weaknesses, it does so to a smaller extent. With that said you should stay away from evaluating any string of code as far as possible. And, when you'll really have no other choice, always favor the use of the Function constructor over the eval() function.

If the syntax feels weird to you, here's some explanation. The whole expression is basically just the Function constructor call. The arguments you have to provide are string-type-only. Almost all the arguments are used as parameter names, while the last one is used for a function's body. Inside it, you can reference all previously listed parameter names and any other pieces of JS code, that you'd normally use inside a "usual" function.

Functions defined using the Function constructor share its functionalities with function expressions, i.e. no hoisting, the runtime-bounded value of this, the .bind() method and the arguments object.

Summary

So, there's definitely a lot of ways to define a function in JS. Ranging from the most functional and widely-used ones to those that rarely see the light of day. Let me know in the comment section below if you knew of all of these methods - especially the last one. ;)

If you like this post, consider following me on Twitter, Facebook, or through my weekly newsletter, to stay up-to-date with my latest content. As always, thank you very much for reading this piece and have a nice day!