Default Parameters

"use strict";

const bookings = [];
const createBooking = function(flightNum, numPassengers = 1, price = 199 * numPassengers) {
    /* // before ES6 we'd set default parameters like this : 
    numPassengers = numPassengers || 1;
    price = price || 199; */

    const booking = {
        flightNum,
        numPassengers,
        price
    }
    console.log(booking); 
    bookings.push(booking);
} 
createBooking('LH123') // Prints: { flightNum: 'LH123', numPassengers: undefined, price: undefined } //when default parameters aren't set
// we can now use undefined which is a falsey value to our advantage


createBooking('LH123', 2); // Prints: { flightNum: 'LH123', numPassengers: 2, price: 398 }
console.log(bookings);
// Prints: 
/* [
    { flightNum: 'LH123', numPassengers: 1, price: 199 },
    { flightNum: 'LH123', numPassengers: 2, price: 398 }
] */

createBooking('LH123', undefined, 123); // Prints: { flightNum: 'LH123', numPassengers: 1, price: 123 }

Value vs. Reference

"use strict";

const flight = 'LH234';
const jonas = {
    name: 'Jonas Schmedtmann',
    passport: 24739123123,
}

const checkIn = function(flightNum, passenger)
{
    flightNum = 'LH999';
    passenger.name = 'Mr. ' + passenger.name;

    if (passenger.passport === 24739123123)
        console.log('Checked in');
    else
        console.log('Wrong passport');
}

checkIn(flight, jonas);
console.log(flight); // Prints: LH234 // remains same as it was passed by value
console.log(jonas); // Prints: { name: 'Mr. Jonas Schmedtmann', passport: 24739123123 } // changed as it was passed by reference even though we changed the name to passenger when we had passed it originally.
  • note that Javascript does not have passing by reference and only has passing by value
    (i used the term pass by reference in a comment in the code above to make things sound easier)
  • JS only has pass by value.
  • We did see that we do in fact pass in a reference i.e. memory address of the object to the function. However that reference itself is still a value it's simply a value that contains a memory address.
  • Basically we pass a reference to the function but we do not pass by reference.

First-Class and Higher-Order Functions

Pasted image 20231220180708.png

  • JS has First-Class functions that enables us to write Higher-Order functions. But what does this mean?
  • What does JS having 'First-Class Functions' mean?
  • It just means that functions are a different 'type' of object in JS. Since objects are values, functions are values too. And since functions are values, they can be stored in variables or even object properties (keys in an object). We have already been doing this so far.
  • We can also pass functions as args to other functions. We already did that with addEvenListener where we passed in the callback function as an argument .
  • We can also return a function from another function.
  • Finally, since functions are objects, and objects can have methods on them, there can also be function methods. bind method is an example of that.
  • What is a Higher-Order Function then?
  • A Higher-Order function is either a function that receives a function as an argument, or returns a new function, or both.
  • the addEventListener is an example of a higher-order function because it accepts another function as an arg.
  • Similarly we can have functions that return other functions.

Functions accepting Callback Functions

"use strict";

const oneWord = function(str) {
    return str.replace(/ /g, '').toLowerCase();
};

const upperFirstWord = function (str) {
    const [first, ...others] = str.split(' ');
    return [first.toUpperCase(), ...others].join(' ');
};

// Higher-order function
const transformer = function (str, fn){
    console.log(`Original string ${str}`);
    console.log(`Transformed string ${fn(str)}`);
    console.log(`Transformed by ${fn.name}`);
}

transformer('Javascript is the best!', upperFirstWord); //notice that we have only passed the variable name here
// Prints: 
/* Original string Javascript is the best!
Transformed string JAVASCRIPT is the best!
Transformed by upperFirstWord */

// so here we are calling the transformer function here and into that function we are passing the callback function
// so, upperFirstWord is the callback function
// basically we aren't the one calling the callback fxn upperFirstWord here. The transformer fxn is the one which will call that fxn later.

transformer('Javascript is the best!', oneWord);
// Prints: 
/* Original string Javascript is the best!
Transformed string javascriptisthebest!
Transformed by oneWord */
// transformer here is the higher order function

Functions returning functions :

"use strict";

const greet = function(greeting){
    return function(name) {
        console.log(`${greeting} ${name}`);
    }
}

