Functions are one of the most basic features of JavaScript. Have you ever wondered how exactly do they work? After all, they are just a special kind of objects. If you are curious, dig into them with me in this article!
The basics of functions
Speaking practically, a function is a subprogram that can be called in another place of the code. It has a function body which is a sequence of statements. It can have values passed to it as arguments. The function can also return a value. In JavaScript, they are objects inheriting from Function.prototype. If you would like to know more about prototypes themselves, look into one of my previous articles: Prototype. The big bro behind ES6 class. Check out this piece of code:
1 2 |
function fn() {}; Function.prototype.isPrototypeOf(fn); // true |
A default return value of a function is undefined. It is a little different when you use the new keyword though because then the default value is the instance of the object created (the one that this points to in the function).
1 2 3 4 5 6 |
function fn(){ // not doing anything } console.log(fn()); // undefined console.log(new fn()); // {} |
Passing arguments to functions
Arguments passed to functions are always passed by value. It means, that if the function changes the value of an argument, it won’t be reflected outside of the scope of a function. It applies to both primitives (like strings) and objects.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
function fn(str, obj) { str = '!dlroW olleH'; obj = { str: '!dlroW olleH' }; } const str = 'Hello World!'; const obj = { str: 'Hello World!' }; fn(str, obj); console.log(str); // 'Hello World!'; console.log(obj.str); // 'Hello World!'; |
You might have read before, that objects are passed by reference in JavaScript. If you look closely at the example above, you can see that we can’t overwrite an obj variable from inside of the function, which would have happened in a “pass by reference” situation. This common misconception arises from a fact, that when we assign an object to a variable, the actual thing that is stored is a reference to the object (to a place in memory that it is stored in). The variables themselves don’t contain the actual value of the object. The function is given just a copy of that reference, that is in fact passed by value to it – not a reference to a variable outside. Thanks to that behaviour, we can mutate the objects that we pass to our functions, but we can’t overwrite the variable:
1 2 3 4 5 6 7 8 9 10 |
function fn(obj) { obj.str = '!dlroW olleH'; } const obj = { str: 'Hello World!' }; fn(obj); console.log(obj.str); // '!dlroW olleH'; |
If you are still not convinced of my point of view on always passing all variables by values, check out this issue on You Don’t Know JS repo or a discussion on Stack Overflow.
Ways to create a function
There are actually quite a few ways to create a function. They generally fall into one of a few groups:
Function declaration
It uses a special syntax for functions declaration with the function keyword followed by an obligatory name of the function. Its greatest advantage is that it is hoisted to the top of the scope, which means that it can be invoked before its declaration:
1 2 3 4 5 |
console.log(square(2)); // 4 function square(number){ return number * number; } |
The created function is named, therefore the function object holds its name. It might be useful when viewing the call stack in the debugging process.
1 |
console.log(square.name); // 'square' |
Function expression
It looks similar to function declaration but can be a part of a larger expression. The main difference compared to the function declaration is that a function name can be omitted here. Functions created with a function expression won’t be hoisted to the top, therefore they can’t be invoked before their declaration:
1 2 3 4 5 |
console.log(square(2)); // Uncaught ReferenceError: square is not defined const square = function square(number) { return number*number; } |
Anonymous functions
Since we can omit the function name, it can cause them to be “anonymous”. This behaviour was changed in the ES6 though. Now, they are given a name from their syntactic position:
1 2 3 4 5 6 7 8 9 10 11 |
const square = function (number) { return number * number; } const obj = { square: function(number) { return number * number; } } console.log(square.name); // "square" console.log(obj.square.name); // "square" |
In previous versions of JavaScript, it would be an empty string. If you look at the babel compiler, it even changes the anonymous functions to the named ones. The more important name for the JavaScript interpreter would be the one explicitly provided:
1 2 3 4 5 |
const squareVariable = function square(number) { return number * number; } console.log(squareVariable.name); // "square" |
If you look closer right into the property descriptor, the name of a function can’t be overwritten:
1 |
Object.getOwnPropertyDescriptor(squareVariable, 'name'); |
1 2 3 4 5 6 |
{ value: "square", writable: false, enumerable: false, configurable: true } |
Since it is configurable, you can define it using Object.defineProperty function,:
1 2 3 4 5 6 7 8 9 10 11 12 |
function square(number) { return number * number; } console.log(square.name); // 'square' square.name = 'square2'; console.log(square.name); // 'square' Object.defineProperty(square, 'name', { value: 'name2' }); console.log(square.name); // 'square2' |
Function constructor
Calling the function constructor creates a new object inheriting from the Function.prototype. You can create a function dynamically this way, but it is less efficient than using function expression, or the function statement. Functions created this way are named “anonymous“:
1 2 3 4 |
const square = new Function('number', 'return number * number'); square(2); // 4 console.log(square.name); // "anonymous" |
In this constructor, the last argument is always the function body and the previous ones are the arguments.
Another important thing about creating functions with the function constructor is the fact that functions created this way do not create closures to their creation contexts. They are always created in the global scope and will only be able to access their own local variables and the global ones.
1 2 3 4 5 6 7 |
(function(){ const number = 2; function square() { return number * number; }; square(); // 4 })(); |
1 2 3 4 5 |
(function(){ const number = 2; const square = new Function('return number * number'); square(); // Uncaught ReferenceError: number is not defined })(); |
Arrow functions
Another important concept connected to the way that functions are created are arrow functions. You can read about them in one of my previous articles: What is “this”? Arrow functions.
Arguments object
As said before, functions can have arguments passed to them. In the function scope, you have an object called arguments containing all of them.
1 2 3 4 5 6 7 8 |
function getArguments(number) { return arguments; } const args = getArguments(2); console.log(args instanceof Array); // false console.log(args[0]); // 2 |
As you can see, it is not exactly an array. You can create one with it though:
1 |
const args = Array.from(getArguments(2)); |
1 |
const args = [...getArguments(2)]; |
It is possible because arguments are iterable:
1 2 |
const arguments = getArguments(); console.log(typeof arguments[Symbol.iterator]); // function |
You can pass any amount of arguments to a function and just use some of them in the function declaration. The arguments object will hold all of them though:
1 2 3 4 5 6 |
function divide(a, b){ console.log([...arguments]); return a / b; } divide(10, 5, 4, 3, 2, 1); |
1 |
[10, 5, 4, 3, 2, 1] |
In the past we used a property of a function, also called arguments, but this is now deprecated:
1 2 3 4 5 6 7 8 |
function factorial(n){ console.log(factorial.arguments[0]); if(n === 1 || n === 0){ return 1; } return factorial(n - 1) * n; } console.log(factorial(5)); // 3! = 6 |
1 2 3 |
[3] [2] [1] |
Summary
And this is all for today! I hope that you learned a lot today and gained a solid understanding of what are functions in JavaScript. Since functions are such a broad topic, there will be more on them in the future: for example about closures. Take care!
Great article. For the first example it is better to use let instead of const.