JavaScript Course 6: Functions

From WikiMLT

Ref­er­ences

Read al­so:

Ba­sics

JavaScript Func­tions are sets of state­ments (like a block of code) that per­form a task or cal­cu­lates a val­ue. The Func­tions could have In­puts, they could use (mul­ti­ple) pa­ra­me­ters, but it is not manda­to­ry.

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

Func­tion De­c­la­ra­tions vs Ex­pres­sions

Func­tions are one of the build­ing blocks of any pro­gram­ming lan­guage and JavaScript has tak­en the Func­tions to a whole new lev­el. Func­tions are said to be a col­lec­tion of state­ments to be ex­e­cut­ed in a prop­er se­quence in a par­tic­u­lar con­text.

JavaScript pro­vides a va­ri­ety of meth­ods to de­fine and ex­e­cute Func­tions, there are Named Func­tions, Anony­mous Func­tions and Func­tions that are ex­e­cut­ed as soon as they are mount­ed, these func­tions are known as Im­me­di­ate­ly In­voked Func­tion Ex­pres­sions or IIFEs.

Func­tion de­c­la­ra­tion

The first thing you should know about func­tion de­c­la­ra­tions is that they are hoist­ed. The func­tion name can­not be changed once de­clared since it is loaded in­to the mem­o­ry. In ES2015 and lat­er, func­tions in­side blocks are scoped to that block.

function walk() {
    console.log('Walk');
}

Func­tion ex­pres­sion

JavaScript's the func­tions are ob­jects. So set­ting a vari­able to a func­tion is sim­i­lar to set it to an Ob­ject. Note, with func­tion ex­pres­sion we need to put semi­colon ; at the end of the state­ment, in con­trast with func­tion de­c­la­ra­tion, by a con­ven­tion, we do need do that.

Func­tion ex­pres­sions deal with the act of defin­ing a func­tion in­side an ex­pres­sion and they are not hoist­ed. It is es­sen­tial­ly while cre­at­ing a func­tion di­rect­ly in func­tion ar­gu­ments like a call­back or as­sign­ing it to a vari­able.

The func­tion ex­pres­sions can be named or anony­mous. Named func­tions are use­ful for a good de­bug­ging ex­pe­ri­ence, while anony­mous func­tions pro­vides con­text scop­ing for eas­i­er de­vel­op­ment.

? Ar­row func­tions should on­ly be used when func­tions act as da­ta.

Func­tion Ex­pres­sions al­so do not have ac­cess to their constructor's name since it is anony­mous, it will re­turn the string ‘anony­mous’ in­stead.

The ar­row func­tions – () => {} – are an ES2015 on­ly syn­tax that lex­i­cal­ly binds its this val­ue.

Named Func­tion ex­pres­sion

let run = function walk() {
    console.log('Run');
};

Anony­mous Func­tion ex­pres­sion

let run = function() {
    console.log('Run');
};
run(); // just how we call a function in JavaScript
Run

We can de­clare an­oth­er vari­able (called move in the ex­am­ple be­low) which refers to the same func­tion (ob­ject in the mem­o­ry).

let move = run; 
move();
Run

Im­me­di­ate­ly In­vok­able Func­tion Ex­pres­sion (IIFEs)

Al­so called Self-in­vok­ing Func­tions or Func­tion Clo­sures: JavaScript vari­ables can be­long to the lo­cal or glob­al scope. Glob­al vari­ables can be made lo­cal (pri­vate) with clo­sures.

The Im­me­di­ate­ly In­vok­able Func­tions can be named and anony­mous, but even if an IIFE does have a name it is im­pos­si­ble to refer/​​​invoke it, so this is use­ful on­ly for de­bug­ging. They could be writ­ten as de­c­la­ra­tion or as ex­pres­sion.

// IIFE (Immediately Invokable Function Expression)
(function() {
  console.log('lumos'); // lumos
})();

IIFEs can al­so have pa­ra­me­ters.

// Declaring the parameter required.
(function(dt) {
    console.log(dt.toLocaleTimeString());
    // Passing the Parameter.
})(new Date());
4:30:12 PM

