Functions


First-class functions


With first-class functions you can do just about anything that you can do with other elements, such as numbers, strings, and arrays. C-like programming languages do not support first-class functions. However, scripting languages like Python and JavaScript have built-in support.

Characteristics of first-class functions:

  1. Can be stored in variables
  2. Can be returned from a function
  3. Can be passed as arguments into another function

Functions are objects


In JavaScript functions can be treated as objects. A key difference between a function and an object is that functions can be called (i.e., invoked with ()), while regular objects cannot.

The following example is a proof that functions are indeed objects:

function average(n1, n2, n3) {
    return (n1 + n2 + n3) / 3;
}

average.length; // 3
average.name; // 'average'

Higher-order functions


A function that returns another function or takes other functions as arguments is known as higher-order function.

function foo() {
    alert('Hello 1');
    return function() {
        alert('Hello 2');
    };
}

foo();
// Alerts 'Hello 1'

const innerFunction = foo();

innerFunction();
// Alerts 'Hello 2'

foo()();
// Alerts 'Hello 1' followed by 'Hello 2'

function hoo() {
    alert('Hello 1');
    function print() {
        alert('Hello 2');
    };
    return print();
}

hoo();
// Alerts 'Hello 1' followed by 'Hello 2'

Callback functions


Introduction

A function that is passed as an argument into another function is called a callback function. Callbacks are possible in JS because functions are first-class functions.

// Higher-order function
function foo(n, callback) {
    return n + callback();
}

// Callback function
function hoo() {
    return 2;
}

foo(5, hoo); // 7

'forEach()' method

The forEach() method executes a provided function once for each array element.

// Array
const numArray = [1,2,3,4,5,6,7,8,9];

// Callback
function logIfEven(n) {
    if(n % 2 === 0) {
        console.log(n);
    }
}

// Method #1: forEach() invokes logIfEven() for each element in the array
numArray.forEach(function logIfEven(n) {
    if(n % 2 === 0) {
        console.log(n);
    }
});

//2
//4
//6
//8

// Method #2: forEach() invokes logIfEven() for each element in the array
numArray.forEach(logIfEven);

//2
//4
//6
//8

'map()' method

The map() method creates a new array with the results of calling a provided function on every element in the calling array. The method returns a new array; it does not modify the original array.

// Array
const numArray = [1,2,3,4,5,6,7,8,9];

// Callback
function logIfEven(n) {
    if(n % 2 === 0) {
        return n;
    }
}

// Method #1: map() invokes logIfEven() for each element in the array
const evenNumbers1 = numArray.map(function logIfEven(n) {
    if(n % 2 === 0) {
        return n;
    }
});

console.log(evenNumbers1);

// [2,4,6,8]

// Method #2: map() invokes logIfEven() for each element in the array
const evenNumbers2 = numArray.map(logIfEven);

console.log(evenNumbers2);

// [2,4,6,8]
/* Using map()
 *
 * Using the musicData array and map():
 *   - Return a string for each item in the array in the following format:
 *     <album-name> by <artist> sold <sales> copies
 *   - Store the returned data in a new albumSalesStrings variable
 *
 * Note:
 *   - Do not delete the musicData variable
 *   - Do not alter any of the musicData content
 *   - Do not format the sales number; leave it as a long string of digits
 */

const musicData = [
    { artist: 'Adele', name: '25', sales: 1731000 },
    { artist: 'Drake', name: 'Views', sales: 1608000 },
    { artist: 'Beyonce', name: 'Lemonade', sales: 1554000 },
    { artist: 'Chris Stapleton', name: 'Traveller', sales: 1085000 },
    { artist: 'Pentatonix', name: 'A Pentatonix Christmas', sales: 904000 },
    { artist: 'Original Broadway Cast Recording',
      name: 'Hamilton: An American Musical', sales: 820000 },
    { artist: 'Twenty One Pilots', name: 'Blurryface', sales: 738000 },
    { artist: 'Prince', name: 'The Very Best of Prince', sales: 668000 },
    { artist: 'Rihanna', name: 'Anti', sales: 603000 },
    { artist: 'Justin Bieber', name: 'Purpose', sales: 554000 }
];

const albumSalesStrings = musicData.map(function(item) {
    return item.name + ' by ' + item.artist + ' sold ' + item.sales + ' copies';
});

console.log(albumSalesStrings);