const greeterHey = greet('Hey'); // here the 'Hey' is passed in 'greeting' and then the function(name) is returned to greeterHey.

greeterHey('Jonas'); // Prints: Hey Jonas
 
// we can do it one line as well : 
greet('Hello')('Jonas'); // this works because `greet('Hello')` returns a function

// writing the same code using arrow functions : 
const greet2 = greeting => name => console.log(`${greeting} ${name}`);
greet2('Hello')('Jonas');

The call, apply and bind methods :

"use strict";

const lufthansa = {
    airline: 'Lufthansa',
    iataCode: 'LH',
    bookings: [],
    // `book: function() {}` // OR :
    book(flightNum, name) {
        // The 'this' keyword will point to the object which called this method
        console.log(
            `${name} booked a seat on flight ${this.airline}  ${this.iataCode} ${flightNum}`
        );
        this.bookings.push({flight: `${this.iataCode} ${flightNum}`, name})
    },
};
  
lufthansa.book(239, 'hitarth'); // Prints: hitarth booked a seat on flight Lufthansa  LH 239
console.log(lufthansa);

const eurowings = {
    airline: 'Eurowings',
    iataCode: 'EW',
    bookings: [],
};

/* ------------------------------------------------------ */
const book = lufthansa.book;

// book(23, 'Sarah Williams'); // Prints: error
// we are getting this error because this book function is no longer a method but it is now a function instead. So, since it's a regular function call therefore 'this' keyword will point to undefined.
// Hence, the 'this' keyword depends on how the function is actually called 

/* ----------------- to fix this problem ---------------- */
// now you'll have to tell JS where the 'this' keyword should point to. For e.g. if you want to make a booking in lufthansa or eurowings then the 'this' keyword should point to lufthansa and eurowings objects respectively.

// there are three methods of doing this : 

/* ---------------------- 1.) Call ---------------------- */

// so we are setting the this keyword to eurowings here
book.call(eurowings, 23, 'Sarah Williams'); // Prints: Sarah Williams booked a seat on flight eurowings  EW 23
console.log(eurowings); //bookings will have : bookings: [ { flight: 'EW 23', name: 'Sarah Williams' } ]

book.call(lufthansa, 23, 'Mary Cooper'); // Prints: Mary Cooper booked a seat on flight Lufthansa  LH 23
console.log(lufthansa); 
/* bookings: [
    { flight: 'LH 239', name: 'hitarth' },
    { flight: 'LH 23', name: 'Mary Cooper' }
  ], */


/* ------------------- 2.) Bind Method ------------------ */
// the bind method allows us to set the this keyword for any function call

// The difference is that, unlike the call and apply methods, bind does not immediately call the function. Instead it returns a NEW function where the this keyword is already bound.

// This is especially useful when we have to manually assign the this keyword in the case of event listeners. Recall for event listeners the callback function should be a function, and it should not be immediately called. call and apply immediately call the function. Hence we use the bind method instead.


// this will not call the book function instead it will return a new function where the this keyword will always be set to eurowings
const bookEW = book.bind(eurowings);
const bookLH = book.bind(lufthansa);

bookEW(23, 'Steven Williams'); // Prints: Steven Williams booked a seat on flight Eurowings  EW 23
 
// we can also set the arguments in stone:
const bookEW2 = book.bind(eurowings, 23); // here the flightNum will now always be equalt to 23 // now this function only needs a name
bookEW2('dev');

// bind method is used very often with event Listeners
lufthansa.planes = 300;
lufthansa.buyPlane = function () {
    this.planes++;
    console.log(this); // Prints: <button class="buy">Buy new plane 🛩</button>
    console.log(this.planes); // Prints: NaN
}

// document.querySelector('.buy').addEventListener('click', lufthansa.buyPlane); 

// we previously learned that the this keyword always points to the element on which that handler is attached to. lufthansa.buyPlane here is the handler function so the this keyword points to the button element

// if we had simply done 'lufthansa.buyPlane();' then the this keyword would point to lufthansa object

// we still need the this keyword to point towards the lufthansa object, so we'll have to manually define the this keyword here, we need to pass in a function here for the 'click' event and not call a function and we know that 'call method' calls the function so we'll use the Bind Method here