Nest­ed Func­tions and Clo­sures

const add = (function() {
  let counter = 0;
  return function() {counter += 1; return counter};
})();
add(); add(); add(); // the counter is now 3

Ex­am­ple Ex­plained:

  • The vari­able add is as­signed to the re­turn val­ue of a self-in­vok­ing func­tion.
  • The self-in­vok­ing func­tion on­ly runs once. It sets the counter to ze­ro (0), and re­turns a func­tion ex­pres­sion.
  • This way add be­comes a func­tion. The "won­der­ful" part is that it can ac­cess the counter in the par­ent scope.
  • This is called a JavaScript clo­sure. It makes it pos­si­ble for a func­tion to have "pri­vate" vari­ables.
  • The counter is pro­tect­ed by the scope of the anony­mous func­tion, and can on­ly be changed us­ing the add func­tion.

Ar­gu­ments and Pa­ra­me­ters

With­in the function's de­c­la­ra­tion or ex­pres­sion a and b (of the fol­low­ing ex­am­ple) are called Pa­ra­me­ters.

function sum(a, b) {
    return a + b;
}

When we call the func­tion and pass val­ues to its pa­ra­me­ters, these val­ues are called Ar­gu­ments.

console.log(sum(1, 2));
3

In JavaScript have a spe­cial ob­ject, called ar­gu­ments, that holds all ar­gu­ments pass to the func­tion.

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 Op­er­a­tor

In mod­ern JS, if you want to have a func­tion with vary­ing num­ber of pa­ra­me­ters, you can use the Rest op­er­a­tor: ...args. It looks like ex­act­ly to the Spread op­er­a­tor used with the Ar­rays and Ob­jects, but don't con­fuse them.

The Rest op­er­a­tor cre­ates an ar­ray of the Ar­gu­ments that are not as­so­ci­at­ed to a func­tion Pa­ra­me­ters.

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 con­trast if we do not use the Rest op­er­a­tor the above func­tion will re­turn on­ly the first ar­gu­ment.

function sum(args) {
    console.log(args);
}
sum(1, 2, 3, 4, 5, 10);
1

An­oth­er ex­am­ple where we use Named Pa­ra­me­ters and the Rest op­er­a­tor. Note, it is not pos­si­ble to have more pa­ra­me­ters af­ter the Rest op­er­a­tor.

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 ap­ply the Rest op­er­a­tor to a pa­ra­me­ter of a func­tion, we can pass very num­ber of ar­gu­ments and the Rest op­er­a­tor will take all of them and put them in an ar­ray.

Now, if you want to get the sum of all the num­bers in an ar­ray, we can use the .re­duce() method and the Rest op­er­a­tor, in­stead of loop­ing over the ar­gu­ments ob­ject as it was done in the above sec­tion.

function sumRest(...args) {
    return args.reduce((a, b) => a + b );
}
console.log(sumRest(1, 2, 3, 4, 5, 10));
25

So you see, in mod­ern JavaScript we can achieve the same func­tion­al­i­ty with less code. In­stead of defin­ing a to­tal vari­able, set­ting it to ze­ro and then loop­ing over the ar­gu­ments ob­ject… we can have one line of code that gives us the same re­sult, and this is more el­e­gant and more pro­fes­sion­al.

De­fault Pa­ra­me­ters

In or­der to de­fine de­fault val­ues for a function's Pa­ra­me­ters we can use the log­i­cal or op­er­a­tor in the fol­low­ing 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 el­e­gant and clear way to de­fine de­fault val­ues.

function interest(principal, rate = 3.5, years = 5) {
    return principal * rate / 100 * years;
}
console.log(interest(1000));
175

It is more cor­rect to place the pa­ra­me­ters with de­fault val­ues at the end of the parameter's list. But there is one con­fus­ing way gow to use the de­fault val­ue to one pa­ra­me­ter and pass a cus­tom val­ue to the next.

console.log(interest(1000, undefined, 10)); // the 'rate' will be used with its default value '3.5'
350

