In Javascript (Node.js context), I use Function.prototype.bind
regularly: bind
allows for changing invocation context and optionally supplying additional prepended arguments.
Are there any recommendations for appending arguments? On a couple occasions I have encountered the need to append rather than prepend in Node.js so I can adhere to its function signature pattern.
Now for a semi-practical and simplified example; I'm using the async module's eachSeries
method.
First, an implementation that wraps the callback (working, but long way):
function func(something,callback) {
async.eachSeries(
[1,2,3],
function iterator(item,asyncCallback) {
// do stuff
asyncCallback(err||null);
},
function finished(err) {
// `callback` expects 2 arguments
// `err` should always be the first arg, null or otherwise
// `something` (unrelated to the async series) should be maintained
callback(err,something);
}
);
};
And now something a bit shorter:
function func(something,callback) {
async.eachSeries(
[1,2,3],
function iterator(item,asyncCallback) {
// do stuff
asyncCallback(err||null);
},
callback.bindAppend(this,something)
// pseudo-equiv: `callback.bind(this,err,something);`
);
};
In the second example, the err
is carried from eachSeries
's asyncCallback
callback, while something
is provided by other means. To clarify, I'm looking to replace bindAppend
from the "shorter" example that would have the functional equivalent of function finished
in the "longer" example.
Perhaps my design is flawed and needs to be reworked or this is just another case of premature optimization. However, the feature I seek could provide the following benefits:
One answer would be to roll my own from a forked Function.prototype.bind polyfill. However, I'm looking for either a native implementation that I'm not seeing, a close-to-native work-around, or a utility module that has the hard work already done (optimization, testing, etc.). FYI, any solution other than the former would actually worsen feature benefit #2.
Assuming i understood the question,my first shot at it with tests :
Function.prototype.bindAppend = function(context) {
var func = this;
var args = [].slice.call(arguments).slice(1);
return function() {
return func.apply(context, [].slice.call(arguments).concat(args));
}
}
Test:
Function.prototype.bindAppend = function(context) {
var func = this;
var args = [].slice.call(arguments).slice(1);
return function() {
return func.apply(context, [].slice.call(arguments).concat(args));
}
}
describe('bindAppend', function() {
beforeEach(function() {
this.obj = {
value: "a",
func: function() {
return this.value + [].slice.call(arguments).join('');
}
}
});
it('should work properly', function() {
expect(this.obj.func.bindAppend(this.obj, "c")("b")).toEqual("abc");
});
it('should work properly', function() {
expect(this.obj.func.bindAppend(this.obj, "c", "d")("b")).toEqual("abcd");
});
it('should work properly', function() {
expect(this.obj.func.bindAppend(this.obj, "d")("b", "c")).toEqual("abcd");
});
})
Working version: https://mparaiso.github.io/playground/#/gist/hivsHLuAuV
There are no native solutions. But I can propose you something like this:
Function.prototype.bindAppend = function (context) {
var bindArgs = Array.prototype.slice.call(arguments, 1);
var func = this;
return function() {
var funcArgs = Array.prototype.slice.call(arguments);
var args = bindArgs.map(function(arg) {
if (arg === undefined) return funcArgs.shift();
return arg;
}).concat(funcArgs);
func.apply(context, args);
}
}
And usage:
function func(something,callback) {
async.eachSeries(
[1,2,3],
function iterator(item,asyncCallback) {
// do stuff
asyncCallback(err||null);
},
callback.bindAppend(this, undefined, something)
// pseudo-equiv: `callback.bind(this,err,something);`
);
};
Other example:
function a() {console.log.apply(console, arguments)};
var b = a.bindAppend(null, 1, undefined, 3, undefined, 5);
b(2,4,6,7); //1,2,3,4,5,6,7
For the sake of maintainability, I would not add a method to Function
to achieve this. The other answers to the question here contain two implementation of Function.prototype.bindAppend
that are incompatible with each other. So imagine creating your own bindAppend
and then having to use your application with a body of code whose author decided to do the same thing but uses a implementation incompatible with yours.
Polyfills are fine insofar as they aim to fill in a missing function according to a specific standard. A polyfill for Function.prototype.bind
, for instance, is not free to deviate from ECMA-262, 5th edition. If it does deviate, then it is arguably "buggy" and should be replaced with an implementation that does not deviate from the standard. When one implements methods that are not defined by a standard there's no such constraint.
The following benchmarking suite shows the difference between various ways of attaining the result in the question.
var async = require("async");
var Benchmark = require("benchmark");
var suite = new Benchmark.Suite();
function dump (err, something) {
console.log(arguments);
console.log(err, something);
}
function addToSuite(name, makeFinished) {
function func(something, callback) {
async.eachSeries([1,2,3],
function iterator(item, asyncCallback) {
var err;
// do stuff
asyncCallback(err || null);
},
makeFinished(this, something, callback)
);
}
console.log(name, "dump");
func("foo", dump);
console.log("");
suite.add(name, function () {
func("foo", function (err, something) {});
});
}
// Taken from mpm's http://stackoverflow.com/a/23670553/1906307
Function.prototype.bindAppend = function(context) {
var func = this;
var args = [].slice.call(arguments).slice(1);
return function() {
return func.apply(context, [].slice.call(arguments).concat(args));
};
};
addToSuite("anonymous function", function (context, something, callback) {
return function finished(err) {
callback(err, something);
};
});
addToSuite("mpm's bindAppend", function (context, something, callback) {
return callback.bindAppend(context, something);
});
addToSuite("addErr, only one parameter", function (context, something,
callback) {
function addErr(f, something) {
return function (err) {
return f(err, something);
};
}
return addErr(callback, something);
});
addToSuite("addErr, multi param", function (context, something, callback) {
function addErr(f, one, two, three, four, five, six) {
return function (err) {
return f(err, one, two, three, four, five, six);
};
}
return addErr(callback, something);
});
addToSuite("addErr, switch", function (context, something, callback) {
function addErr(f, one, two, three, four, five, six) {
var args = arguments;
return function (err) {
switch(args.length) {
case 1: return f(err);
case 2: return f(err, one);
case 3: return f(err, one, two);
case 4: return f(err, one, two, three);
case 5: return f(err, one, two, three, four);
case 6: return f(err, one, two, three, four, five);
case 6: return f(err, one, two, three, four, five, six);
default: throw Error("unsupported number of args");
}
};
}
return addErr(callback, something);
});
suite
.on('cycle', function(event) {
console.log(String(event.target));
})
.on('complete', function() {
console.log('The fastest is ' + this.filter('fastest').pluck('name'));
})
.run();
Output:
anonymous function dump
{ '0': undefined, '1': 'foo' }
undefined 'foo'
mpm's bindAppend dump
{ '0': 'foo' }
foo undefined
addErr, only one parameter dump
{ '0': undefined, '1': 'foo' }
undefined 'foo'
addErr, multi param dump
{ '0': undefined,
'1': 'foo',
'2': undefined,
'3': undefined,
'4': undefined,
'5': undefined,
'6': undefined }
undefined 'foo'
addErr, switch dump
{ '0': undefined, '1': 'foo' }
undefined 'foo'
anonymous function x 4,137,843 ops/sec ±3.18% (93 runs sampled)
mpm's bindAppend x 663,044 ops/sec ±1.42% (97 runs sampled)
addErr, only one parameter x 3,944,633 ops/sec ±1.89% (91 runs sampled)
addErr, multi param x 3,209,292 ops/sec ±2.57% (84 runs sampled)
addErr, switch x 3,087,979 ops/sec ±2.00% (91 runs sampled)
The fastest is anonymous function
Notes:
The dump
calls dump to screen what it is the final callback gets when there is no error whatsoever.
When there is no error, mpm's bindAppend
implementation will call callback
with only one parameter that has the value "foo"
. This is a very different behavior from the original function finished(err) { callback(err,something);}
callback it is meant to replace.
Everything is faster than mpm's bindAppend
.
I would not use the addErr, multi param
implementation. I put it there to check how that approach would perform.
The addErr
functions are faster than bindAppend
, at the cost of flexibility. They add only one parameter to the callback.
At the end of the day, I'd most likely use the addErr, switch
implementation with a large enough number of cases to handle the needs of my code. If I needed something which would have absolute flexibility, for some limited cases, I'd use something similar to bindAppend
but not as a method on Function
.
lodash _.partial allows you to append arguments..
https://lodash.com/docs/4.17.4#partial
function test(a, b, c) {
return a + ' ' + b + ' ' + c;
}
test.bind(null, 1)(2, 3); // 1 2 3
_.partial(test, _, _, 1)(2, 3); // 2 3 1
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With