// document.querySelector('.buy').addEventListener('click', lufthansa.buyPlane.bind(lufthansa)); // this here now will point to lufthansa object and it // Prints: 301

// Partial Application : 

// remember that partial application means that we can preset parameters
const addTax = (rate, value) => value + value*rate;
console.log(addTax(0.1, 200)); // Prints: 220 

// now if want to preset the rate always : 
const addVAT = addTax.bind(null, 0.23); // here in (rate, value) the rate value is set now
console.log(addVAT(100)); // Prints: 123

Immediately Invoked Function Expressions (IIFE):

  • Sometimes in JS, we need a function that is only executed once, and never executed again. We need this to implement something known as aync await.
"use strict";

const runOnce = function () {
    console.log(('This will never run again'));
}

runOnce();

// function () {
//     console.log(('This will never run again'));
// } 
// Prints: this will give an error as we haven't set a name for the function yet.

// we can trick javascript into thinking that this function is just an expression by enclosing the entire thing parentheses

(function () {
    console.log(('This will never run again'));
    const isPrivate = 23;
})();

// note that the '()' before semi colon ';' is used to call the funciton immediately  


// ()  => console.log('This will also never run again'); // Prints: error

(() => console.log('This will also never run again'))();

/* ------------------------------------------------------ */

// The privateVariable declared above will not be accessible over here:
// console.log(privateVariable); // Uncaught ReferenceError: privateVariable is not defined
// And hence, this pattern can be used to perform encapsulation

// If your concern is just protecting scopes, then you do not need to follow the IFFE pattern.
// You can just do this:
{
    const oneName = 'Ein name';
    let twoName = 'Du name';
    var threeName = 'Three name'; // this will still be accessible outside the scope
}

Closures :

  • closure is the combination of a function and the lexical environment within which that function was declared. This environment consists of any local variables that were in-scope at the time the closure was created.
  • Any function always has access to the variable environment of the execution context in which the function was created, even after that execution context is gone.
  • Pasted image 20231221141748.png
"use strict";

const secureBooking = function () {
    let passengerCount = 0;

    return function() {
        passengerCount++;
        console.log(`${passengerCount} passengers`);
    }
}

const booker = secureBooking();
booker(); // Prints: 1 passengers
booker(); // Prints: 2 passengers
booker(); // Prints: 3 passengers

// basically the booker function has access to the passengerCount variable because it's basically defined in the scope in which the booker function was actually created

Pasted image 20231221151727.png

  • We can access the closure of a variable by doing:
// We can log the variables stored in the closure like this:
console.dir(booker);

Pasted image 20231221151933.png

  • This Closure here is basically the variable environment of this secure booking.

More examples of closure :

"use strict";

/* ---------------------- example 1 --------------------- */

let f;

const g = function () {
    const a = 23;
    f = function () {
        console.log(a*2);
    };
};


const h = function () {
    const b = 777;
    f = function () {
        console.log(b*2);
    };
};


g(); // hence the 'a' variable is inside the backpack of f function
f(); // Prints: 46
console.dir(f); // closure was 'a' here

// re assigning f function
h();
f(); // Prints: 1554 // f here is now a different function compared to the f() above

console.dir(f); // closure was 'b' here

/* ------------------------------------------------------ */

/* ---------------------- example 2 --------------------- */
const boardPassengers = function (n, wait) {
    const perGroup = n / 3;

    setTimeout(function () {
        console.log(`We are now boarding all ${n} passengers`);
        console.log(`There are 3 groups, each with ${perGroup} passengers`);
    }, wait*1000); //1000ms i.e 1s
    

    console.log(`Will start boarding in ${wait} seconds`); // this console.log will not wait for the above setTimeout function to finish printing
};

const perGroup = 1000; // notice that this variable wasn't used when boardPassengers(180, 3); was executed, hence we can conclude that closure variable has priority // if we remove the closure perGroup variable then the fxn will use this variable instead
boardPassengers(180, 3);

/* 
Will start boarding in 3 seconds
We are now boarding all 180 passengers
There are 3 groups, each with 60 passengers 
*/


// notice that the setTimeout fxn was executed completely independently from the board passengers function but still the callback fxn was able to use all the variables that were in the variable environment in which it was created.