LEVEL 4 · LESSON 1 OF 17

The object model Core

this in depth

Predict this in every kind of call, fix a lost this with call, apply, bind and arrow functions, and write your own bind and bindAll helpers.

  • 50 min to read and try
  • You need: this, prototypes and classes, Closures in depth, and the JavaScript fundamentals course
  • You build: A bind function written from scratch and a bindAll helper that makes a payment service safe to pass around as callbacks
Test yourself

BY THE END OF THIS LESSON YOU CAN

  • State the rule that decides this for any call and apply it to method calls, plain calls, constructors and callbacks
  • Explain how strict mode changes this and why modules and classes behave differently from old scripts
  • Use call, apply and bind, including partial application, and predict what cannot be rebound
  • Choose between arrow functions, bound methods, arrow class fields and wrapper callbacks
  • Predict this inside DOM and EventEmitter handlers
  • Implement bind and bindAll yourself

The worker that lost its gateway

A shop processes payments in a background job runner. The runner is generic: it takes a list of jobs and a handler function, and calls the handler for each job. The payment logic lives in a class, as you learned in this, prototypes and classes:

problem.js
class PaymentService {
  constructor(gateway) {
    this.gateway = gateway;
    this.processed = 0;
  }

  charge(job) {
    this.processed += 1;
    return this.gateway.charge(job.orderId, job.amountKobo);
  }
}

function runJobs(jobs, handler) {
  const results = [];
  for (const job of jobs) {
    try {
      results.push(handler(job));
    } catch (error) {
      results.push(`failed: ${error.message}`);
    }
  }
  return results;
}

const gateway = { charge: (orderId, kobo) => `charged ₦${kobo / 100} for ${orderId}` };
const payments = new PaymentService(gateway);
const jobs = [{ orderId: "ORD-7", amountKobo: 1170000 }, { orderId: "ORD-8", amountKobo: 500000 }];

console.log(payments.charge(jobs[0]));
console.log(runJobs(jobs, payments.charge));
Output of node problem.js and of the browser terminal
charged ₦11700 for ORD-7
[
  "failed: Cannot read properties of undefined (reading 'processed')",
  "failed: Cannot read properties of undefined (reading 'processed')"
]

The same method works when called as payments.charge(job) and fails when the runner calls it. Nothing about the method changed; only the way it was called changed. The earlier lesson showed you this problem and two quick fixes. This lesson gives you the complete rule, so you can predict this in any situation you meet: plain calls in old and new code, call, apply and bind, arrow functions, class fields, DOM events and Node.js event emitters. At the end you write bind yourself and a bindAll helper that makes the payment service safe to hand to any runner.

One rule: this is decided by the call

Inside a normal function (anything written with function or as a method, but not an arrow function), this is an extra, hidden parameter. Like any parameter, it gets its value when the function is called, from the way it is called. Where the function was written, and which object it was first attached to, do not matter. The place in the code where a call happens is called the call site, and this table covers every kind of call site:

How the function is calledValue of this
obj.method(), obj["method"](), obj.method?.()obj, the object before the last dot or bracket
fn(), a plain callundefined in strict mode; globalThis in old non-strict code
new Fn()The brand new object being created
fn.call(x, a, b), fn.apply(x, [a, b])x
bound(), where bound = fn.bind(x)x, always
A callback: other(fn)Whatever other does when it calls fn, usually a plain call
An arrow function, called any way at allThe this of the code around the arrow

Here is one function called in six of those ways:

call-sites.js
function whoAmI() {
  return this === undefined ? "undefined" : this.name;
}

const shop = { name: "shop", whoAmI, branch: { name: "branch", whoAmI } };
const courier = { name: "courier" };

console.log(shop.whoAmI());
console.log(shop.branch.whoAmI());
console.log(shop["whoAmI"]());
console.log(whoAmI());
console.log(whoAmI.call(courier));

const detached = shop.whoAmI;
console.log(detached());
Output of node call-sites.js and of the browser terminal
shop
branch
shop
undefined
courier
undefined

The same function object gave four different answers. shop.branch.whoAmI() gives branch: only the object directly before the method name counts, not the whole chain. And detached is the very same function as shop.whoAmI, but called without a dot, so it gets nothing. Keep the table in mind: the rest of this lesson is each row in detail.

