1

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)?

Elbasel
  • 11
  • 4
  • 5
    "*f(...args) instead of f.call(this, ...args)?*" because the value of `this` will not be preserved. – VLAZ Jul 24 '22 at 14:07
  • yes, but why do we need to preserve the value of `this` in this particular situation? – Elbasel Jul 24 '22 at 14:09
  • 4
    @Elbasel - Because `promisify` is a **general-purpose** function, and you might use it on a method. – T.J. Crowder Jul 24 '22 at 14:10
  • Duplicate of http://stackoverflow.com/questions/20279484/how-to-access-the-correct-this-context-inside-a-callback ? – T.J. Crowder Jul 24 '22 at 14:10
  • 1
    @T.J.Crowder I think it's basically a duplicate, but this is an interesting corner case. – Pointy Jul 24 '22 at 14:11
  • Note that `promisify` only works on functions whose callbacks take an error and the result as arguments. It was created specifically for Node functions which follow that pattern. – Heretic Monkey Jul 24 '22 at 14:12
  • @Pointy it is a duplicate of many other questions that have asked why a generic utility will preserve `this`. There have been at least a few specifically asked for `debounce`, for example. – VLAZ Jul 24 '22 at 14:13
  • @VLAZ yes I know, but I think something specifically about `promisify()` is worth having for search purposes. That's just my opinion; now that TJ gave an answer, closing it as a dup seems fine to me. – Pointy Jul 24 '22 at 14:46

1 Answers1

1

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);
    }
})();
T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
  • Thank you, it's a very detailed answer. Can I bother you for some references that will help me understand your usage of `this` and `call`? – Elbasel Jul 24 '22 at 15:44
  • @Elbasel - MDN is a great resource: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/call Also [this question's answers](http://stackoverflow.com/questions/20279484/how-to-access-the-correct-this-context-inside-a-callback) and [this one](https://stackoverflow.com/questions/3127429/how-does-the-this-keyword-work). Happy coding! – T.J. Crowder Jul 24 '22 at 15:58