Search code examples
javascriptarraysecmascript-6callbackcall

Why javascript call method fails to execute on array prototype method


I am playing with JS features to improve my understanding of the language.

In below example, I was trying to use call method to initiate Array function on given array object. I know alternatives to get the job done; however, I am curious why the code I have written does not work?.

Thank you!

Code

const f_timeit = (f) => {
  return function(...args) {
    util.p(`Entering function ${f.name}`);
    const start_time = Date.now();
    try {
      return f(...args);
    } finally {
      util.p(`Exiting ${f.name} after ${Date.now() - start_time}ms`);
    }
  }
};

const rarr = [...Array(10000000)].map(e => ~~(Math.random() * 20));

// following works
console.log(f_timeit((arr) => {
  return arr.reduce((acc, val) => acc + val, 0);
})(rarr));

// following fails
util.p(f_timeit(Array.prototype.reduce.call)(rarr, (acc, val) => acc + val, 0));

Error:

        return f(...args);
               ^
TypeError: f is not a function

Solution

  • The error message is a bit misleading, it's not that "f is not a function" but rather that f is the Function.prototype.call method that was invoked on something that is not a function - in particular undefined. This is due to how the this argument works, and in particular because accessing methods on an object does not automatically bind them.

    So various ways to achieve what you want are

    f_timeit(() => rarr.reduce((acc, val) => acc + val, 0))()
    f_timeit((ctx) => ctx.reduce((acc, val) => acc + val, 0))(rarr)
    f_timeit((ctx, reducer) => ctx.reduce(reducer, 0))(rarr, (acc, val) => acc + val)
    f_timeit((ctx, reducer, init) => ctx.reduce(reducer, init))(rarr, (acc, val) => acc + val, 0)
    f_timeit((ctx, ...args) => ctx.reduce(...args))(rarr, (acc, val) => acc + val, 0)
    f_timeit((ctx, ...args) => Array.prototype.reduce.apply(ctx, args))(rarr, (acc, val) => acc + val, 0)
    f_timeit((ctx, ...args) => Array.prototype.reduce.call(ctx, ...args))(rarr, (acc, val) => acc + val, 0)
    f_timeit((...args) => Array.prototype.reduce.call(...args))(rarr, (acc, val) => acc + val, 0)
    // f_timeit(Array.prototype.reduce.call)(rarr, (acc, val) => acc + val, 0) - no!
    f_timeit(Array.prototype.reduce.call.bind(Array.prototype.reduce))(rarr, (acc, val) => acc + val, 0) // yes!
    f_timeit(Function.prototype.call.bind(Array.prototype.reduce))(rarr, (acc, val) => acc + val, 0)
    

    However, I would generally recommend that you change f_timeit to respect the this value passed when the returned function is called:

    const f_timeit = (f) => {
      return function(...args) {
        util.p(`Entering function ${f.name}`);
        const start_time = Date.now();
        try {
          return f.call(this, ...args);
    //                  ^^^^
          return f.apply(this, args); // or equivalently
        } finally {
          util.p(`Exiting ${f.name} after ${Date.now() - start_time}ms`);
        }
      }
    };
    

    That way, you can use

    f_timeit(Array.prototype.reduce).call(rarr, (acc, val) => acc + val, 0)