Plain calls and strict mode

What a plain call gives depends on whether the function runs in strict mode, the stricter set of JavaScript rules that turns silent mistakes into errors. Code is strict when it is inside an ES module (every file in this course), inside a class, or in a file or function that starts with the line "use strict". Old browser scripts and CommonJS files without that line are sloppy mode, the informal name for non-strict code.

  • In strict mode, a plain call gets this = undefined. Reading this.name then throws a TypeError right away.
  • In sloppy mode, a plain call gets this = globalThis, the global object (window in a browser, global in Node.js). Reading this.name quietly reads a global, and writing this.x = … quietly creates a global variable.

Every example here runs in strict mode, so to see sloppy mode this example uses the Function constructor, which builds a function from a string of code and always makes a sloppy one:

strict.js
function strictWho() {
  return this;
}
const sloppyWho = new Function("return this;");

console.log(strictWho());
console.log(sloppyWho() === globalThis);

const sloppyCounter = new Function("this.visits = (this.visits || 0) + 1; return this.visits;");
sloppyCounter();
sloppyCounter();
console.log("globalThis.visits:", globalThis.visits);
delete globalThis.visits;

function strictType() {
  return typeof this;
}
const sloppyType = new Function("return typeof this;");
console.log(strictType.call("NGN"), sloppyType.call("NGN"));
console.log(strictType.call(null), sloppyType.call(null));
Output of node strict.js and of the browser terminal
undefined
true
globalThis.visits: 2
string object
object object

Three differences to take away:

  1. A plain call in sloppy mode reaches the global object. sloppyCounter meant to count something on an object, and instead created a global variable called visits that every other piece of code can see and break. In strict mode the same code throws on the first line, which is what you want.
  2. In sloppy mode, a primitive this such as the string "NGN" is wrapped into an object (a String object). Strict mode passes it as it is.
  3. In sloppy mode, null or undefined as this is replaced with the global object (typeof says "object" for both lines here, but for different reasons: strict mode really got null).

You will write strict code, but you will read and call sloppy code: older libraries, CommonJS files, snippets pasted into a browser console. When a this bug behaves differently in two places, check which mode each one runs in.

NOTE

At the top level of a file, outside any function, this is undefined in an ES module, and module.exports (an empty object at first) in a CommonJS file. Module systems covers the difference.

Method calls

A call written as something.method() sets this to something. That is called implicit binding: nothing says "this" out loud, the dot does it. Because the value comes from the call, one function can serve many objects. That is how methods are shared through prototypes, and it also lets you borrow a method by putting it on another object:

implicit.js
const naira = {
  code: "NGN",
  symbol: "₦",
  format(kobo) {
    return `${this.symbol}${(kobo / 100).toFixed(2)} ${this.code}`;
  },
};

const cedi = { code: "GHS", symbol: "GH₵", format: naira.format };

console.log(naira.format(150000));
console.log(cedi.format(150000));
console.log(naira.format === cedi.format);

const formats = { primary: naira };
console.log(formats.primary.format(99));
console.log(formats.primary?.format?.(99));
Output of node implicit.js and of the browser terminal
₦1500.00 NGN
GH₵1500.00 GHS
true
₦0.99 NGN
₦0.99 NGN

cedi.format is the same function as naira.format, yet it used the cedi's symbol and code, because it was called on cedi. Optional chaining (?.) keeps the object too: formats.primary?.format?.(99) is still a method call on formats.primary.

Getters and setters

Getters and setters (Objects in depth) follow the same rule: this is the object the property was read on. That includes reading through the prototype chain: if the getter lives on a prototype, this is still the object you started from, which is why one getter on a class prototype can serve every instance.

How this gets lost

Implicit binding only lasts for the call written with the dot. The moment the function is taken away from its object and called some other way, the binding is gone. That happens in more places than it looks:

losing.js
class Basket {
  constructor() {
    this.items = [];
  }
  add(sku) {
    this.items.push(sku);
    return this.items.length;
  }
}

function attempt(label, run) {
  try {
    console.log(`${label}: ${run()}`);
  } catch (error) {
    console.log(`${label}: ${error.message}`);
  }
}

const basket = new Basket();

