Skip to content

Imagine you have a universal utility function, but you want to use it in different contexts. JavaScript's call, apply, and bind methods are like three magicians that allow you to precisely control the this binding in functions, making your code more flexible and reusable.

Let's start with a simple scenario:

javascript
const person = {
  name: "Alice",
  age: 30,
  introduce() {
    return `Hi, I'm ${this.name} and I'm ${this.age} years old`;
  },
};

const anotherPerson = {
  name: "Bob",
  age: 25,
};

console.log(person.introduce()); // "Hi, I'm Alice and I'm 30 years old"

// How to make person.introduce execute in anotherPerson's context?
console.log(person.introduce.call(anotherPerson)); // "Hi, I'm Bob and I'm 25 years old"

Through the call method, we successfully made the introduce function execute in the context of anotherPerson!

Call Method: Direct Invocation and Parameter Passing

The call method allows you to call a function and explicitly specify the this value and parameters for when the function runs.

Basic Syntax

javascript
functionName.call(thisArg, arg1, arg2, ..., argN)
  • thisArg: The this value to be used when the function runs
  • arg1, arg2, ..., argN: List of arguments to pass to the function

Use Cases

1. Function Borrowing

javascript
const arrayLike = {
  0: "apple",
  1: "banana",
  2: "orange",
  length: 3,
};

// Borrow Array.prototype.slice method
const realArray = Array.prototype.slice.call(arrayLike);
console.log(realArray); // ['apple', 'banana', 'orange']
console.log(Array.isArray(realArray)); // true

2. Changing this Binding

javascript
function greet(greeting, punctuation) {
  return `${greeting}, ${this.name}${punctuation}`;
}

const person = {
  name: "John",
};

console.log(greet.call(person, "Hello", "!")); // "Hello, John!"
console.log(greet.call(person, "Hi", ".")); // "Hi, John."

3. Constructor Inheritance

javascript
function Parent(name) {
  this.name = name;
  this.colors = ["red", "blue", "green"];
}

function Child(name, age) {
  // Inherit Parent's properties
  Parent.call(this, name);
  this.age = age;
}

const child = new Child("Emma", 12);
console.log(child.name); // 'Emma'
console.log(child.colors); // ['red', 'blue', 'green']
console.log(child.age); // 12

Apply Method: The Magic of Array Parameters

The apply method is similar to call, but it accepts an array (or array-like object) as parameters.

Basic Syntax

javascript
functionName.apply(thisArg, [argsArray]);
  • thisArg: The this value to be used when the function runs
  • argsArray: An array or array-like object containing arguments to pass to the function

Use Cases

1. Array Operations

javascript
const numbers = [5, 2, 8, 1, 9, 3];

// Find maximum value
const max = Math.max.apply(null, numbers);
console.log(max); // 9

// Find minimum value
const min = Math.min.apply(null, numbers);
console.log(min); // 1

2. Function Parameter Assembly

javascript
function calculateTotal(price, tax, discount, shipping) {
  return price + tax - discount + shipping;
}

const costs = [100, 8, 10, 15];
const total = calculateTotal.apply(null, costs);
console.log(total); // 113

3. Chained Calls

javascript
const methods = ["push", "pop", "shift", "unshift"];
const array = [1, 2, 3];

methods.forEach((method) => {
  if (method === "push") {
    Array.prototype[method].apply(array, [4]);
  }
});

console.log(array); // [1, 2, 3, 4]

Bind Method: The Art of Permanent Binding

The bind method creates a new function that, when called, has its this value set to the first argument of bind, with subsequent arguments available as preset arguments for the bound function.

Basic Syntax

javascript
const newFunction = functionName.bind(thisArg, arg1, arg2, ..., argN)
  • thisArg: The this value to be used when the new function runs
  • arg1, arg2, ..., argN: Preset arguments

Use Cases

1. Permanent this Binding

javascript
const counter = {
  count: 0,
  increment() {
    this.count++;
    console.log(this.count);
  },
};

const increment = counter.increment;

// Without bind, this points to global object
increment(); // NaN (global object has no count property)

// Use bind to bind correct this
const boundIncrement = counter.increment.bind(counter);
boundIncrement(); // 1
boundIncrement(); // 2

2. Preset Arguments (Currying)

javascript
function multiply(a, b) {
  return a * b;
}

// Create function with fixed first argument
const double = multiply.bind(null, 2);
const triple = multiply.bind(null, 3);

