ESC

Type to search the knowledge base.

Function.prototype.bind Polyfill

Implement Function.prototype.bind: bound this, prepended arguments, and new Bound() that still uses the original function prototype.

intermediate5 min read
  • javascript
  • this
  • polyfill
  • interview

A Function.prototype.bind polyfill that only closes over thisArg and returns fn.apply(thisArg, args) looks done until someone types new Bound(). Native bind still constructs: the bound this is ignored, prepended arguments still land first, and the instance’s [[Prototype]] is the original function’s .prototype, not some empty object you allocated by accident. The other miss is wrapping with an arrow. Arrows have no new.target and cannot be constructors, so new Bound() throws even when the target is a perfectly good constructor.

Write a standalone myBind(fn, thisArg, ...boundArgs). Putting it on Function.prototype is interview convention and mutates a shared builtin.

Key takeaways

  • Ordinary calls use the bound this. Later call / apply on the bound function cannot replace it.
  • Bound arguments are prepended. The later call’s arguments are concatenated after them.
  • new Bound() ignores bound this and constructs through the original function’s prototype.
  • An arrow target already has lexical this. Bind still prepends args; it does not rebind this.
  • The wrapper must be a function, not an arrow, or new is dead on arrival.

this, prepended args, and new on a bound function

bind returns a new function. It does not call the target. Spec shape, trimmed for a whiteboard: store the target, the bound this, and the bound argument list. On [[Call]], invoke the target with that this and boundArgs + args. On [[Construct]], ignore the bound this, forward the concatenated args, and let the original constructor allocate.

function myBind(fn, thisArg, ...boundArgs) {
  if (typeof fn !== 'function') {
    throw new TypeError('myBind called on a non-function');
  }

  function bound(...args) {
    const allArgs = boundArgs.concat(args);
    if (new.target) {
      return Reflect.construct(fn, allArgs, new.target);
    }
    return fn.apply(thisArg, allArgs);
  }

  if (fn.prototype) {
    bound.prototype = Object.create(fn.prototype);
  }
  return bound;
}

function greet(greeting, mark) {
  return greeting + ', ' + this.role + mark;
}

const boundHi = myBind(greet, { role: 'editor' }, 'Hi');
boundHi('!'); // 'Hi, editor!'
boundHi.call({ role: 'ignored' }, '?'); // still 'Hi, editor?'

Reflect.construct(fn, allArgs, new.target) is the construct path. When the caller wrote new bound(), new.target is bound. Wiring bound.prototype to Object.create(fn.prototype) is what makes the instance inherit the original methods. Native bind is an exotic function: it has no .prototype of its own and its [[Construct]] replaces newTarget with the original function when they match. The Object.create line is the interview stand-in for that prototype link.

If you skip Reflect.construct and only fn.apply(this, allArgs) under new, class constructors throw (Class constructor cannot be invoked without new), and a constructor that returns an object is easy to get wrong. Mention the new operator: if the constructor returns an object or a function, that value wins; primitives and null do not.

Call vs construct

Call shape this inside the target Arguments Prototype of the result
bound(x) bound thisArg prepended + x n/a (whatever the target returns)
bound.call(other, x) still bound thisArg prepended + x n/a
new bound(x) the new instance prepended + x original fn.prototype
Arrow target, any of the above lexical this of the arrow prepended + x new throws: arrows are not constructors
function Widget(id, label) {
  this.id = id;
  this.label = label;
}
Widget.prototype.describe = function describe() {
  return this.id + ':' + this.label;
};

const BoundWidget = myBind(Widget, { id: 'bound-this' }, 'w1');
const w = new BoundWidget('panel');

w.id;                                    // 'w1' (prepended), not 'bound-this'
w.label;                                 // 'panel'
w.describe();                            // 'w1:panel'
Object.getPrototypeOf(w) === BoundWidget.prototype; // true in this polyfill
w instanceof Widget;                     // true

The object { id: 'bound-this' } never becomes this under new. That is the whole construct test. An ES5-only fallback uses this instanceof bound instead of new.target. It lies if someone does bound.call(existingInstance) where existingInstance was created by new bound. Prefer new.target and say so.

Arrows and partial args

bind on an arrow does not change this. It still returns a new function and still prepends arguments, which is why people use it as a poor man’s partial:

const add = (a, b, c) => a + b + c;
const addTen = myBind(add, { unused: true }, 10);
addTen(1, 2); // 13, thisArg ignored by the arrow

That is partial application, not this control. Real this work wants an ordinary function. See arrow functions and currying and partial application.

Footguns in a Function.prototype.bind polyfill

Returning an arrow wrapper fails new and fails prototype wiring. The bound function has to be constructable when the target is.

bound.prototype = fn.prototype (same reference, no Object.create) means BoundWidget.prototype.extra = 1 mutates Widget.prototype. Native code does not share that object that way. Object.create is the isolation.

Native bind also sets length to max(0, fn.length - boundArgs.length) and name to "bound " + fn.name. Skip those unless asked. Do not copy other own properties off fn; a bound function is not a clone.

bind is sticky for this on ordinary calls. bound.call(other) still uses the bound thisArg. The only thing that outranks it is new, which is this-binding rule 1.

Assigning Function.prototype.myBind = function (thisArg, ...boundArgs) { return myBind(this, thisArg, ...boundArgs); } is the prototype form. It mutates a builtin. Prefer the standalone function unless they insist.

Interview answer (out loud)

“bind returns a new function that stores thisArg and any leading arguments. On a normal call I apply the original with that this and the concatenated args. Later call and apply cannot override the bound this. On new I ignore the bound this, construct through the original function, and link the instance to the original prototype, usually with Object.create on the wrapper’s prototype or Reflect.construct. The wrapper is a regular function so new.target works. An arrow target keeps lexical this; bind still prepends args.”

Further reading

Related guides