attempt("method call", () => basket.add("RICE-5"));
attempt("stored in a variable", () => {
  const add = basket.add;
  return add("OIL-1");
});
attempt("destructured", () => {
  const { add } = basket;
  return add("OIL-1");
});
attempt("passed as a callback", () => ["SALT", "SUGAR"].map(basket.add));
attempt("thisArg of map", () => ["SALT", "SUGAR"].map(basket.add, basket));
attempt("wrapper arrow", () => ["BEANS"].map((sku) => basket.add(sku)));
console.log(basket.items);
Output of node losing.js and of the browser terminal
method call: 1
stored in a variable: Cannot read properties of undefined (reading 'items')
destructured: Cannot read properties of undefined (reading 'items')
passed as a callback: Cannot read properties of undefined (reading 'items')
thisArg of map: 2,3
wrapper arrow: 4
[ 'RICE-5', 'SALT', 'SUGAR', 'BEANS' ]
  • Stored in a variable and destructured: const { add } = basket is only a shorter way to write const add = basket.add. The function is copied out; the object is not.
  • Passed as a callback: map receives the function only. It calls it as a plain function.
  • thisArg: map, filter, forEach, find, some and every accept a second argument that they use as this for the callback. It works, but few people know it, so wrapper arrows are clearer.
  • Wrapper arrow: (sku) => basket.add(sku) puts the dot back. This is the fix you will write most often.

Timers, event emitters, routers and job runners all receive functions, not objects. Each decides how to call your function, so a method passed to any of them has lost its this unless you do something about it. (Timers are a special case: in Node.js, setTimeout calls your function with its own timer object as this, and browsers pass window. Neither is your object.)

call, apply and bind

Every function has three methods that set this on purpose, called explicit binding:

  • fn.call(thisValue, arg1, arg2) calls fn now, with the given this and arguments.
  • fn.apply(thisValue, [arg1, arg2]) does the same, but takes the arguments as one array.
  • fn.bind(thisValue, arg1) does not call anything. It returns a new bound function that, whenever it is called, calls fn with that this and those first arguments, followed by any new ones.
explicit.js
function describeTransfer(amountKobo, note) {
  return `${this.owner} sends ₦${amountKobo / 100} (${note})`;
}

const ada = { owner: "Ada" };
const chidi = { owner: "Chidi" };

console.log(describeTransfer.call(ada, 500000, "rent"));
console.log(describeTransfer.apply(chidi, [20000, "lunch"]));

const adaSends = describeTransfer.bind(ada);
console.log(adaSends(150000, "school fees"));

const adaSendsRent = describeTransfer.bind(ada, 500000);
console.log(adaSendsRent("October rent"));

console.log(adaSends.name, adaSends.length, adaSendsRent.length);
Output of node explicit.js and of the browser terminal
Ada sends ₦5000 (rent)
Chidi sends ₦200 (lunch)
Ada sends ₦1500 (school fees)
Ada sends ₦5000 (October rent)
bound describeTransfer 2 1

adaSendsRent fixes both this and the first argument: that is partial application again, the same idea as the factories in Closures in depth, done by bind. The bound function's name starts with "bound ", which helps when you read stack traces, and its length (the number of parameters it expects) shrinks by the arguments already fixed.

Where apply still appears

apply used to be the only way to call a function with an array of arguments. Spread replaced most of those uses, but you will read both:

apply.js
const pricesKobo = [850000, 320000, 20000];

console.log(Math.max.apply(null, pricesKobo));
console.log(Math.max(...pricesKobo));

function logWithPrefix(prefix) {
  const rest = Array.prototype.slice.call(arguments, 1);
  return `${prefix} ${rest.join(" ")}`;
}
console.log(logWithPrefix("[orders]", "ORD-7", "shipped"));
Output of node apply.js and of the browser terminal
850000
850000
[orders] ORD-7 shipped

Math.max does not use this, so old code passes null. The second pattern is method borrowing: arguments (the old hidden list of all arguments) is not a real array, so old code borrowed slice from Array.prototype and ran it with arguments as this. Today you would write a rest parameter, (prefix, ...rest).

A bound function cannot be rebound

Binding is permanent. Once a function is bound, call, apply, a method call and even another bind cannot change its this:

bind-permanent.js
function owner() {
  return this.owner;
}