// ['25 by Adele sold 1731000 copies',
//  'Views by Drake sold 1608000 copies',
//  'Lemonade by Beyonce sold 1554000 copies',
//  'Traveller by Chris Stapleton sold 1085000 copies',
//  'A Pentatonix Christmas by Pentatonix sold 904000 copies',
//  'Hamilton: An American Musical by Original Broadway Cast Recording sold 820000 copies',
//  'Blurryface by Twenty One Pilots sold 738000 copies',
//  'The Very Best of Prince by Prince sold 668000 copies',
//  'Anti by Rihanna sold 603000 copies',
//  'Purpose by Justin Bieber sold 554000 copies']

'filter()' method

The filter() method is similar to the map() method. The difference is that the callback function is used as a test, and only items in the array that pass the test are included in the new array.

const numArray = [1,2,3,4,5,6,7,8,9];

// filter() invokes the anonymous function for each element in the array
const evenNumbers = numArray.filter(function(n) {
    // Test if n is even. If n it's even, then n % 2 === 0 returns true and n
    // is included in the new array
    return n % 2 === 0;
});

console.log(evenNumbers);

// [2,4,6,8]
/* Using filter()
 *
 * Using the musicData array and filter():
 *   - Return only album objects where the album's name is
 *     10 characters long, 25 characters long, or anywhere in between
 *   - Store the returned data in a new `results` variable
 *
 * Note:
 *   - Do not delete the musicData variable
 *   - Do not alter any of the musicData content
 */

const musicData = [
    { artist: 'Adele', name: '25', sales: 1731000 },
    { artist: 'Drake', name: 'Views', sales: 1608000 },
    { artist: 'Beyonce', name: 'Lemonade', sales: 1554000 },
    { artist: 'Chris Stapleton', name: 'Traveller', sales: 1085000 },
    { artist: 'Pentatonix', name: 'A Pentatonix Christmas', sales: 904000 },
    { artist: 'Original Broadway Cast Recording',
      name: 'Hamilton: An American Musical', sales: 820000 },
    { artist: 'Twenty One Pilots', name: 'Blurryface', sales: 738000 },
    { artist: 'Prince', name: 'The Very Best of Prince', sales: 668000 },
    { artist: 'Rihanna', name: 'Anti', sales: 603000 },
    { artist: 'Justin Bieber', name: 'Purpose', sales: 554000 }
];

const results = musicData.filter(function(item) {
    return ((item.name.length >= 10) && (item.name.length <= 25));
});

console.log(results);

//[{artist: 'Pentatonix', name: 'A Pentatonix Christmas', sales: 904000},
// {artist: 'Twenty One Pilots', name: 'Blurryface', sales: 738000},
// {artist: 'Prince', name: 'The Very Best of Prince', sales: 668000}]

Function scope


Runtime scope

Runtime scope refers to the context of the function, or more specifically, the set of variables available for the function to use.

A functions has access to the following:

  1. Function arguments
  2. Local variables
  3. Global variables
  4. Variables from its parent function's scope

Consider the child() function below:

// Global variable
    var global = 10;
    function parent() {
        // Variable from the parent scope
        let parentLocal = 20;
        return function child() {
            // Local variable
            let childLocal = 30;
            console.log(global + parentLocal + childLocal);
        }
    }
    parent()();

    // 60

The child() function has access to global, parentLocal, and childLocal variables.

Variable scope

Example 1

/*
var globalScoped = 100; // Available everywhere

function foo() {

    var functionScoped = 0; // Available inside function foo() ONLY

    for (let i = 0; i < 10; i++) {

        let blockScoped = 2; // Available inside for loop ONLY

        functionScoped += 1;

        console.log('functionScoped:' + functionScoped + ' ' + blockScoped:' + blockScoped);
        // functionScoped:1 blockScoped:2
        // functionScoped:2 blockScoped:2
        // functionScoped:3 blockScoped:2
        // functionScoped:4 blockScoped:2
        // functionScoped:5 blockScoped:2
        // functionScoped:6 lockScoped:2
        // functionScoped:7 blockScoped:2
        // functionScoped:8 blockScoped:2
        // functionScoped:9 blockScoped:2
        // functionScoped:10 blockScoped:2
    }

    console.log('blockScoped:' + blockScoped);
    // Uncaught ReferenceError: blockScoped is not defined

    globalScoped += 1;

    console.log('globalScoped:' + globalScoped);
    // globalScoped:101

}

console.log(globalScoped);
// 101

console.log(functionScoped);
// Uncaught ReferenceError: functionScoped is not defined

console.log(blockScoped);
// Uncaught ReferenceError: blockScoped is not defined
*/

Example 2

var has functional scope while const and let have block scope.

const, and let only remain in memory while the for loop is running and are not longer accessible once the for loop finishes.

