I have a question regarding ‘this’ in JavaScript higher-order-function's callback.
I've been exploring the following code - the goal is conversion of a function that accepts a callback into a function returning a promise.
Source: https://javascript.info/promisify
A function that accepts a callback:
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);
}
Now, the author uses a higher-order-function that will accept the stated above function as a callback and do the promisification:
function promisify(f) {
return function (...args) {
return new Promise((resolve, reject) => {
function callback(err, result) {
if (err) {
return reject(err);
} else {
resolve(result);
}
}
args.push(callback);
f.call(this, ...args);
});
};
};
// usage:
let loadScriptPromise = promisify(loadScript);
loadScriptPromise('path/script.js').then(...);
The thing that I don't understand is:
Why do we call the f function in this way:
f.call(this, ...args); ?
What will ‘this’ be in this case?
Why can't we just call it like this: f(...args); ?
I know that in order to track down what a callback’s ‘this’ is pointing to, you need to inspect the higher order function enclosing it...
But I can't get why in this case do we have to state the callback’s ‘this’ explicitly?
Thank you for the help!
Why do we call the f function in this way: f.call(this, ...args); ?
What will ‘this’ be in this case? Why can't we just call it like this: f(...args); ?
Let me answer the What will ‘this’ be in this case? part first:
We don't know (in general) and that is why .call is used, but I will get to that.
promisify is supposed to "seamlessly" wrap an existing function. That means that calling either f or promisify(f) should return the same result.
The value of this depends on how a function is called. promisify can't know how the new wrapper function is going to be called, nor does it know whether the wrapped function uses this or not. Therefore it needs to assume that this is going to be significant and has to call the wrapped function in a way that sets this correctly.
The only way to call a function and explicitly set the this value is via .call or .apply. If the function was called as f(...args) then this inside f would either be the global object or undefined.
Here is a simpler wrapper function that demonstrates the issue:
function wrapWithThis(f) {
return function(...args) {
f.call(this, ...args);
}
}
function wrapWithoutThis(f) {
return function(...args) {
f(...args);
}
}
function wrapMe() {
console.log(this.foo);
}
const obj = {
foo: 42,
withThis: wrapWithThis(wrapMe),
withoutThis: wrapWithoutThis(wrapMe),
};
obj.withThis();
obj.withoutThis();
Having said all that, specific to your example, given that loadScript doesn't use this, it wouldn't make a difference if f.call(this, ...) or f(...) was used.
Related
I have started learning promises in JS, and there are (resolve, reject) which are passed to promise. After some actions in the function you call resolve() if there is everything OK and reject() if not. So, that's why I am asking about this.
Can we actually call the parameters of a function inside of it? Actually, if I put () after the parameter inside a function in VS Code, it is highlighted as a function.
Sorry, if there is some issues with explaining. I am not a native speaker...
There is an example: (what is stack() actually?)
const iGotIt = true;
const f = (stack, overflow) => {
if (iGotIt) {
stack({
newKnowledge: 'Finally',
message: ':)'
})
}
}
It depends on the function parameter, since it may not be a function.
For example:
myFunction(data, callback){
const result = do_something_with_data();
callback(result);
}
myFunction(0, result => {
do_something_with_result();
}); //works
myFunction(1, console.log); //works
myFunction(2, 0); //doesn't work, 0 is not a function.
what is stack() actually?
It's the first parameter in the f function.
The f function expects the first parameter to itself also be a function, and one which accepts at least one parameter which is an object.
But what stack actually is depends on what gets passed to f.
For example, consider this usage in which a function is passed as the first parameter:
const iGotIt = true;
const f = (stack, overflow) => {
if (iGotIt) {
stack({
newKnowledge: 'Finally',
message: ':)'
})
}
}
f(x => console.log(x));
Then invoking stack() invokes the function that was provided. Alternatively, consider this example:
const iGotIt = true;
const f = (stack, overflow) => {
if (iGotIt) {
stack({
newKnowledge: 'Finally',
message: ':)'
})
}
}
f();
This fails because nothing was provided, so stack is undefined. Or this example:
const iGotIt = true;
const f = (stack, overflow) => {
if (iGotIt) {
stack({
newKnowledge: 'Finally',
message: ':)'
})
}
}
f('test');
This also fails, because a string is not a function and can't be invoked like one.
I'm desperately trying to recover the value of a callback function but I have no idea how to do that. I have a function where I execute this code:
if (final.error !== undefined) {
console.log("Initial authentication:", final.error_description, "Please refresh the authentication grant");
extAuthCallback(84);
} else {
tokens.set('access_token', final.access_token)
.set('expires_in', final.expires_in)
.set('refresh_token', final.refresh_token)
.set('refresh_date', moment())
.write()
extAuthCallback(1);
}
});
Who performs this function:
function extAuthCallback(result) {
return result;
}
And which is called by this variable:
let authentication = auth.extAuth(access_token, auth.extAuthCallback);
I would like my `authentication' variable to take the value returned in the callback, and I have no idea how to do that. Returning the callback function to my original function doesn't work.
You could use a promise, would need to use an async function as well though.
function asyncExtAuth(access_token) {
return new Promise(resolve => {
auth.extAuth(access_token, resolve);
});
}
let authentication = await asyncExtAuth(access_token);
I need to understand how callback functions works.
I wrote this little script with 2 functions :
function fn1(callback) {
alert('fn1');
callback();
}
function fn2() {
alert('fn2');
}
fn1(fn2); //display 'fn1' then 'fn2'
Now, I want to update my script with a "fn3" function in order to display 'fn1' then 'fn2' then 'fn3'. I tried this :
function fn1(callback) {
alert('fn1');
callback();
}
function fn2(callback) {
alert('fn2');
callback();
}
function fn3() {
alert('fn3');
}
fn1(fn2(fn3));
but it dispay 'fn2', then 'fn3', then 'fn1', then log an error ("callback is not a function").
Any idea ? what's wrong ?
Thanks in advance, Florent.
In order to execute f1 then f2 then f3 you need to create a callback function in order to make sure a function will be executed in steps.
wrong:
fn1(fn2(fn3))) // this first call f2 with f3 as parameter then calls f1 with its result
right:
fn1(function () { // call f1
fn2(function () { // then call f2 after f1
fn3(); // then call f3 after f2
})
})
This sounds more like you're looking for promises
function fn1(){
return new Promise(resolve => {
alert("fn1");
resolve()
});
}
function fn2(){
return new Promise(resolve => {
alert("fn2");
resolve()
});
}
function fn3(){
return new Promise(resolve => {
alert("fn3");
resolve()
});
}
fn1().then(fn2).then(fn3);
fn3
You take the value fn3 which is a function.
fn2(fn3)
You pass that value as the argument to fn2, another function, which you call.
function fn2(callback) { alert('fn2'); callback(); }
You alert, then you call the argument (the function you got from fn3) as a function (I'll skip over the details of what it does) and then return undefined (since you have no return statement).
fn1(fn2(fn3));
Since fn2(fn3) returns undefined this is the same as fn1(undefined).
function fn1(callback) { alert('fn1'); callback(); }
You alert, then try to call undefined as a function, which it isn't, so it errors.
How to call 3 or 4 callback functions
Probably… rewrite the functions so they make use of Promises instead of plain old callbacks.
But there is no point in using callbacks for your example at all, and the best solution to a real problem will depend on what that problem is.
fn1(fn2(fn3))
Here we have a call to function fn1 with 1 argument which is return value of function fn2. This statement is evaluated so that fn2 is first called with its parameter fn3 and then the return value is passed to fn1. fn2 calls its parameter. This is why you get "unexpected" order of alerts.
fn2 does not return anything so that is why you get error from fn1.
I'd like to create a logging decorator around jQuery function but it is called only once (in initialization time). How to fix it? Please look at the code.
function makeLogging(f) {
function wrapper(...rest) {
console.log(f.name);
console.log(rest);
return f.apply(this, rest);
}
return wrapper;
}
const task = $('.task');
task.on = makeLogging(task.on);
task.on('click', () => console.log('hi'));
The click event does not display messages about the called function.
You are doing it a little bit wrong, if I caught the idea what you want to achieve. For functionality, you described, please try following:
task.on('click', makeLogging(() => console.log('hi')));
In your original code, you wrapped the functionality of on() function, but this on() function is not called as event handler - it only install actual event handler. That's why logging is called only once during installation of the handler.
Code example of answer
function makeLogging(f) {
function auxiliaryWrapper(x, rest) {
return () => {
console.log(f.name);
console.log(rest);
x();
}
}
function mainWrapper(...rest) {
const restWithWrap = rest.map(arg => {
if (typeof arg === 'function') {
return auxiliaryWrapper(arg,rest);
}
return arg;
});
console.log(restWithWrap);
return f.apply(this, restWithWrap);
}
return mainWrapper;
}
const task = $('.task');
task.on = makeLogging(task.on);
task.on('click', () => console.log('hi'));
In my project, I have a scenario where I have different kind of functions with different arguments. One of those arguments holds the callback function. And also there is a case where the function doesn't have any arguments. As shown below:
abc(string, function, number)
aaa(function, string)
bcd()
xyz(function)
cda(string, number, function, string)
I need to write a function such that irrespective of the irregularity of above functions, the function should return a promise.
Example:
// Assume $q has been injected
var defer = $q.defer();
function returnPromise(functionName, functionArgumentsInArray){
defer.resolve(window[functionName].apply(null, functionArgumentsInArray));
return defer.promise;
}
As you can see, the above function doesn't resolve the callback function but just the function.
Note: The functions will have max of one function argument or none.
I know it can be done individually for each case as follows:
function returnPromise(){
var defer = $q.defer();
abc("hey", function() {
defer.resolve(arguments);
}, 123);
return defer.promise;
}
But I am looking for a common wrapper for all such functions.
I think you are looking for something like
const cbPlaceholder = Symbol("placeholder for node callback");
function wrap(fn) {
return function(...args) {
return $q((resolve, reject) => {
function nodeback(err, result) {
if (err) reject(err);
else resolve(result);
}
for (var i=0; i<args.length; i++)
if (args[i] === cbPlaceholder) {
args[i] = nodeback;
break;
}
const res = fn.apply(this, args);
if (i == args.length) // not found
resolve(res);
});
};
}
You could use it as
const abcPromised = wrap(abc);
abcPromised("hey", cbPlaceholder, 123).then(…)
Also have a look at How do I convert an existing callback API to promises?. In general you should not need that placeholder thing when all promisified functions follow the convention to take the callback as their last parameter.