const ada = { owner: "Ada" };
const chidi = { owner: "Chidi" };

const adaOwner = owner.bind(ada);
chidi.adaOwner = adaOwner;

console.log(adaOwner());
console.log(adaOwner.call(chidi));
console.log(chidi.adaOwner());
console.log(adaOwner.bind(chidi)());
Output of node bind-permanent.js and of the browser terminal
Ada
Ada
Ada
Ada

The bound function ignores whatever this it is called with and always calls the original with ada. That is its whole purpose, and also a trap: if a library calls your bound callback with a this it expects you to use, you will not see it. The only call that overrides a binding is new, which always creates a new object; constructing a bound function is rare, and Prototypes in depth explains what new does.

Arrow functions: this from outside

An arrow function has no this of its own. Inside an arrow, this is looked up like any other variable, in the surrounding code, the same way closures find variables (Closures in depth). That is called lexical this. It has two consequences, one helpful and one harmful:

arrows.js
const report = {
  branch: "Lagos",
  orders: [{ id: "ORD-7", kobo: 1170000 }, { id: "ORD-8", kobo: 500000 }],

  linesWithArrow() {
    return this.orders.map((order) => `${this.branch}: ${order.id}`);
  },

  linesWithFunction() {
    return this.orders.map(function (order) {
      return `${this?.branch}: ${order.id}`;
    });
  },

  linesOldStyle() {
    const self = this;
    return this.orders.map(function (order) {
      return `${self.branch}: ${order.id}`;
    });
  },

  arrowMethod: () => typeof this,
};

console.log(report.linesWithArrow());
console.log(report.linesWithFunction());
console.log(report.linesOldStyle());
console.log(report.arrowMethod());

const arrow = () => typeof this;
console.log(arrow.call(report), arrow.bind(report)());
Output of node arrows.js and of the browser terminal
[ 'Lagos: ORD-7', 'Lagos: ORD-8' ]
[ 'undefined: ORD-7', 'undefined: ORD-8' ]
[ 'Lagos: ORD-7', 'Lagos: ORD-8' ]
undefined
undefined undefined
  • Helpful: inside linesWithArrow, the arrow callback uses the method's this, which is report. A callback inside a method is exactly where arrows belong.
  • A function callback in the same place gets its own this, from map's plain call: undefined. Before arrows existed, code saved the outer value in a variable, usually called self or that. You will see linesOldStyle in older code; it is a closure over self.
  • Harmful: arrowMethod is an arrow written directly in the object literal. The code around it is the module, where this is undefined. An object literal does not create a this; only functions do. Never write methods as arrows in object literals.
  • Explicit binding has no effect on an arrow: call and bind cannot give it a this, because it never looks for one of its own.

Arrow functions also have no arguments of their own and cannot be called with new. The practical rule: methods with method syntax, callbacks with arrows.

this in classes

Class bodies are always strict, and class methods live on the prototype, so a detached class method always gets this = undefined. There are three standard ways to make a class method safe to hand out as a callback:

class-handlers.js
class Checkout {
  constructor(label) {
    this.label = label;
    this.boundPay = this.pay.bind(this);
  }
  pay(orderId) {
    return `${this.label} paid ${orderId}`;
  }
  refund = (orderId) => `${this.label} refunded ${orderId}`;
}

const lagos = new Checkout("Lagos till");
const abuja = new Checkout("Abuja till");
const orders = ["ORD-7"];

console.log(orders.map((id) => lagos.pay(id)));
console.log(orders.map(lagos.boundPay));
console.log(orders.map(lagos.refund));

console.log(Object.keys(lagos));
console.log(lagos.pay === abuja.pay, lagos.refund === abuja.refund);
console.log(typeof Checkout.prototype.pay, typeof Checkout.prototype.refund);
Output of node class-handlers.js and of the browser terminal
[ 'Lagos till paid ORD-7' ]
[ 'Lagos till paid ORD-7' ]
[ 'Lagos till refunded ORD-7' ]
[ 'refund', 'label', 'boundPay' ]
true false
function undefined
  1. Wrap at the call site: (id) => lagos.pay(id). Nothing changes in the class. Clear, and you see the object at the point where it matters.
  2. Bind in the constructor: this.boundPay = this.pay.bind(this). Each instance gets its own bound copy, stored as an own property.
  3. Arrow function class field: refund = (orderId) => …. A class field is a property that every new instance gets, set up just before the constructor body runs (look at the order in Object.keys: refund comes before label). An arrow in a field takes this from that setup, which is the new instance, so it is bound for life.

