# Functions
# Introduction
We often want to be able to re-execute blocks of code when we are writing programs, without having to re-write the block of code entirely. We need a way of grouping code together and giving it a name, so that we can call it by that name later, and that's what we call a function.
# Function Declaration
The classic way to define functions in JavaScript is through functions declarations. This is declared with the function
keyword followed by the function name and parentheses. It may or may not have parameters. Next, between braces, we will have the set of instructions and optionally the return
keyword and the return value.
function doSomething() {
return "Doing Something";
}
We could put any code inside that function - one statement, multiple statements - depends on what we want to do. Now, if all we do is declare the function, nothing will happen. In order for the program to execute the code that's inside the function, we actually have to "call" the function, by writing its name followed by empty parentheses:
doSomething(); // "Doing Something"
# Function Expression
A function expression has a similar syntax to a function declaration, with the exception that we assign the function to a variable:
const doSomething = function() {
return "Doing Something";
}
doSomething() // "Doing Something"
# Arrow Functions
With the appearance of ES6, the arrow functions syntax was added to the language, a way to define functions much more readable and concise.
/* Arrow function */
const doSomething = () => "Doingsomething";
// return is implicit if we not add the braces.
Arrow functions are ideal for declaring lambda expressions (opens new window) (inline functions), since they reduce syntax noise and improve the expressiveness and intentionality of the code.
/* Lambda Expressions */
//Without arrow functions
[1,2,3].map(function(n){ return n*2 });
// With arrow functions
[1,2,3].map((n) => n*2);
# How this
object works in arrow functions
Another interesting feature of arrow functions is that they change the default behavior of the this
object in JavaScript. When we create an arrow function, its value of this is permanently associated with the value of this
of its immediate outer scope; window in the case of the global scope of the browser or global in the case of the global scope of NodeJS:
const isWindow = () => this === window;
isWindow(); //true
In the case of being in the local scope of a method, the value of this
would be the value of the scope of the function. Let's see an example:
const counter = {
number: 0,
increase() {
setInterval(() => ++this.number, 1000);
}
};
counter.increase(); // 1 2 3 4 5
Inside the arrow function, the value of this
is the same as in the func()
. Although this may seem like the expected behavior, it wouldn't be the case if we didn't use arrow functions. Let's look at the same example using a function created with the function keyword:
const counter = {
number: 0,
increase() {
setInterval(function(){ ++this.number }, 1000);
}
};
counter.increase(); // NaN NaN NaN...
Although NaN (Not a Number) is not the intuitive result, it makes sense in JavaScript, since inside setInterval()
this has lost the reference to the counter object. Before the advent of arrow functions, this problem with callbacks used to be corrected by making a copy of the this object:
const counter = {
number: 0,
increase() {
const that = this;
setInterval(function(){ ++that.number }, 1000);
}
};
counter.increase(); // 1 2 3...
Or, by "binding" the this
object, using the bind
function:
const counter = {
number: 0,
increase() {
setInterval(function(){ ++this.number }.bind(this), 1000);
}
};
counter.increase(); // 1 2 3...
In this example, it is shown that both solutions generate a lot of noise, so using the arrow functions in callbacks in which this is used becomes essential.
# Immediately-invoked Function Expression (IIFE)
An IIFE (Immediately Invoked Function Expression) is a JavaScript function that runs as soon as it is defined.
(function () {
statements
})();
It is a design pattern which is also known as a Self-Executing Anonymous Function and contains two major parts:
- The first is the anonymous function with lexical scope enclosed within the Grouping Operator (). This prevents accessing variables within the IIFE idiom as well as polluting the global scope.
- The second part creates the immediately invoked function expression () through which the JavaScript engine will directly interpret the function.
The function becomes a function expression which is immediately executed. The variable within the expression can not be accessed from outside it.
(function () {
var aName = "Barry";
})();
// Variable aName is not accessible from the outside scope
aName // throws "Uncaught ReferenceError: aName is not defined"
Assigning the IIFE to a variable stores the function's return value, not the function definition itself.
var result = (function () {
var name = "Barry";
return name;
})();
// Immediately creates the output:
result; // "Barry"
# Parameters and Arguments
The arguments are the values with which we call the functions, while the parameters are the named variables that receive these values within our function:
// creating the function
const double = (x) => x*2; // "x" is the parameter of our function
// calling the function
double(2); // 2 is the argument with which we call our function
We often want to be able to customize functions, to tell the program "well, do all of this code, but change a few things about how you do it." That way we have code that is both reusable and flexible, the best of both worlds. We can achieve that by specifying "arguments" for a function, using those arguments to change how the function works, and passing them in when we call the function.
# Limit the number of arguments
An important recommendation is limit the number of arguments that a function receives. In general, we should limit ourselves to a maximum of three parameters. In the case of having to exceed this number, it might be a good idea to add one more level of indirection through an object:
function createMenu(title, body, buttonText, cancellable) {
// ...
}
function createMenu({ title, body, buttonText, cancellable }) {
// ...
}
createMenu({
title: "Foo",
body: "Bar",
buttonText: "Baz",
cancellable: true
})
# Default Parameters
Since ES6, JavaScript allows function parameters to be initialized with default values.
// ES6
function greet(text = "world") {
console.log("Hello " + text);
}
greet(); // without parameter. Hello world
greet(undefined) // undefined. Hello world
greet("Foo"); // with parameter. Hello Foo
In classic JavaScript, to do something as simple as this we had to check if the value is undefined and assign it the desired default value:
// Before ES6
function greet(text) {
if (typeof text === "string") {
text = "world";
}
console.log("Hello" + text);
}
Although we should not abuse the default parameters, this syntax can help us to be more concise in some contexts.
# rest parameter and spread operator
The operator ...
is known as the rest parameter or as the operator spread, depending on where it is used.
The rest parameter unifies the remaining arguments in a function call when the number of arguments exceeds the number of parameters declared in it.
In a way, the rest parameter acts in the opposite way to spread, while spread "expands" the elements of a given array or object, rest unifies a set of elements in an array.
function add(x, y) {
return x + y;
}
add(1, 2, 3, 4, 5) // 3
function add(...args) {
return args.reduce((previous, current) => previous + current, 0);
}
add(1, 2, 3, 4, 5) // 15
// The rest parameter is the last parameter of the function and, as we have mentioned, it is an array:
function process(x, y, ...rest) {
console.log(rest);
}
process(1, 2, 3, 4, 5) // [3, 4, 5]
Like the default parameters, this feature was introduced in ES6. In order to access the additional arguments in classic JavaScript, we have the arguments
object:
function process(x, y) {
console.log(arguments);
}
process(1, 2, 3, 4, 5) // [1, 2, 3, 4, 5]
The arguments object has some problems. The first of these is that, although it looks like an array, it is not, and therefore does not implement the functions of array.prototype
. Also, unlike rest, it can be overwritten and does not contain the remaining arguments, but all of them. For this reason its use is usually discouraged.
On the other hand, the spread operator splits an object or an array into multiple individual elements. This allows you to expand expressions in situations where multiple values are expected such as in function calls or in array literals:
function doStuff(x, y, z) { }
const args = [0, 1, 2];
// with spread
doStuff(...args);
// without spread
doStuff.apply(null, args);
// spread in Math functions
const numbers = [9, 4, 7, 1];
Math.min(...numbers); // 1
Spread also allows us to clone objects and arrays in a very simple and expressive way:
const post = { title: "spread operator", content: "lorem ipsum..." };
// clone with Object.assign();
const postCloned = Object.assign({}, post);
// clone with spread operator
const postCloned = { ...post };
const myArray = [1, 2, 3];
// clone with slice
const myArrayCloned = myArray.slice();
// clone with spread operator
const myArrayCloned = [...myArray];
# Indentation size and levels
Simplicity is a fundamental pillar when it comes to writing good code and, therefore, one of the key recommendations is that our functions be small.
If your functions, as a general rule, tend to be too large or have too many levels of indentation, they are likely to do too much. This leads us to another recommendation, perhaps the most important: functions should do one thing and do it well. Another fundamental point to keep our functions simple is to try to limit the indentation levels to 1 or 2 levels. To do this we must avoid nesting conditionals and loops. This will allow us to keep the "spaghetti code (opens new window)" at bay as well as reducing the cyclomatic complexity (opens new window) of the function.
Let's see an example of how our functions should NOT be:
const getPayAmount = () => {
let result;
if(isDead) {
result = deadAmount();
} else {
if(isSeparated) {
result = separatedAmount();
} else {
if(isRetired) {
result = retiredAmount();
} else {
result = normalPayAmount();
}
}
}
return result;
}