//----------------- Code 1 -----------------
for (var x = 0; x < 10; x++) {}
console.log(x);
// 10

//----------------- Code 2 -----------------
var x;
for (x = 0; x < 10; x++) {}
console.log(x);
// 10

//----------------- Code 3 -----------------

for (let y = 0; y < 10; y++) {}
console.log(y);
// Uncaught ReferenceError: y is not defined

Example 3

The JavaScript interpreter will always start off by looking within its own local variables. If the variable isn't found, the search will continue looking up what is called the scope chain.

function one() {
    two();
    function two() {
        three();
        function three() {
            // Function three's code here
        }
    }
}

one();

three() will not only have access to the variables and functions 'above' it (i.e., those of two() and one()) - three() will also have access to any global variables defined outside one().

We can visualize the scope chain as:

  1. three() <----- innermost scope
  2. two()
  3. one()
  4. window (global object) <----- outermost scope

Generally, the order that the JS interpreter will search for variables along the scope chain is:

  1. Local variables
  2. Parent function's variables
  3. Parent function's parent function's variables
  4. Global variables

Example 4

What happens when you create a variable with the same name as another variable somewhere in the scope chain?

JavaScript won't throw an error or otherwise prevent you from creating that extra variable. In fact, the variable with local scope will just temporarily 'shadow' the variable in the outer scope. This is called variable shadowing.

const symbol = '¥';

function displayPrice(price) {
    const symbol = '$';
    console.log(symbol + price);
}

displayPrice('80');
// '$80'

Since the variable pointing to '$' is declared inside a function (i.e., the 'inner' scope), it will override any variables of the same name that belong in an outer scope -- such as the global variable pointing to '¥''. As a result, '$80' is displayed rather than '¥80'.

Example 5

When the following code runs, what is the output of the first, second, and third logs to the console (respectively)?

let n = 8;

function functionOne() {
    let n = 9;

    function functionTwo() {
        let n = 10;
        console.log(n);  // First log
    }

    functionTwo();

    console.log(n);  // Second log
}

functionOne();

console.log(n);  // Third log

The answer is: 10, 9, 8

Closures

Definition

A closure is the combination of a function and the lexical environment within which that function was declared.

Examples

Example 1

After remember(5) is executed and returned, the returned function is still able to access number's value (i.e., 5). Closures allow us to store a snapshot of state at the time the function object is created. Closures really shine in situations where a function is defined within another function, allowing the nested function to access variables outside of it.

The nested anonymous function is still able to access its parent function's scope (i.e. number) even after its parent function (i.e. remember()) has returned.

function remember(number) {
    return function() {
        return number;
    }
}

// Reference to the anonymous function
const returnedFunction = remember(5);

// The anonymous function is called and maintains access to its scope; that is,
// all the variables it was able to access back when it was originally defined
// (i.e. number)
console.log( returnedFunction() );
// 5

Example 2

The logger() function is still able to access its parent function's scope (i.e. otherNumber) even after its parent function (i.e. foo()) has returned. It's important to note that the state of otherNumber is private and no method outside the closure can alter it.

const number = 5;
function foo() {
    const otherNumber = 3;
    function logger() {
        console.log(otherNumber);
    }
    return logger;
}

// Reference to logger()
const result = foo();

// logger() is called and maintains access to its scope; that is, all the
// variables it was able to access back when it was originally defined
// (i.e. otherNumber)
result();
// 3

// The state of otherNumber is private
result.otherNumber;

// The state of otherNumber is private
otherNumber;

Example 3 - Counter

The reference to the anonymous function is stored in counter. As long as this reference exists, the reference to myCounter() is maintained through the anonymous function itself. count is retained and can be modified. It's important to note that the state of count is private and no method outside the closure can alter it.

function myCounter() {
    let count = 0;
    return function() {
        count += 1;
        return count;
    }
}

// Reference to the anonynous function
let counter = myCounter();

// The anonynous function is called and maintains access to its scope (i.e. count)
counter();
// 1

// The anonynous function is called and maintains access to its scope (i.e. count)
counter();
// 2

// The anonynous function is called and maintains access to its scope (i.e. count)
counter();
// 3

// The state of count is private
counter.count;
// undefined

// The state of count is private
count;
// Uncaught ReferenceError: count is not defined

Example 4 - Expanded array

Declare a function named expandArray() that:

  • Takes no arguments
  • Contains a single local variable, myArray, which points to [1, 1, 1]
  • Returns an anonymous function that directly modifies myArray by appending another 1 into it
  • The returned function then returns the value of myArray