The last two lines show the price of options 2 and 3: they create a new function per instance (lagos.refund !== abuja.refund), and the arrow field is not on the prototype at all. That has consequences beyond memory:

field-override.js
class Checkout {
  pay(orderId) {
    return `paid ${orderId}`;
  }
  refund = (orderId) => `refunded ${orderId}`;
}

class AuditedCheckout extends Checkout {
  pay(orderId) {
    return `${super.pay(orderId)} (audited)`;
  }
  refund = (orderId) => `${super.refund?.(orderId) ?? "no parent refund"} (audited)`;
}

const till = new AuditedCheckout();
console.log(till.pay("ORD-7"));
console.log(till.refund("ORD-7"));
Output of node field-override.js and of the browser terminal
paid ORD-7 (audited)
no parent refund (audited)

super.pay looks on the parent's prototype and finds pay. super.refund finds nothing, because the parent's arrow refund is an own property of each instance, and the child's field replaced it on the same instance. Arrow fields cannot be extended with super, and tools that replace methods on the prototype (such as test spies) do not see them.

ApproachPer-instance costWorks with super and prototype spiesUse when
Wrapper arrow at the call siteNone (one small arrow where you pass it)YesDefault choice
bind in the constructorOne function per bound methodYes (the original stays on the prototype)A method is passed around in many places
Arrow class fieldOne function per fieldNoSmall classes that are never extended, such as UI handlers

this in event handlers

An event system is a function that calls your listener, so it decides this. The two you will meet most follow the same convention: a listener written with function gets the object the listener was registered on.

In the browser

For element.addEventListener("click", listener), the browser calls a function listener with this set to element, the same value as event.currentTarget. An arrow listener keeps the this of the surrounding code instead:

index.html
<!doctype html>
<html>
<body>
  <ul id="cart">
    <li><button id="remove-rice">Remove rice</button></li>
    <li><button id="remove-oil">Remove oil</button></li>
  </ul>
</body>
</html>
handlers.js
const rice = document.getElementById("remove-rice");
const cart = document.getElementById("cart");

rice.addEventListener("click", function (event) {
  console.log("function:", this.id, this === event.currentTarget);
});
rice.addEventListener("click", (event) => {
  console.log("arrow:", this, event.currentTarget.id);
});

cart.addEventListener("click", function (event) {
  console.log("delegated: this is", this.id, "but the click was on", event.target.id);
});

rice.click();
document.getElementById("remove-oil").click();
What the browser terminal prints
function: remove-rice true
arrow: undefined remove-rice
delegated: this is cart but the click was on remove-rice
delegated: this is cart but the click was on remove-oil

The last two lines show the trap with event delegation (one listener on a parent for all its children): this is the element that has the listener, the list, not the button that was clicked. Code that reads event.target and event.currentTarget says exactly what it means, and works the same in function and arrow listeners. Prefer them over this in handlers.

In Node.js

Node's EventEmitter, which many Node.js APIs are built on, calls a function listener with this set to the emitter:

emitter.jsNode.js only
import { EventEmitter } from "node:events";

const orders = new EventEmitter();
orders.name = "order events";

orders.on("paid", function (orderId) {
  console.log(`function: ${this.name} got ${orderId}`);
});
orders.on("paid", (orderId) => {
  console.log(`arrow: this is ${this}, got ${orderId}`);
});

orders.emit("paid", "ORD-7");
Output of node emitter.js
function: order events got ORD-7
arrow: this is undefined, got ORD-7

If your listener is a method of your own class, neither of these is your object. Register it with a wrapper, orders.on("paid", (id) => mailer.sendReceipt(id)), so the call inside has its dot back.

When several rules apply

A call can match more than one row of the table: a bound function stored on an object and called as a method, say. The rules have a fixed order of strength:

  1. An arrow function ignores all of them and uses the outer this.
  2. new wins over everything else: the new object.
  3. A bound function uses its bound value, whatever the call looks like.
  4. call and apply use their first argument.
  5. A method call uses the object before the dot.
  6. A plain call gets undefined (strict) or globalThis (sloppy).
