JavaScript Course 6: Functions
References
- Code with Mosh: The Ultimate JavaScript Mastery Series – Part 1
- W3School: JavaScript Tutorial
- W3School: JavaScript Functions
- W3School: JavaScript Hoisting
- W3School: JavaScript Function Closures
- W3School: JavaScript Object Accessors – Getters and Setters
- Geeks for Geeks: JavaScript | Immediately Invoked Function Expressions (IIFE)
- LinkedIn: JavaScript Named vs Anonymous Functions
Read also:
- JavaScript Basics: Functions
- JavaScript Arrays: Arrow Functions
- JavaScript Objects: Constructor Functions
- JavaScript Objects: Factory functions
Basics
JavaScript Functions are sets of statements (like a block of code) that perform a task or calculates a value. The Functions could have Inputs, they could use (multiple) parameters, but it is not mandatory.
function greet(name) { // 'name' is an input parameter of the function
console.log('Hello ' + name);
}
greet('Spas'); // 'Spas' is an argument which become a value of the param. 'name'
Hello Spas
Function Declarations vs Expressions
Functions are one of the building blocks of any programming language and JavaScript has taken the Functions to a whole new level. Functions are said to be a collection of statements to be executed in a proper sequence in a particular context.
JavaScript provides a variety of methods to define and execute Functions, there are Named Functions, Anonymous Functions and Functions that are executed as soon as they are mounted, these functions are known as Immediately Invoked Function Expressions or IIFEs.
Function declaration
The first thing you should know about function declarations is that they are hoisted. The function name cannot be changed once declared since it is loaded into the memory. In ES2015 and later, functions inside blocks are scoped to that block.
function walk() {
console.log('Walk');
}
Function expression
JavaScript's the functions are objects. So setting a variable to a function is similar to set it to an Object. Note, with function expression we need to put semicolon ;
at the end of the statement, in contrast with function declaration, by a convention, we do need do that.
Function expressions deal with the act of defining a function inside an expression and they are not hoisted. It is essentially while creating a function directly in function arguments like a callback or assigning it to a variable.
The function expressions can be named or anonymous. Named functions are useful for a good debugging experience, while anonymous functions provides context scoping for easier development.
? Arrow functions should only be used when functions act as data.
Function Expressions also do not have access to their constructor's name since it is anonymous, it will return the string ‘anonymous’ instead.
The arrow functions – () => {}
– are an ES2015 only syntax that lexically binds its this
value.
Named Function expression
let run = function walk() {
console.log('Run');
};
Anonymous Function expression
let run = function() {
console.log('Run');
};
run(); // just how we call a function in JavaScript
Run
We can declare another variable (called move
in the example below) which refers to the same function (object in the memory).
let move = run;
move();
Run
Immediately Invokable Function Expression (IIFEs)
Also called Self-invoking Functions or Function Closures: JavaScript variables can belong to the local or global scope. Global variables can be made local (private) with closures.
The Immediately Invokable Functions can be named and anonymous, but even if an IIFE does have a name it is impossible to refer/invoke it, so this is useful only for debugging. They could be written as declaration or as expression.
// IIFE (Immediately Invokable Function Expression)
(function() {
console.log('lumos'); // lumos
})();
IIFEs can also have parameters.
// Declaring the parameter required.
(function(dt) {
console.log(dt.toLocaleTimeString());
// Passing the Parameter.
})(new Date());
4:30:12 PM
Nested Functions and Closures
const add = (function() {
let counter = 0;
return function() {counter += 1; return counter};
})();
add(); add(); add(); // the counter is now 3
Example Explained:
- The variable
add
is assigned to the return value of a self-invoking function. - The self-invoking function only runs once. It sets the counter to zero (0), and returns a function expression.
- This way add becomes a function. The "wonderful" part is that it can access the counter in the parent scope.
- This is called a JavaScript closure. It makes it possible for a function to have "private" variables.
- The counter is protected by the scope of the anonymous function, and can only be changed using the add function.
Arguments and Parameters
Within the function's declaration or expression a
and b
(of the following example) are called Parameters.
function sum(a, b) {
return a + b;
}
When we call the function and pass values to its parameters, these values are called Arguments.
console.log(sum(1, 2));
3
In JavaScript have a special object, called arguments
, that holds all arguments pass to the function.
function sum(a, b) {
console.log(arguments);
}
sum(1, 2);
Arguments(2) [1, 2, callee: ƒ, Symbol(Symbol.iterator): ƒ]
0: 1
1: 2
callee: ƒ sum(a, b)
length: 2
Symbol(Symbol.iterator): ƒ values()
[[Prototype]]: Object
function sumArguments() {
let total = 0;
for (let argument of arguments)
total += argument;
return total;
}
console.log(sumArguments(1, 2, 3, 4, 5, 10));
25
Rest Operator
In modern JS, if you want to have a function with varying number of parameters, you can use the Rest operator: ...args
. It looks like exactly to the Spread operator used with the Arrays and Objects, but don't confuse them.
The Rest operator creates an array of the Arguments that are not associated to a function Parameters.
function sum(...args) {
console.log(args);
}
sum(1, 2, 3, 4, 5, 10); // will return a real array.
(6) [1, 2, 3, 4, 5, 10]
In contrast if we do not use the Rest operator the above function will return only the first argument.
function sum(args) {
console.log(args);
}
sum(1, 2, 3, 4, 5, 10);
1
Another example where we use Named Parameters and the Rest operator. Note, it is not possible to have more parameters after the Rest operator.
function sum(first, second, ...rest) {
console.log(first, second, rest);
}
sum(1, 2, 3, 4, 5, 10);
1 2 (4) [3, 4, 5, 10]
So, when we apply the Rest operator to a parameter of a function, we can pass very number of arguments and the Rest operator will take all of them and put them in an array.
Now, if you want to get the sum of all the numbers in an array, we can use the .reduce()
method and the Rest operator, instead of looping over the arguments
object as it was done in the above section.
function sumRest(...args) {
return args.reduce((a, b) => a + b );
}
console.log(sumRest(1, 2, 3, 4, 5, 10));
25
So you see, in modern JavaScript we can achieve the same functionality with less code. Instead of defining a total variable, setting it to zero and then looping over the arguments object… we can have one line of code that gives us the same result, and this is more elegant and more professional.
Default Parameters
In order to define default values for a function's Parameters we can use the logical or operator in the following way.
function interest(principal, rate, years) {
rate = rate || 3.5; // if the parameter 'rate' is 'undifined' which is falsy value the value 3.5 will be used
years = years || 5; // if the parameter 'years' is 'undifined' which is falsy value the value 5 will be used
return principal * rate / 100 * years;
}
In ES6, there is more elegant and clear way to define default values.
function interest(principal, rate = 3.5, years = 5) {
return principal * rate / 100 * years;
}
console.log(interest(1000));
175
It is more correct to place the parameters with default values at the end of the parameter's list. But there is one confusing way gow to use the default value to one parameter and pass a custom value to the next.
console.log(interest(1000, undefined, 10)); // the 'rate' will be used with its default value '3.5'
350
Another example for the same trick., where the years
parameter even doesn't have a default value. Note, in such cases case is more correct to move the parameters with default values (in this case rate
parameter) at the end of the list in order to avoid such confusing solution.
function interest(principal, rate = 3.5, years) {
return principal * rate / 100 * years;
}
console.log(interest(1000, undefined, 5)); // the 'rate' will be used with its default value '3.5'
175
Getter and Setters
Getters and setters allow you to define Object Accessors (Computed Properties). These are special methods of JavaScript Objects, which allows to threat methods as propertioes. We use getters to access the properties of an an Object, and use setters to change or mutate them.
const person = {
firstName: "Spas",
lastName: "Spasov",
get fullName() {
return `${this.firstName} ${this.lastName}`;
},
set fullName(value) { // 'value' is the value that will be assigned by this method
const parts = value.split(' '); // the split() method will return an array...
this.firstName = parts[0];
this.lastName = parts[1];
}
};
person.fullName = 'John Smith'; // Use the Setter function
console.log(person.fullName); // Use the Getter function
John Smith
console.log(person);
{firstName: 'John', lastName: 'Smith'}
firstName: "John"
lastName: "Smith"
fullName: (...)
get fullName: ƒ fullName()
set fullName: ƒ fullName(value)
[[Prototype]]: Object
Error handling – Try and Catch
In the above examle we assume the value
that will be passed is a valid string of two parts, separated by a whitespase, but what will happen if we pass a boolean, or somethin other?
const person = {
firstName: "Spas",
lastName: "Spasov",
get fullName() {
return `${this.firstName} ${this.lastName}`;
},
set fullName(value) {
if (typeof value !== 'string')
throw new Error('Value is not a string.'); // Error1
const parts = value.split(' ');
if (parts.length !== 2)
throw new Error('Enter First Name and Last name.'); // Error2
this.firstName = parts[0];
this.lastName = parts[1];
}
};
try {
person.fullName = null;
}
catch (e) { // here 'e' is the JS error object thrown by the setter function - Error 1
console.log(e); // or provide it as a message within the user interface
}
Error: Value is not a string.
at Object.set fullName [as fullName] (<anonymous>:9:19)
at <anonymous>:2:21
try {
person.fullName = '';
}
catch (e) { // here 'e' is the JS error object thrown by the setter function - Error 2
console.log(e); // or provide it as a message within the user interface
}
Error: Enter First Name and Last name.
at Object.set fullName [as fullName] (<anonymous>:14:19)
at <anonymous>:2:21
Local vs Global Scope
Read also:
Scope of a variable or constant determinates where that variable or constant is accessible. When we declare variables or constants with let
or const
their scope is limited to the block in which they are defined.
{
const message = 'hi';
console.log(message);
}
hi
{
const message = 'hi';
}
console.log(message);
Uncaught ReferenceError: message is not defined
at <anonymous>:4:13
function start() {
const message = 'hi';
if (true) {
const another = 'bye';
}
console.log(another);
}
start();
Uncaught ReferenceError: another is not defined
at start (<anonymous>:8:17)
at <anonymous>:11:1
function start() {
for (let i = 0; i < 2; i++)
console.log(i);
console.log(i);
}
start();
0
1
Uncaught ReferenceError: i is not defined
at start (<anonymous>:5:17)
at <anonymous>:7:1
We can have different variables with the same name within different scope – different function as it is in the following example.
function start() {
const message = 'hi';
}
function stop() {
const message = 'bye';
}
When we define a variable or constant outside any code block that variable or constant has global scope and can be accessed by all code blocks as functions, loops, etc. Global means the variable is accessible everywhere – globally.
const color = 'red';
function paint() {
console.log(color);
}
paint();
red
When we have a constant or variable with exact same name at the global and a local scope – the local scope takes precedence over the global level.
const color = 'red';
function paint() {
const color = 'blue';
console.log(color);
}
paint();
blue
Defining global variables or constants is considered bad practice! We should avoid that when it is possible.
Let vs Var
Read also:
- JavaScript Basics: Variables and Constants
- JavaScript Basics: Var vs Let and Const (issues with Var)
Issues with Var #1: Var Creates Function Scope
The first issue with var
is that, it creates function scope, while let
(and const
) creates block scope. So the variables created by var
can be accessed outside of the scope where they are defined.
function start() {
for (let i = 0; i < 2; i++)
console.log(i);
console.log(i);
}
start();
0
1
Uncaught ReferenceError: i is not defined
at start (<anonymous>:5:17)
at <anonymous>:1:1
function start() {
for (var i = 0; i < 2; i++)
console.log(i);
console.log(i);
}
start(); // In the output below you can see the 'var i' is not terminated when the 'for(...){...}'' block is finished
0
1
2
Another example how var
is accessible from outside the block where it is defined.
function start() {
for (let i = 0; i < 5; i++) {
if (true) {
var color = 'red';
}
}
console.log(color);
}
start();
red
If we have used let color = 'red'
the above code will throw an error, but when we use var color = 'red'
we can access the variable inside the whole function not only within the if(…){…}
scope.
Issues with Var #2: Variables are Attached to the Window
The second issue with var
is related to the Global and the Local Scope – the variables, defined by var
are attached to the window
object (used by the browsers at the frontend).
var firstName = 'John';
let lastName = 'Smith';
console.log(window.firstName, window.lastName);
John undefined
Local vs Global Scope of Functions
The functions defined by the function
keyword are technically global functions, attached to the Window Object. This is actually bad practice – to prevent this behavior we need to use modules in order to encapsulate the functions in these modules and won't be attached to the Window Object.
function sayHi() {
console.log('hi');
}
window.sayHi();
hi
This Keyword in JavaScript
The this
keyword references the Object that is executing the current Function. For example if a function is method of an Object this
references to that Object itself. Otherwise if that function is a regular function
(which means it is not part of certain Object) it references to the global object which is the Window Object in the bowsers and Global Object in Nod.js.
Examples for Methods. Which references to that Object itself.
const video = {
title: 'a',
play() {
console.log(this);
}
};
video.play();
{title: 'a', play: ƒ}
video.stop = function() {
console.log(this);
};
video.stop();
{title: 'a', play: ƒ, stop: ƒ}
Examples for Regular Functions. Which references to the global object which is the Window Object in the bowsers and Global Object in Nod.js.
function playVideo() {
console.log(this);
}
playVideo();
Window {window: Window, self: Window, document: document, name: '', location: Location, …}
Example for Constructor Functions. Note the Constructor Functions are used with the new
operator, that creates a new empty object and set this
from the constructor function to point to this new empty object.
function Video(title) {
this.title = title;
console.log(this);
}
const video = new Video('b');
Video {title: 'b'}
Example for Callback Function. The Callback Functions are regular functions despite they can be called inside of a method of an object. Exception of this rule are the Arrow Function and they will be described within the next section.
const video = {
title: 'Title',
tags: ['a', 'b', 'c'],
showTags() {
this.tags.forEach(function(tag) {
console.log(this.title, tag);
});
}
};
video.showTags();
undefined 'a'
undefined 'b'
undefined 'c'
We can see this.title
returns undefined
because the callback function refers to the global Window Object.
const video = {
title: 'Title',
tags: ['a', 'b', 'c'],
showTags() {
this.tags.forEach(function(tag) {
console.log(this);
});
}
};
video.showTags();
Window {window: Window, self: Window, document: document, name: '', location: Location, …}
Window {window: Window, self: Window, document: document, name: '', location: Location, …}
Window {window: Window, self: Window, document: document, name: '', location: Location, …}
A solution for this particular case is to pass this as argument to the second parameter to the .forEach(calback_fn(){…}, thisArg)
method.
const video = {
title: 'Title',
tags: ['a', 'b', 'c'],
showTags() {
this.tags.forEach(function(tag) {
console.log(this.title, tag);
}, this);
}
};
video.showTags();
Title a
Title b
Title c
Within the .forEach()
method thisArg
could be this
that refers to the parent object which runs the .showTags()
method, but it could be any other object.
const video = {
title: 'Title',
tags: ['a', 'b', 'c'],
showTags() {
this.tags.forEach(function(tag) {
console.log(this.title, tag);
}, {title: 'SuperCoolMovie'});
}
};
video.showTags();
SuperCoolMovie a
SuperCoolMovie b
SuperCoolMovie c
Changing this
As we said: this
references the Object that is executing the current Function. Here are listed few different solutions to change the value of this
(the reference Object) in a function. The first solution is available when is used a built-in method which provides this functionality – as it is shown in the above section. Let's imagine the .forEach()
method from the above example doesn't provide this functionality.
Get reference of this
by a variable and use it later. It is a valid but not recommended approach.
const video = {
title: 'Title',
tags: ['a', 'b', 'c'],
showTags() {
const self = this;
this.tags.forEach(function(tag) {
console.log(self.title, tag);
});
}
};
video.showTags();
Title a
Title b
Title c
Use the built-in methods .apply()
, .call()
or .bund()
– remember in JavaScript the functions are objects and there are few built-in methods for them. By default the functions are attached to the Window (or Global) Object – as we saw above.
function playVideo() {
console.log(this);
}
playVideo();
Window {window: Window, self: Window, document: document, name: '', location: Location, …}
Use the .call(thisArg
, 'a', 'b')
method – it is the simplest methods.
playVideo.call({name: 'Spas'});
{name: 'Spas'}
Use the .apply(thisArg
, ['a', 'b'])
method – its usage is similar to the above, the difference is how the next arguments are passed.
playVideo.apply({name: 'Spas'});
{name: 'Spas'}
The difference between .call()
and .apply()
is only about the way passing arguments. Let's assume the function has multiple parameters like a and b, we can supply them in the following way.
function playVideo(a, b) {
console.log(this, a, b);
}
{name: 'Spas'} 1 2
{name: 'Spas'} 1 2
Use the .bind()
method – it doesn't call the function, instead it creates (returns) a new function and sets its this
to point the new (passed) Object permanently. We can store the result in a constant and that constant can be used just like a regular function.
const fn = playVideo.bind({name: 'Spas'});
fn(1, 2);
{name: 'Spas'} 1 2
We can immediately call the function that is returned from the .bind()
method.
playVideo.bind({name: 'Spas'})(1, 2);
{name: 'Spas'} 1 2
Use the .bind()
method within callback function – in order to solve the problem from the beginning of the section.
const video = {
title: 'Title',
tags: ['a', 'b', 'c'],
showTags() {
this.tags.forEach(function(tag) {
console.log(this.title, tag);
}.bind(this));
}
};
video.showTags();
Title a
Title b
Title c
Using the Arrow Functions in order to solve such problems (with the callback functions) is a newer and better solution. The Arrow Functions inherit the this
value from the containing function.
const video = {
title: 'Title',
tags: ['a', 'b', 'c'],
showTags() {
this.tags.forEach(tag => console.log(this.title, tag));
}
};
video.showTags();
Title a
Title b
Title c