Function Context

When a function runs, it runs inside an execution context. The context holds information about how and where the function was called. The context matters because it affects what data a function can access beyond its parameters and local variables.

What is Execution Context?

Every time a function is invoked, an execution context is created for that call. This context contains:

  • The function’s local variables and parameters
  • A reference to the outer (lexical) scope
  • Information about how the function was invoked

How the function was invoked is exposed through a special keyword called this.

The this Keyword

The this keyword gives a function access to its execution context, and more specifically to the object that “owns” or invoked the function. Parameters are passed explicitly. this is set implicitly, based on how the function is called.

function displayBalance() {
  console.log(this.balance);
}

const account = {
  balance: 100,
};

displayBalance(); // undefined (in non-strict mode, 'this' refers to the global object; in strict mode, 'this' is undefined)

In the example above, displayBalance tries to access this.balance, but when called as a standalone function, this does not refer to our account object. The following methods let you set this explicitly.

Controlling Context with bind

The bind() method creates a new function with its this value permanently set to a specific object.

const account = {
  balance: 100,
};

function displayBalance() {
  console.log(this.balance);
}

const showAccountBalance = displayBalance.bind(account);
showAccountBalance(); // 100 ('this' now refers to 'account')

Here is another example showing how bind lets you create specialized versions of a function:

function greet() {
  console.log(`Welcome to ${this.name}`);
}

const cs280 = { name: "Full-Stack JavaScript" };
const cs226 = { name: "Data Structures" };

const greet280 = greet.bind(cs280);
const greet226 = greet.bind(cs226);

greet280(); // "Welcome to Full-Stack JavaScript"
greet226(); // "Welcome to Data Structures"

Controlling Context with call

The call() method invokes a function immediately with a specified this value. Arguments are passed individually after the context object.

const account = {
  balance: 100,
};

function deposit(amount) {
  this.balance += amount;
}

function withdraw(amount) {
  this.balance -= amount;
}

deposit.call(account, 50); // account.balance is now 150
withdraw.call(account, 20); // account.balance is now 130

console.log(account.balance); // 130

Controlling Context with apply

The apply() method works like call, but arguments are passed as an array instead of individually.

const stats = {
  max: Number.MIN_SAFE_INTEGER,
  min: Number.MAX_SAFE_INTEGER,
};

function max(...args) {
  for (let i = 0; i < args.length; i++) {
    if (this.max < args[i]) {
      this.max = args[i];
    }
  }
}

function min(...args) {
  for (let i = 0; i < args.length; i++) {
    if (this.min > args[i]) {
      this.min = args[i];
    }
  }
}

const numbers = [5, 6, 2, 3, 7];
max.apply(stats, numbers);
min.call(stats, ...numbers);

console.log(stats); // { max: 7, min: 2 }

Notice that apply takes the array directly, while call requires spreading the array into individual arguments.

Arrow Functions and this

Unlike regular functions, arrow functions do not have their own this binding. Instead, they capture this from the enclosing lexical scope at the time they are defined.

const account = {
  balance: 100,
  displayWithRegular: function () {
    setTimeout(function () {
      console.log(this.balance); // undefined ('this' is not the account)
    }, 100);
  },
  displayWithArrow: function () {
    setTimeout(() => {
      console.log(this.balance); // 100 ('this' is captured from displayWithArrow)
    }, 100);
  },
};

account.displayWithRegular(); // undefined
account.displayWithArrow(); // 100

In displayWithRegular, the callback passed to setTimeout is a regular function, so it gets its own this (which is the global object or undefined in strict mode). In displayWithArrow, the arrow function captures this from the enclosing displayWithArrow method, which refers to account.

This behavior makes arrow functions useful for callbacks where you want to preserve the surrounding context. Before arrow functions, developers would use workarounds like storing this in a variable:

const account = {
  balance: 100,
  display: function () {
    const self = this; // save reference to 'this'
    setTimeout(function () {
      console.log(self.balance); // 100
    }, 100);
  },
};