Search code examples
javascriptcallbackthises6-promise

Javascript Promisification, why use "call"?


I want to understand why in the below example, the "call" method was used.

loadScript is a function that appends a script tag to a document, and has an optional callback function.

promisify returns a wrapper function that in turn returns a promise, effectively converting `loadScript' from a callback-based function to a promise based function.

function promisify(f) {
  return function (...args) { // return a wrapper-function 
    return new Promise((resolve, reject) => {
      function callback(err, result) { // our custom callback for f 
        if (err) {
          reject(err);
        } else {
          resolve(result);
        }
      }

      args.push(callback); // append our custom callback to the end of f arguments

      f.call(this, ...args); // call the original function
    });
  };
}

// usage:
let loadScriptPromise = promisify(loadScript);
loadScriptPromise(...).then(...);

loadScript():

function loadScript(src, callback) {
  let script = document.createElement("script");
  script.src = src;

  script.onload = () => callback(null, script);
  script.onerror = () => callback(new Error(`Script load error for ${src}`));

  document.head.append(script);
}

I understand that call is used to force a certain context during function call, but why not use just use f(...args) instead of f.call(this, ...args)?


Solution

  • promisify is a general-purpose function. Granted, you don't care about this in loadScript, but you would if you were using promisify on a method. So this works:

    function promisify(f) {
      return function (...args) { // return a wrapper-function 
        return new Promise((resolve, reject) => {
          function callback(err, result) { // our custom callback for f 
            if (err) {
              reject(err);
            } else {
              resolve(result);
            }
          }
    
          args.push(callback); // append our custom callback to the end of f arguments
    
          f.call(this, ...args); // call the original function
        });
      };
    }
    
    class Example {
        constructor(a) {
            this.a = a;
        }
        method(b, callback) {
            const result = this.a + b;
            setTimeout(() => callback(null, result), 100);
        }
    }
    
    (async () => {
        try {
            const e = new Example(40);
            const promisifiedMethod = promisify(e.method);
            const result = await promisifiedMethod.call(e, 2);
            console.log(result);
        } catch (error) {
            console.error(error);
        }
    })();

    That wouldn't work if promisify didn't use the this that the function it returns receives:

    function promisifyNoCall(f) {
      return function (...args) { // return a wrapper-function 
        return new Promise((resolve, reject) => {
          function callback(err, result) { // our custom callback for f 
            if (err) {
              reject(err);
            } else {
              resolve(result);
            }
          }
    
          args.push(callback); // append our custom callback to the end of f arguments
    
          f(...args); // call the original function *** changed
        });
      };
    }
    
    class Example {
        constructor(a) {
            this.a = a;
        }
        method(b, callback) {
            const result = this.a + b;
            setTimeout(() => callback(null, result), 100);
        }
    }
    
    (async () => {
        try {
            const e = new Example(40);
            const promisifiedMethod = promisifyNoCall(e.method);
            const result = await promisifiedMethod.call(e, 2);
            console.log(result);
        } catch (error) {
            console.error(error);
        }
    })();