Skip to the code
cf.

Function.prototype.bind

Write Function.prototype.myBind(thisArg, ...presetArgs), which works like bind without calling it (call and apply are fine). It returns a new function, the bound function, and doesn't call anything yet.

Calling the bound function calls the original with this set to thisArg, and with presetArgs followed by the arguments of this call. It returns the original's result. The bound this is fixed: calling the bound function as a method, or with call, doesn't change it.

Calling the bound function with new ignores thisArg: it does what new on the original would do with the same arguments. So the result is an instance of the original, and if the original returns an object, that object is the result.

If myBind is used on something that isn't a function, throw a TypeError.

const user = { name: "Ada" };
function greet(greeting, mark) {
  return greeting + ", " + this.name + mark;
}
const hi = greet.myBind(user, "Hi");
hi("!");                  // "Hi, Ada!"
 
function Point(x, y) {
  this.x = x;
  this.y = y;
}
const OnYAxis = Point.myBind({ ignored: true }, 0);
const p = new OnYAxis(5); // Point { x: 0, y: 5 }
p instanceof Point;       // true

What the tests check

  1. returns a function that calls the original with thisArg
    const fn = spy();
    const user = { name: "Ada" };
    const bound = fn.myBind(user);
    expect(typeof bound).toBe("function");
    expect(fn).not.toHaveBeenCalled();
    bound();
    expect(fn).toHaveBeenCalledTimes(1);
    expect(fn.calls[0].this).toBe(user);
    
  2. puts the preset arguments before the call's arguments
    const fn = spy();
    fn.myBind(null, 1, 2)(3, 4);
    expect(fn).toHaveBeenCalledWith(1, 2, 3, 4);
    
  3. returns the original's result
    function add(a, b) {
      return this.base + a + b;
    }
    expect(add.myBind({ base: 10 }, 1)(2)).toBe(13);
    
  4. keeps thisArg however the bound function is called
    const fn = spy();
    const user = { name: "Ada" };
    const bound = fn.myBind(user);
    const other = { method: bound };
    other.method();
    bound.call({ name: "someone else" });
    expect(fn.calls[0].this).toBe(user);
    expect(fn.calls[1].this).toBe(user);
    
  5. ignores thisArg when called with new
    function Point(x, y) {
      this.x = x;
      this.y = y;
    }
    const ignored = {};
    const Bound = Point.myBind(ignored, 1);
    const point = new Bound(2);
    expect(point).not.toBe(ignored);
    expect(point.x).toBe(1);
    expect(point.y).toBe(2);
    expect(Object.keys(ignored)).toEqual([]);
    
  6. makes instances of the original with new
    function Point(x, y) {
      this.x = x;
      this.y = y;
    }
    Point.prototype.sum = function () {
      return this.x + this.y;
    };
    const Bound = Point.myBind(null, 1);
    const point = new Bound(2);
    expect(point).toBeInstanceOf(Point);
    expect(point.sum()).toBe(3);
    
  7. with new, returns an object the original returns
    const made = { made: true };
    function Factory() {
      this.ignored = true;
      return made;
    }
    const Bound = Factory.myBind({});
    expect(new Bound()).toBe(made);
    
  8. throws a TypeError when used on something that isn't a function
    const notAFunction = { myBind: Function.prototype.myBind };
    expect(() => notAFunction.myBind({})).toThrow(TypeError);
    
  9. works without the built-in bind
    const builtIn = Function.prototype.bind;
    Function.prototype.bind = function () {
      throw new Error("myBind called the built-in bind");
    };
    let result;
    try {
      const add = function (a, b) {
        return this.base + a + b;
      };
      result = add.myBind({ base: 1 }, 2)(3);
    } finally {
      Function.prototype.bind = builtIn;
    }
    expect(result).toBe(6);
    

Learn more

solution.jsSaved in this browser

Run tests runs every test on your code, here in your browser. The problem is solved when they all pass.

Ctrl + Enter runs the tests (⌘ + Enter on a Mac).