precedence.js
function label() {
  return this?.name ?? "nobody";
}

const shop = { name: "shop" };
const bank = { name: "bank" };

shop.label = label;
shop.boundToBank = label.bind(bank);

console.log(shop.label());
console.log(shop.label.call(bank));
console.log(shop.boundToBank());
console.log(shop.boundToBank.call(shop));

function Account(owner) {
  this.owner = owner;
}
const BoundAccount = Account.bind(bank);
const made = new BoundAccount("Ada");
console.log(made.owner, bank.owner);
Output of node precedence.js and of the browser terminal
shop
bank
bank
bank
Ada undefined

The last line shows new beating bind: the constructor ran with a fresh object, and bank was not touched. You will rarely combine rules like this on purpose, but when a this bug confuses you, find the call site and walk down this list.

Before you build: a helper that binds everything

REASON IT OUT

Design bind and bindAll

You will write myBind(fn, thisValue, ...fixedArgs) and then bindAll(instance), which replaces every method of an instance with a bound copy so that runJobs(jobs, payments.charge) works. Before writing them, decide:

  • How can myBind call fn with a chosen this without using bind? And how does the returned function remember fn, thisValue and the fixed arguments?
  • Where do a class instance's methods actually live? Does Object.keys(instance) list them?
  • What about methods inherited from a parent class?
  • Which names on a prototype must not be bound?
  • A getter such as get total() is also on the prototype. What happens if the helper reads it while looking for methods?
Show the reasoning
  • myBind: fn.apply(thisValue, args) (or call) calls with a chosen this. A closure remembers fn, thisValue and fixedArgs; the returned arrow combines the fixed arguments with the new ones. An arrow is right for the returned function, because it must ignore its own call-site this.
  • Where methods live: on the prototype, Object.getPrototypeOf(instance), as non-enumerable properties. Object.keys(instance) lists only own enumerable data such as gateway. Use Object.getOwnPropertyNames(prototype).
  • Inheritance: walk up the prototype chain with Object.getPrototypeOf until you reach Object.prototype, and stop there, because binding toString and friends onto every instance is pointless. A child's method must win over the parent's method of the same name, so skip names you have already bound.
  • Not bound: constructor, and anything that is not a function.
  • Getters: reading prototype[name] runs a getter with the prototype as this, which can throw or give nonsense. Read the descriptor with Object.getOwnPropertyDescriptor and only bind when it has a function value.

Build: bind and bindAll

binding.js
export function myBind(fn, thisValue, ...fixedArgs) {
  if (typeof fn !== "function") throw new TypeError("myBind needs a function");
  const bound = (...laterArgs) => fn.apply(thisValue, [...fixedArgs, ...laterArgs]);
  Object.defineProperty(bound, "name", { value: `bound ${fn.name}` });
  return bound;
}

export function bindAll(instance) {
  const seen = new Set();
  let proto = Object.getPrototypeOf(instance);
  while (proto !== null && proto !== Object.prototype) {
    for (const name of Object.getOwnPropertyNames(proto)) {
      if (name === "constructor" || seen.has(name)) continue;
      seen.add(name);
      const descriptor = Object.getOwnPropertyDescriptor(proto, name);
      if (typeof descriptor.value === "function") {
        instance[name] = descriptor.value.bind(instance);
      }
    }
    proto = Object.getPrototypeOf(proto);
  }
  return instance;
}

myBind is a closure (it remembers fn, thisValue and fixedArgs) around apply. Because bound is an arrow, calling it as a method or with call cannot change what it passes on, which is exactly the "cannot be rebound" behaviour of the real bind. (The real one also supports new; this version does not, because arrows cannot be constructed.) bindAll walks the prototype chain as the reasoning block planned. The seen set makes sure a child's method is bound instead of the parent's version it overrides.

main.js
import { bindAll, myBind } from "./binding.js";

class PaymentService {
  constructor(gateway) {
    this.gateway = gateway;
    this.processed = 0;
  }
  charge(job) {
    this.processed += 1;
    return this.gateway.charge(job.orderId, job.amountKobo);
  }
  get summary() {
    return `${this.processed} processed`;
  }
}

