WDD 330: Web Frontend Development II

W04 Learning Activity: Advanced Function Concepts

Overview

The JavaScript engine offers powerful ways to solve problems by using functions to organize code and perform specific tasks. You already know how to define and call functions, pass arguments to them, and handle their return values. In this activity, you'll explore how JavaScript functions are executed and how they inter act with the call stack, and how they can be used as first-class objects.

Prepare

This learning activity focuses on a deeper understanding of functions including the execution context, the callstack, callbacks, and higher order functions.

What is a first-class object?

In JavaScript, functions are first-class objects. This means that functions can be treated like any other object in JavaScript. They can be assigned to variables, passed as arguments to other functions, returned from functions, and stored in data structures like arrays and objects. This allows for a high degree of flexibility and reusability in your code.

As you know, functions can be created using function declarations, function expressions, and arrow functions. Each of these methods has its own syntax and use cases, but they all create first-class functions that can be used in the same way.

Here is a simple example the demonstrates passing a function as an argument:

const fullNames = ["Aday Lovina", "Benstras Turing", "Grace Ojo"];
const lastNames = fullNames.map(function getLastName(name) {
  return name.split(" ").slice(-1)[0];
});

Higher Order Functions are functions that can take other functions as arguments or return functions as their result. This allows for more flexible and reusable code, as well as the ability to create more complex functionality by combining simple functions. The are a fundamental and major part of functional programming and include functions that you have already been using including:

Execution Context

The Execution Context is the environment in which a function is executed. It includes the scope of the function, the value of 'this', and any variables that are defined within the function. Understanding the execution context is crucial for understanding how functions work in JavaScript.

The following example demonstrates how contexts are created and how scope chains determine what variable can be accessed at different levels. You should run this code in the console to verify the concept.

const globalVariable = "I'm Global";

function outerFunction() {
  const outerVariable = "I'm Outer";  

  function innerFunction() {
    const innerVariable = "I'm Inner";
    console.log(globalVariable); // I'm Global
    console.log(outerVariable); // I'm Outer
    console.log(innerVariable); // I'm Inner
  }

  innerFunction();
  console.log(innerVariable); // ReferenceError: innerVariable is not defined
  console.log(this); // in this scope, refers to the global object (the window in browsers)
}
outerFunction();

In this example, the innerFunction() has access to the variables defined in its own scope (innerVariable) as well as the variables defined in its parent scope (outerVariable) and the global scope (globalVariable). However, the outerFunction() does not have access to the innerVariable, because it is defined in a different scope.

In the example, the this keyword refers to the global object (the window in browsers) because outerFunction() is called in the global context. If you were to call innerFunction() directly, this would refer to the function itself.

Hoisting

Hoisting is a JavaScript mechanism where variables and function declarations (but not arrow functions) are moved to the top of their containing scope during the compile or "Creation" phase. It also sets up the this binding. Hoisting is a mechanism that affects what is available and when, before any code is actually run.

Call Stacks

Call Stacks in JavaScript is a data structure that keeps track of function calls. When a function is called, it is added to the stack, and when it returns, it is removed from the stack. This allows JavaScript to keep track of where it is in the code and what functions are currently being executed.

Here is an example that you can run in the console:

function firstFunction() {
  console.log("First Function");
  secondFunction();
  console.log("Back to First Function");
}

function secondFunction() {
  console.log("Second Function");
  thirdFunction();
  console.log("Back to Second Function");
}

function thirdFunction() {
  console.log("Third Function");
  // Uncomment the next line to see the stack overflow error (RangeError)
  // thirdFunction(); 
  console.log("Back to Third Function");
}

firstFunction();

In this example, when firstFunction() is called, it adds itself to the call stack. Then it calls secondFunction(), which adds itself to the stack. Finally, it calls thirdFunction(), which adds itself to the stack. When thirdFunction() returns, it is removed from the stack, and control goes back to secondFunction(), and then back to firstFunction().

Optional Resources