console.log(double(5)); // 10
console.log(triple(5)); // 15

// Preset multiple arguments
function discountPrice(price, discountRate, tax) {
  return price * (1 - discountRate) + tax;
}

const applyTenPercentDiscount = discountPrice.bind(null, 0.1);
const finalPrice = applyTenPercentDiscount(100, 5);
console.log(finalPrice); // 95

3. this in Event Handlers

javascript
const button = {
  text: "Click me",
  clicks: 0,

  init() {
    const buttonElement = document.getElementById("myButton");

    // Without bind, this points to button element
    buttonElement.addEventListener("click", function () {
      this.clicks++; // Error: this points to button element
      console.log(this.text); // undefined
    });

    // Use bind to correctly bind this
    buttonElement.addEventListener(
      "click",
      function () {
        this.clicks++;
        console.log(`${this.text} clicked ${this.clicks} times`);
      }.bind(this)
    );
  },
};

4. this in Timers

javascript
const timer = {
  seconds: 0,
  start() {
    setInterval(
      function () {
        this.seconds++;
        console.log(`Timer: ${this.seconds} seconds`);
      }.bind(this),
      1000
    );
  },
};

timer.start(); // Timer: 1 seconds, Timer: 2 seconds, ...

Call vs Apply vs Bind Comparison

Syntax Comparison

javascript
function greet(greeting, punctuation) {
  return `${greeting}, ${this.name}${punctuation}`;
}

const person = { name: "Sarah" };

// Call: Pass arguments individually
console.log(greet.call(person, "Hello", "!")); // "Hello, Sarah!"

// Apply: Pass arguments through array
console.log(greet.apply(person, ["Hi", "."])); // "Hi, Sarah."

// Bind: Create new function, can be called later
const boundGreet = greet.bind(person, "Hey");
console.log(boundGreet("?")); // "Hey, Sarah?"

Execution Timing Comparison

javascript
function showTime() {
  console.log(new Date().toLocaleTimeString());
}

// Call and Apply: Execute immediately
showTime.call(null); // Show time immediately
showTime.apply(null); // Show time immediately

// Bind: Create new function, execute with delay
const delayedShowTime = showTime.bind(null);
setTimeout(delayedShowTime, 1000); // Show time after 1 second

Advanced Application Techniques

1. Function Method Chaining

javascript
const calculator = {
  value: 0,

  add(num) {
    this.value += num;
    return this; // Return self to support chaining
  },

  multiply(num) {
    this.value *= num;
    return this;
  },

  subtract(num) {
    this.value -= num;
    return this;
  },

  getValue() {
    return this.value;
  },
};

const result = calculator.add(5).multiply(2).subtract(3).getValue();
console.log(result); // 7

2. Borrowing Constructor Functions

javascript
function EventEmitter() {
  this.events = {};
}

EventEmitter.prototype.on = function (event, callback) {
  if (!this.events[event]) {
    this.events[event] = [];
  }
  this.events[event].push(callback);
};

EventEmitter.prototype.emit = function (event, ...args) {
  if (this.events[event]) {
    this.events[event].forEach((callback) => callback.apply(this, args));
  }
};

function User(name) {
  EventEmitter.call(this); // Borrow EventEmitter's constructor
  this.name = name;
}

// Inherit prototype methods
User.prototype = Object.create(EventEmitter.prototype);
User.prototype.constructor = User;

const user = new User("John");
user.on("login", function () {
  console.log(`${this.name} logged in`);
});

user.emit("login"); // "John logged in"

3. Array Operation Utility Functions

javascript
const arrayUtils = {
  // Borrow Array.prototype methods
  slice: Array.prototype.slice,
  map: Array.prototype.map,
  filter: Array.prototype.filter,

  // Convert array-like to real array
  toArray(list) {
    return this.slice.call(list);
  },

  // Filter object arrays
  filterByProperty(array, property, value) {
    return this.filter.call(array, function (item) {
      return item[property] === value;
    });
  },
};

const nodeList = document.querySelectorAll("div");
const divs = arrayUtils.toArray(nodeList);

const users = [
  { name: "Alice", age: 25, role: "admin" },
  { name: "Bob", age: 30, role: "user" },
  { name: "Charlie", age: 35, role: "admin" },
];