An­oth­er ex­am­ple for the same trick., where the years pa­ra­me­ter even doesn't have a de­fault val­ue. Note, in such cas­es case is more cor­rect to move the pa­ra­me­ters with de­fault val­ues (in this case rate pa­ra­me­ter) at the end of the list in or­der to avoid such con­fus­ing so­lu­tion.

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

Get­ter and Set­ters

Get­ters and set­ters al­low you to de­fine Ob­ject Ac­ces­sors (Com­put­ed Prop­er­ties). These are spe­cial meth­ods of JavaScript Ob­jects, which al­lows to threat meth­ods as prop­er­tioes. We use get­ters to ac­cess the prop­er­ties of an an Ob­ject, and use set­ters to change or mu­tate 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

Er­ror han­dling – Try and Catch

In the above ex­am­le we as­sume the val­ue that will be passed is a valid string of two parts, sep­a­rat­ed by a whites­pase, but what will hap­pen if we pass a boolean, or some­thin oth­er?

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

Lo­cal vs Glob­al Scope

Read al­so:

Scope of a vari­able or con­stant de­ter­mi­nates where that vari­able or con­stant is ac­ces­si­ble. When we de­clare vari­ables or con­stants with let or con­st their scope is lim­it­ed to the block in which they are de­fined.

{
    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 dif­fer­ent vari­ables with the same name with­in dif­fer­ent scope – dif­fer­ent func­tion as it is in the fol­low­ing ex­am­ple.

function start() {
    const message = 'hi';
}

function stop() {
    const message = 'bye';
}

When we de­fine a vari­able or con­stant out­side any code block that vari­able or con­stant has glob­al scope and can be ac­cessed by all code blocks as func­tions, loops, etc. Glob­al means the vari­able is ac­ces­si­ble every­where – glob­al­ly.

const color = 'red';

function paint() { 
    console.log(color);
}

paint();
red

When we have a con­stant or vari­able with ex­act same name at the glob­al and a lo­cal scope – the lo­cal scope takes prece­dence over the glob­al lev­el.

const color = 'red';

function paint() {
    const color = 'blue';
    console.log(color);
}

paint();
blue

Defin­ing glob­al vari­ables or con­stants is con­sid­ered bad prac­tice! We should avoid that when it is pos­si­ble.

Let vs Var

Read al­so:

Is­sues with Var #1: Var Cre­ates Func­tion Scope

The first is­sue with var is that, it cre­ates func­tion scope, while let (and con­st) cre­ates block scope. So the vari­ables cre­at­ed by var can be ac­cessed out­side of the scope where they are de­fined.

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

An­oth­er ex­am­ple how var is ac­ces­si­ble from out­side the block where it is de­fined.

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 er­ror, but when we use var color = 'red' we can ac­cess the vari­able in­side the whole func­tion not on­ly with­in the if(…){…} scope.

Is­sues with Var #2: Vari­ables are At­tached to the Win­dow

The sec­ond is­sue with var is re­lat­ed to the Glob­al and the Lo­cal Scope – the vari­ables, de­fined by var are at­tached to the win­dow ob­ject (used by the browsers at the fron­tend).

var firstName = 'John';
let lastName = 'Smith';
console.log(window.firstName, window.lastName);
John undefined

Lo­cal vs Glob­al Scope of Func­tions

The func­tions de­fined by the func­tion key­word are tech­ni­cal­ly glob­al func­tions, at­tached to the Win­dow Ob­ject. This is ac­tu­al­ly bad prac­tice – to pre­vent this be­hav­ior we need to use mod­ules in or­der to en­cap­su­late the func­tions in these mod­ules and won't be at­tached to the Win­dow Ob­ject.

function sayHi() {
    console.log('hi');
}
window.sayHi();
hi

This Key­word in JavaScript

The this key­word ref­er­ences the Ob­ject that is ex­e­cut­ing the cur­rent Func­tion. For ex­am­ple if a func­tion is method of an Ob­ject this ref­er­ences to that Ob­ject it­self. Oth­er­wise if that func­tion is a reg­u­lar func­tion (which means it is not part of cer­tain Ob­ject) it ref­er­ences to the glob­al ob­ject which is the Win­dow Ob­ject in the bowsers and Glob­al Ob­ject in Nod.js.

Ex­am­ples for Meth­ods. Which ref­er­ences to that Ob­ject it­self.

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: ƒ}