class RetryingPaymentService extends PaymentService {
  charge(job) {
    return `${super.charge(job)} (with retry)`;
  }
  refund(job) {
    return `refunded ${job.orderId}`;
  }
}

function runJobs(jobs, handler) {
  return jobs.map((job) => handler(job));
}

const gateway = { charge: (orderId, kobo) => `charged ₦${kobo / 100} for ${orderId}` };
const jobs = [{ orderId: "ORD-7", amountKobo: 1170000 }, { orderId: "ORD-8", amountKobo: 500000 }];

const payments = bindAll(new RetryingPaymentService(gateway));
console.log(runJobs(jobs, payments.charge));
console.log(runJobs(jobs, payments.refund));
console.log(payments.summary, Object.keys(payments));

function fee(percent, amountKobo) {
  return `${this.bank}: ₦${(amountKobo * percent) / 10000}`;
}
const gtFee = myBind(fee, { bank: "GTBank" }, 0.5);
console.log(gtFee(1000000), gtFee.name);
console.log(gtFee.call({ bank: "Other" }, 1000000));
Output of node main.js and of the browser terminal
[
  'charged ₦11700 for ORD-7 (with retry)',
  'charged ₦5000 for ORD-8 (with retry)'
]
[ 'refunded ORD-7', 'refunded ORD-8' ]
2 processed [ 'gateway', 'processed', 'charge', 'refund' ]
GTBank: ₦50 bound fee
GTBank: ₦50

The runner gets plain functions and calls them without a dot, and everything works: charge is the child's version (with retry), it still reaches the parent's through super, and the summary getter was left alone and still counts correctly.

Testing the behaviour

binding.test.js
import { bindAll, myBind } from "./binding.js";

function check(label, actual, expected) {
  console.log(`${Object.is(actual, expected) ? "PASS" : "FAIL"} ${label} -> ${actual}`);
}

function whoAmI(greeting = "hi") {
  return `${greeting} ${this?.name}`;
}
const ada = { name: "Ada" };
const bound = myBind(whoAmI, ada);

check("uses the bound this", bound(), "hi Ada");
check("ignores call", bound.call({ name: "Chidi" }), "hi Ada");
check("ignores method call", { name: "Chidi", bound }.bound(), "hi Ada");
check("passes arguments", bound("hello"), "hello Ada");
check("fixes leading arguments", myBind(whoAmI, ada, "welcome")(), "welcome Ada");
check("names the function", bound.name, "bound whoAmI");

class Counter {
  count = 0;
  increment() {
    this.count += 1;
    return this.count;
  }
  get double() {
    return this.count * 2;
  }
}
const counter = bindAll(new Counter());
const { increment } = counter;
increment();
increment();
check("detached method works", counter.count, 2);
check("getter still a getter", counter.double, 4);
check("instance holds its own bound copy", Counter.prototype.increment === increment, false);
check("other instances unaffected", Object.hasOwn(new Counter(), "increment"), false);
Output of node binding.test.js and of the browser terminal
PASS uses the bound this -> hi Ada
PASS ignores call -> hi Ada
PASS ignores method call -> hi Ada
PASS passes arguments -> hello Ada
PASS fixes leading arguments -> welcome Ada
PASS names the function -> bound whoAmI
PASS detached method works -> 2
PASS getter still a getter -> 4
PASS instance holds its own bound copy -> false
PASS other instances unaffected -> false

In production

  • Prefer the simplest fix. A wrapper arrow where you pass the callback is explicit and costs nothing. Reach for bind or bindAll when the same object's methods are handed out in many places, for example registering a whole controller's methods as routes.
  • Or avoid this entirely. A service built by a factory function with closures (Closures in depth) has nothing to lose. Many backend codebases use classes for structure and dependency injection, and wrappers or factories at the edges.
  • Know what binding costs. bindAll adds one function per method per instance. For a few long-lived services that is nothing; for a million short-lived objects it is real memory.
  • Strict mode everywhere. A lost this in sloppy code reads and writes globals silently. ES modules, which ZudoJS projects use, are strict automatically; a CommonJS file needs "use strict" at the top.

Practice

TRY IT YOURSELF

Predict and fix a timer

This notifier should print the customer's name after a short delay, but it prints something else. Predict the output, explain it with the call-site rule, and fix it without changing the class.