function expandArray() {
    const myArray = [1,1,1];
    return function() {
        myArray.push(1);
        return myArray;
    }
}

const array = expandArray();

array();
// [1, 1, 1, 1]

array();
// [1, 1, 1, 1, 1]

array();
// [1, 1, 1, 1, 1, 1]

Garbage collection

JavaScript manages memory with automatic garbage collection. This means that when data is no longer referable (i.e., there are no remaining references to that data available for executable code), it is 'garbage collected' and will be destroyed at some later point in time. This frees up the resources (i.e., computer memory) that the data had once consumed, making those resources available for re-use. As such, referenceable variables in JavaScript are not garbage collected!

function myCounter() {
    let count = 0;
    return function() {
        count += 1;
        return count;
    }
}

count is not available for garbage colection since the anonymous function retains access to the scope it was created even after its parent (i.e. myCounter()) has returned.

Immediately-invoked function expressions


Function types

  1. Function Declarations
  2. Function Expressions
  3. Immediately-Invoked Function Expressions

Function declarations

function foo() {
    return 'Hello World!';
}

Function expressions

// Anonymous
const myFunct = function() {
    return 'Hello World!';
};

// Named
const myFunct = function foo() {
    return 'Hello World!';
};

Immediately-invoked function expressions

A function that is called immediately after it is defined is called an Immediately-Invoked Function Expression (IIFE)

// Method #1

// Anonymous
(function() {
    return 'Hello World!';
})();

// Named
(function foo() {
  return 'Hello World!';
})();

// Method #2

// Anonymous
(function() {
    return 'Hello World!';
}());

// Named
(function foo() {
  return 'Hello World!';
}());

All we're doing is wrapping a function in parentheses, then adding a pair of parentheses at the end of that to invoke it.

Passing arguments to IIFE's

(function(name) {
    console.log(`Hello ${name}`);
})('Marios');

// Logs 'Hello Marios'

The second pair of parentheses not only immediately executes the function preceding it - it's also the place to put any arguments that the function may need.

(`Hello ${name}`) is different from ('Hello ${name}')

IIFE's & private scope

const myFunction = (
    function() {
        const hi = 'Hi!';
        return function() {
            console.log(hi);
        }
    }
)();

myFunction();
// Hi!

An immediately-invoked function expression is used to immediately run a function. This function runs and returns an anonymous function that is stored in the myFunction variable. myFunction maintains a private, mutable state that cannot be accessed outside the function.

What's true about IIFE's

  1. IIFE's can be used to create private scope.
  2. IIFE's are closely related to scope and closures.

IIFE's, private scope & event handling

Let's check out another example of an IIFE -- this time in the context of handling an event. Say that we want to create a button on a page that alerts the user on every other click. One way to begin doing this would be to keep track of the number of times that the button was clicked. But how should we maintain this data?

We could keep track of the count with a variable that we declare in the global scope (this would make sense if other parts of the application need access to the count data). However, an even better approach would be to enclose this data in event handler itself.

For one, this approach prevents us from polluting the global with extra variables (and potentially variable name collisions). What's more: if we use an IIFE, we can leverage a closure to protect the count variable from being accessed externally! This prevents any accidental mutations or unwanted side-effects from inadvertently altering the count.

<html>
    <body>

        <button id='button'>Click me!</button>

        <script>
            const button = document.getElementById('button');

            button.addEventListener('click', (function() {

                let count = 0;

                return function() {

                    count += 1;

                    if (count === 2) {
                        alert('This alert appears every other press!');
                        count = 0;
                    }
                };
            })());
        </script>

    </body>
</html>

The count variable is local to addEventListener(). The returned function maintains a reference to its parent's scope (i.e. addEventListener()). In other words, count is available for the returned function to use. As a result, we immediately invoke a function that returns that function. And since the returned function has access to the internal variable, count, a private scope is created - effectively protecting the data!

Steps:

  1. Button is clicked
  2. Callback is invoked
  3. The returned function that has access to count is immediately invoked

Summary

Utilizing an IIFE alongside closures allows for a private scope, which maintains privacy for variables defined within them. And since less variables are created, an IIFE will help to minimize pollution of the global environment, hindering the chances of variable name collisions.

All in all, if you simply have a one-time task (e.g., initializing an application), an IIFE is a great way to get something done without polluting your the global environment with extra variables. Cleaning up the global namespace decreases the chance of collisions with duplicate variable names, after all.

Ad1
advertisement
Ad2
advertisement
Ad3
advertisement
Ad4
advertisement
Ad5
advertisement
Ad6
advertisement