Skip to the code
cf.

Function.prototype.call

MediumPolyfills

Write Function.prototype.myCall(thisArg, ...args), which works like call: it runs the function it's called on with this set to thisArg and the given arguments, and returns the function's result. Don't run it with call, apply, bind, or Reflect.apply.

Match what a non-strict function sees with the real call. When thisArg is null or undefined, this is globalThis. A primitive thisArg is wrapped in its object, so 5 becomes a Number object and "" a String object.

Leave thisArg as you found it: no extra keys afterwards, even when the function throws (the error still reaches the caller), and its existing properties untouched. If myCall is used on something that isn't a function, throw a TypeError.

function greet(greeting, mark) {
  return greeting + ", " + this.name + mark;
}
greet.myCall({ name: "Ada" }, "Hi", "!"); // "Hi, Ada!"

What the tests check

  1. calls the function with thisArg as this and the arguments
    const fn = spy();
    const user = { name: "Ada" };
    fn.myCall(user, 1, 2);
    expect(fn).toHaveBeenCalledTimes(1);
    expect(fn).toHaveBeenCalledWith(1, 2);
    expect(fn.calls[0].this).toBe(user);
    
  2. returns the function's result
    function add(a, b) {
      return this.base + a + b;
    }
    expect(add.myCall({ base: 10 }, 1, 2)).toBe(13);
    
  3. uses globalThis when thisArg is null or undefined
    function getThis() {
      return this;
    }
    expect(getThis.myCall(null)).toBe(globalThis);
    expect(getThis.myCall(undefined)).toBe(globalThis);
    expect(getThis.myCall()).toBe(globalThis);
    
  4. wraps a primitive thisArg in its object, even a falsy one
    function getThis() {
      return this;
    }
    const five = getThis.myCall(5);
    expect(five).toBeInstanceOf(Number);
    expect(five.valueOf()).toBe(5);
    const zero = getThis.myCall(0);
    expect(zero).toBeInstanceOf(Number);
    expect(zero.valueOf()).toBe(0);
    expect(getThis.myCall("")).toBeInstanceOf(String);
    
  5. leaves no extra keys on thisArg
    const user = { name: "Ada" };
    function read() {
      return this.name;
    }
    read.myCall(user);
    expect(Reflect.ownKeys(user)).toEqual(["name"]);
    
  6. doesn't clash with a property of the same name on thisArg
    const box = { fn: "mine" };
    function readFn() {
      return this.fn;
    }
    expect(readFn.myCall(box)).toBe("mine");
    expect(box.fn).toBe("mine");
    
  7. cleans up and rethrows when the function throws
    const user = {};
    function fail() {
      throw new RangeError("boom");
    }
    expect(() => fail.myCall(user)).toThrow(RangeError);
    expect(Reflect.ownKeys(user)).toEqual([]);
    
  8. throws a TypeError when used on something that isn't a function
    const notAFunction = { myCall: Function.prototype.myCall };
    expect(() => notAFunction.myCall({})).toThrow(TypeError);
    
  9. works without call, apply, bind, or Reflect.apply
    function add(a, b) {
      return this.base + a + b;
    }
    const builtIns = { call: Function.prototype.call, apply: Function.prototype.apply, bind: Function.prototype.bind };
    const reflectApply = Reflect.apply;
    // Only uses on add throw, so other code, like console.log, still works.
    for (const name of ["call", "apply", "bind"]) {
      Function.prototype[name] = function (...args) {
        if (this === add) throw new Error("myCall used " + name);
        return reflectApply(builtIns[name], this, args);
      };
    }
    Reflect.apply = (target, thisArg, args) => {
      if (target === add) throw new Error("myCall used Reflect.apply");
      return reflectApply(target, thisArg, args);
    };
    let result;
    try {
      result = add.myCall({ base: 1 }, 2, 3);
    } finally {
      Function.prototype.call = builtIns.call;
      Function.prototype.apply = builtIns.apply;
      Function.prototype.bind = builtIns.bind;
      Reflect.apply = reflectApply;
    }
    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).