Ex­am­ples for Reg­u­lar Func­tions. Which ref­er­ences to the glob­al ob­ject which is the Win­dow Ob­ject in the bowsers and Glob­al Ob­ject in Nod.js.

function playVideo() {
    console.log(this);
}
playVideo();
Window {window: Window, self: Window, document: document, name: '', location: Location, …}

Ex­am­ple for Con­struc­tor Func­tions. Note the Con­struc­tor Func­tions are used with the new op­er­a­tor, that cre­ates a new emp­ty ob­ject and set this from the con­struc­tor func­tion to point to this new emp­ty ob­ject.

function Video(title) {
    this.title = title;
    console.log(this);
}
const video = new Video('b');
Video {title: 'b'}

Ex­am­ple for Call­back Func­tion. The Call­back Func­tions are reg­u­lar func­tions de­spite they can be called in­side of a method of an ob­ject. Ex­cep­tion of this rule are the Ar­row Func­tion and they will be de­scribed with­in the next sec­tion.

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 re­turns un­de­fined be­cause the call­back func­tion refers to the glob­al Win­dow Ob­ject.

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 so­lu­tion for this par­tic­u­lar case is to pass this as ar­gu­ment to the sec­ond pa­ra­me­ter to the .forE­ach(calback_​​​fn(){…}, this­Arg) 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

With­in the .forE­ach() method this­Arg could be this that refers to the par­ent ob­ject which runs the .show­Tags() method, but it could be any oth­er ob­ject.

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

Chang­ing this

As we said: this ref­er­ences the Ob­ject that is ex­e­cut­ing the cur­rent Func­tion. Here are list­ed few dif­fer­ent so­lu­tions to change the val­ue of this (the ref­er­ence Ob­ject) in a func­tion. The first so­lu­tion is avail­able when is used a built-in method which pro­vides this func­tion­al­i­ty – as it is shown in the above sec­tion. Let's imag­ine the .forEach() method from the above ex­am­ple doesn't pro­vide this func­tion­al­i­ty.

Get ref­er­ence of this by a vari­able and use it lat­er. It is a valid but not rec­om­mend­ed ap­proach.

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 meth­ods .apply(), .call() or .bund() – re­mem­ber in JavaScript the func­tions are ob­jects and there are few built-in meth­ods for them. By de­fault the func­tions are at­tached to the Win­dow (or Glob­al) Ob­ject – 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 sim­plest meth­ods.

playVideo.call({name: 'Spas'});
{name: 'Spas'}

Use the .apply(thisArg, ['a', 'b']) method – its us­age is sim­i­lar to the above, the dif­fer­ence is how the next ar­gu­ments are passed.

playVideo.apply({name: 'Spas'});
{name: 'Spas'}

The dif­fer­ence be­tween .call() and .apply() is on­ly about the way pass­ing ar­gu­ments. Let's as­sume the func­tion has mul­ti­ple pa­ra­me­ters like a and b, we can sup­ply them in the fol­low­ing 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 func­tion, in­stead it cre­ates (re­turns) a new func­tion and sets its this to point the new (passed) Ob­ject per­ma­nent­ly. We can store the re­sult in a con­stant and that con­stant can be used just like a reg­u­lar func­tion.

const fn = playVideo.bind({name: 'Spas'});
fn(1, 2);
{name: 'Spas'} 1 2

We can im­me­di­ate­ly call the func­tion that is re­turned from the .bind() method.

playVideo.bind({name: 'Spas'})(1, 2);
{name: 'Spas'} 1 2

Use the .bind() method with­in call­back func­tion – in or­der to solve the prob­lem from the be­gin­ning of the sec­tion.

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

Us­ing the Ar­row Func­tions in or­der to solve such prob­lems (with the call­back func­tions) is a new­er and bet­ter so­lu­tion. The Ar­row Func­tions in­her­it the this val­ue from the con­tain­ing func­tion.

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