timer-bug.js
class Notifier {
  constructor(customer) {
    this.customer = customer;
  }
  remind() {
    console.log(`Reminder for ${this?.customer}`);
  }
}

const notifier = new Notifier("Ada");
setTimeout(notifier.remind, 0);
Output of node timer-bug.js and of the browser terminal
Reminder for undefined
Show a solution

setTimeout receives only the function and calls it itself, so this is not notifier. (In Node.js it is the timer object, which has no customer; in a browser it is window.) Give the call its dot back with a wrapper, or bind:

timer-fix.js
class Notifier {
  constructor(customer) {
    this.customer = customer;
  }
  remind() {
    console.log(`Reminder for ${this.customer}`);
  }
}

const notifier = new Notifier("Ada");
setTimeout(() => notifier.remind(), 0);
setTimeout(notifier.remind.bind(notifier), 0);
Output of node timer-fix.js and of the browser terminal
Reminder for Ada
Reminder for Ada

TRY IT YOURSELF

Borrow a method

A receipt object has a total() method that adds up this.lines. A plain object { lines: [500, 1500] } from an old API has no methods. Without copying the method onto it, use call to compute its total, then use bind to make a reusable oldApiTotal function.

Show a solution
borrow.js
const receipt = {
  lines: [850000, 320000],
  total() {
    return this.lines.reduce((sum, kobo) => sum + kobo, 0);
  },
};

const fromOldApi = { lines: [500, 1500] };
console.log(receipt.total.call(fromOldApi));

const oldApiTotal = receipt.total.bind(fromOldApi);
fromOldApi.lines.push(250);
console.log(oldApiTotal(), receipt.total());
Output of node borrow.js and of the browser terminal
2000
2250 1170000

bind fixes which object is used, not a copy of it, so the bound function sees the line added later. receipt.total() still uses receipt: borrowing never changes the original method.

TRY IT YOURSELF

Callbacks inside a method

Finish priceList() so it returns lines like "Rice 5kg: ₦8500.00" using this.format for each product. Write it once with an arrow callback and once with a function callback plus map's thisArg.

Show a solution
price-list.js
const shop = {
  symbol: "₦",
  products: [{ name: "Rice 5kg", kobo: 850000 }, { name: "Oil 1L", kobo: 320000 }],
  format(kobo) {
    return `${this.symbol}${(kobo / 100).toFixed(2)}`;
  },
  priceList() {
    return this.products.map((p) => `${p.name}: ${this.format(p.kobo)}`);
  },
  priceListThisArg() {
    return this.products.map(function (p) {
      return `${p.name}: ${this.format(p.kobo)}`;
    }, this);
  },
};

console.log(shop.priceList());
console.log(shop.priceListThisArg());
Output of node price-list.js and of the browser terminal
[ 'Rice 5kg: ₦8500.00', 'Oil 1L: ₦3200.00' ]
[ 'Rice 5kg: ₦8500.00', 'Oil 1L: ₦3200.00' ]

Both work. The arrow version is the one most teams prefer, because a reader sees at once that this is the method's this; the thisArg version depends on a second argument that is easy to miss.

Recap

  • this is a hidden parameter set by the call, not by where the function is written. Find the call site.
  • Method call: the object before the last dot. Plain call: undefined in strict code, globalThis in sloppy code. new: the new object.
  • Storing, destructuring or passing a method as a callback detaches it. Fix it with a wrapper arrow, bind, or a thisArg.
  • call and apply call now with a chosen this; bind returns a permanently bound function and can fix leading arguments too. Only new overrides a binding.
  • Arrow functions use the surrounding this and cannot be rebound: perfect callbacks inside methods, wrong as methods in object literals.
  • In classes, choose between wrappers at the call site, bind in the constructor, and arrow fields; arrow fields are per instance and invisible to super.
  • DOM and EventEmitter call function listeners with the object they are registered on; prefer event.currentTarget and event.target.

Next: Prototypes in depth, where you follow the prototype chain step by step and see exactly what new and class do.

Test yourself

Five questions, picked at random from this lesson's question bank. Some ask you to choose an answer, some to predict what code prints, and some to write code and run it in the terminal. Get 4 of 5 right to pass. If you don't, read the explanations and try again: you get 5 different questions.