const admins = arrayUtils.filterByProperty(users, "role", "admin");
console.log(admins); // [{ name: 'Alice', age: 25, role: 'admin' }, { name: 'Charlie', age: 35, role: 'admin' }]

Performance Considerations

1. Avoid Overusing bind

javascript
// Not recommended: creating new function every render
function Component() {
  this.handleClick = function () {
    console.log("clicked");
  }.bind(this);

  // Every Component instantiation creates new handleClick function
}

// Recommended: define methods on prototype
function Component() {
  // Constructor
}

Component.prototype.handleClick = function () {
  console.log("clicked");
};

// Bind when using
const component = new Component();
element.addEventListener("click", component.handleClick.bind(component));

2. Use apply for Handling Large Numbers of Arguments

javascript
// Handle variable number of arguments
function sum() {
  return Array.prototype.reduce.call(
    arguments,
    function (total, num) {
      return total + num;
    },
    0
  );
}

// Use apply to pass argument array
const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const total = sum.apply(null, numbers);
console.log(total); // 55

Real-World Project Applications

1. Event Handling in React Components

javascript
class Counter extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 };

    // Bind in constructor to avoid creating new functions on every render
    this.increment = this.increment.bind(this);
    this.decrement = this.decrement.bind(this);
  }

  increment() {
    this.setState((prevState) => ({ count: prevState.count + 1 }));
  }

  decrement() {
    this.setState((prevState) => ({ count: prevState.count - 1 }));
  }

  render() {
    return (
      <div>
        <button onClick={this.increment}>+</button>
        <span>{this.state.count}</span>
        <button onClick={this.decrement}>-</button>
      </div>
    );
  }
}

2. Function Composition in Functional Programming

javascript
// Create universal function composer
function compose() {
  const funcs = Array.prototype.slice.call(arguments);

  return function () {
    const args = Array.prototype.slice.call(arguments);

    return funcs.reduceRight(function (acc, func) {
      return func.apply(null, Array.isArray(acc) ? acc : [acc]);
    }, args);
  };
}

const add = (a, b) => a + b;
const multiply = (a, b) => a * b;
const square = (x) => x * x;

const addAndSquare = compose(square, add);
const multiplyAndAdd = compose(add, multiply);

console.log(addAndSquare(3, 4)); // 49 ((3 + 4) * (3 + 4))
console.log(multiplyAndAdd(3, 4)); // 12 (3 * 4 + 0)

3. Borrowing Array Methods for String Processing

javascript
String.prototype.reverse = function () {
  return Array.prototype.reduce.call(
    this,
    function (acc, char) {
      return char + acc;
    },
    ""
  );
};

String.prototype.uniqueChars = function () {
  return Array.prototype.filter
    .call(this, function (char, index, arr) {
      return arr.indexOf(char) === index;
    })
    .join("");
};

console.log("hello".reverse()); // "olleh"
console.log("banana".uniqueChars()); // "ban"

Common Traps and Solutions

1. bind Cannot Be Rebound

javascript
function greet() {
  return this.name;
}

const person1 = { name: "Alice" };
const person2 = { name: "Bob" };

const boundGreet = greet.bind(person1);
console.log(boundGreet()); // "Alice"

// Attempting to rebind will fail
const reboundGreet = boundGreet.bind(person2);
console.log(reboundGreet()); // "Alice", still Alice

// Solution: create new binding of original function
const newBoundGreet = greet.bind(person2);
console.log(newBoundGreet()); // "Bob"

2. bind in Constructor Functions

javascript
function Person(name) {
  this.name = name;
}

Person.prototype.sayHello = function () {
  return `Hello, ${this.name}`;
};

const boundSayHello = Person.prototype.sayHello.bind({ name: "Stranger" });

const person = new Person("John");
console.log(boundSayHello.call(person)); // "Hello, Stranger", won't change bound this

Summary

call, apply, and bind are the three magicians for controlling this binding in JavaScript:

  • call: Execute function immediately, pass arguments individually
  • apply: Execute function immediately, pass arguments through array
  • bind: Create new function, preset this and arguments, execute with delay

These three methods provide powerful flexibility for JavaScript functional programming, enabling you to:

  • Borrow methods from other objects
  • Change function execution context
  • Preset function arguments
  • Handle variable parameter lists
  • Implement function composition and currying

Mastering these three methods gives you mastery over the core skills of this binding control in JavaScript, allowing you to write more flexible and reusable code.