JavaScript function arguments: positional -> map transition - javascript

I'm looking for a vanilla JavaScript solution.
Say I've got a function with the following header:
generateEmail(firstName, lastname, provider)
I need to run it like this:
generateEmail("John","Smith","gmail.com");
I would like to be able to call it with argument map instead of positional arguments, i.e.
generateEmail({
"firstName":"John",
"lastname": "Smith",
"provider": "gmail.com"
});
And I'm looking for an already-written solution to do this in JavaScript, since I've got an unlimited number of functions such as generateEmail above to handle. Does such library exist?
I have seen https://github.com/kilianc/node-introspect which handles function introspection (returning function abstract parameter information). But the second part is missing - mapping map-call into positional-call.
Please tell me, whether such thing exists.
edit: if I didn't make myself clear: I don't want to modify the original positional-argument function. I get such functions from an external provider which may update his code. I'd rather prefer to have a wrapper that could call the original function beneath, and provide a map-argument API outside.

Assuming you have access to introspect from node-introspect (which takes a function and returns an ordered list of arguments names), you can simply do:
function objArgsify(fn) {
var argNames = introspect(fn);
return function(obj) {
return fn.apply(this,
argNames.map(function(a) { return obj[a]; });
}
}
Call it by:
var generateEmailWithObjArgs = objArgsify(generateEmail);
generateEmailWithObjArgs({
"firstName":"John",
"lastname": "Smith",
"provider": "gmail.com"
});
This accepts a function, reads its argument names, and then returns a wrapper function that accepts an object and uses the positional argument names to pull properties from the object-argument in the correct order.
This function uses the call-time object-argument as a map to transform the array ["firstName", "lastname", "provider"] into the array ["John", "Smith", "gmail.com"]. That array is then used with apply to invoke the postional-argument function.

Without using any external libraries,
var STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg;
var ARGUMENT_NAMES = /([^\s,]+)/g;
function getParamNames(func) {
var fnStr = func.toString().replace(STRIP_COMMENTS, '');
var result = fnStr.slice(fnStr.indexOf('(')+1, fnStr.indexOf(')')).match(ARGUMENT_NAMES);
if(result === null)
result = [];
return result;
}
function call(method, object) {
var params = getParamNames(method);
var arrParams = [];
for (var i = 0; i < params.length; i++) {
arrParams.push(object[params[i]]);
}
return method.apply(arrParams);
}
Just call it with call(generateEmail, generateEmailObject).

Something like
var orig_generateEmail = generateEmail;
var generateEmail = function (map) {
orig_generateEmail(map.firstName, map.lastname, map.provider);
};
But that obviously ends up in a list of static "mappings".

Related

array.filter by object property, argument is not defined

I am trying to write a filter function that takes 2 parameters:
id type and the actual id value. Using these IDs, I want to filter an array of objects.For example, here I am trying to get a new array that only includes the values with the name of 'Mike'.
object:
var sample = [
{ name: 'Mike'},
{ name: 'John'}
];
filter function:
function filterById(obj, parameter, id) {
return obj.parameter == id;
}
this:
console.log(sample.filter(filterById(name, 'Mike')));
returns name is not defined.
Do I need to pass in the actual array as well? Is it possible to pass parameters into filter functions at all?
You would need to pass the "parameter" as a string too, and use the square bracket notation, and for this all to work your filterById function would itself have to return a function which matches the function used by Array.prototype.filter:
var sample = [
{ name: 'Mike'},
{ name: 'John'}
];
function filterById(parameter, id) {
return function(obj){
return obj[parameter] == id;
}
}
console.log(sample.filter(filterById('name', 'Mike')));
You don't have to invoke the function by yourself – it is a high-order function, so you have to provide only function. And here we come to the problem – you want to pass arguments there, but you can't!
So, there are few approaches. The first one is just to return another function, which will keep data in closure:
function filterById(parameter, id) {
return function(item) {
return item[parameter] == id;
}
}
The second option is to create another function via .bind, which is close to the idea of partial application. It will create new function with pre-defined parameters. They are always first, so you have to move actual item definition to the last position:
function filterById(parameter, id, item) {
return item[parameter] === id;
}
// we can create function for the future reference:
const filterByMike = filterById.bind(null, 'name', 'Mike');
sample.filter(filterByMike);
It's hard to say what is better, but I'd personally prefer the second approach.

Compare functions in Javascript

I have an API that takes a function as an input, and then inside the API, the intent is to add the function to an Array if the function is not already added to the Array.
The call to the API is of the form:
myApiHandle.addIfUnique(function(){
myResource.get(myObj);
});
The API is:
myApiHandle.addIfUnique(myFunc) {
if (myArray.indexOf(myFunc) === -1) {
return;
}
// add to array
}
Now this obviously does not work as expected, since each time a new function is being passed in.
My Question is: Is there a way to pass in a function into the myApiHandle.addIfUnique call that will allow me to compare the existing functions in the array with this function that is currently passed in? The comparison should compare the function name and the object, and if both are the same, then not add the function to the array. I want to avoid adding another argument to the addIfUnique call if at all possible.
In other words, is the below possible:
myApiCall.addIfUnique (someFunc) {
}
If so, what is the someFunc. And what would be the logic inside the API to detect if the function already exists in myArray?
The same problem occurs with addEventListener and removeEventListener, where the callback must be identical (in the === sense) for removeEventListener to remove it.
As you've found, obviously if you call addIfUnique like this:
addIfUnique(function() { })
the function passed each time will be a unique object. The solution is to create the function once:
var fn = function() { };
addIfUnique(fn);
addIfUnique(fn);
A related problem occurs when the function being passed in is a method invocation, so I need to bind it:
var x = { val: 42, method: function() { console.log(this.val); } };
I want to pass a bound version of it, so
addIfUnique(x.method.bind(x));
addIfUnique(x.method.bind(x));
But again, each call to x.method.bind(x) will return a separate function. So I need to pre-bind:
var boundMethod = x.method.bind(x);
addIfUnique(boundMethod);
addIfUnique(boundMethod);
First of all, comparing functions is meaningless, even if two functions are literally different, they may be functionally the same.
And for your problem, you can compare whether it's exactly the same object, or you can compare it literally by using toString() function and regExp.
var addIfUnique = (function() {
var arr = [];
return function(func) {
if (~arr.indexOf(func)) return false;
var nameArr = [];
var funcName = func.name;
var funcRegExp = new RegExp('[^\{]+\{(.+)\}$', 'i');
var funcStr = func.toString().match(funcRegExp);
funcStr = funcStr && funcStr[1];
if (!funcStr) return false;
var strArr = arr.map(function(v){
nameArr.push(v.name);
return v.toString().match(funcRegExp)[1];
});
if (~strArr.indexOf(funcStr) && ~nameArr.indexOf(funcName)) return false;
arr.push(func);
};
}());

How to call a function on the scope from a string value

I have an object containing an array of strings
$scope.actions=[
"add_inscription",
"add_tools",
"add_instruction",
"remove_inscription",
"remove_tools",
"remove_instruction"
];
and I would like to be able to do dynamic action calls through a delegating function..
$scope.delegate = function () {
var arg = arguments[0];
for ( key in $scope.actions ) {
if ($scope.actions[key] == arg ) {
// call function that has a matching name
}
}
}
So in my template I have something like this
<button ng-click="delegate('add_inscription')">Add Inscription</button>
I don't know if I am thinking in the right direction with this either,, but the point is that my actions object is actually pretty large and I don't want to write massive switch case statement that I will have to update all the time.
Is there a way to do this in angular?
I have no problem doing this in straight up javascript
var fnstring = "add_inscription";
// find object
var fn = window[fnstring];
// if object is a function
if (typeof fn === "function") fn();
but in angular I can't get this done..
assuming that your "actions" functions are defined inside the scope, like:
$scope.add_inscription = function(){ ... }
you should do:
var _action = 'add_inscription';
$scope[_action]();

Dynamic javascript function declaration

I'm working on a code where I must pass a different function to some objects.
In this case, I'm trying to pass a different function for the onchange event. So currently what I got is something like this this:
var ArrayList; //Contains some data to use with ObjectArray format { n: data }
var ObjectArray; //Contains several objects format Array[n] = Object;
for(var key in ArrayList){
var doFunction = function() {
Object[key].doSomething(ArrayList[key]);
}
Object[key].onchange = doFunction;
}
The problem here I believe is that I'm afraid it will execute the code as it is declared and not with the values of the actual variables.
Is there a way to pass the function with the values as it executes? or will the variables get parsed the way its written?
It's the classic function in a loop problem. You need to understand how closures work.
Read the "Example 3" part of this answer carefully. The whole How do JavaScript closures work? question, too.
Another example that might help understand intuitively:
var key = 5;
var onchange = function () {
console.log(key);
};
onchange(); // 5
key = 10; // the loop reassigns the key on each iteration
onchange(); // 10
This is how it should be done:
var ArrayList; //Contains some data to use with ObjectArray format { n: data }
var ObjectArray; //Contains several objects format Array[n] = Object;
for(var key in ArrayList)
{
(function(key)
{
var doFunction = function()
{
Object[key].doSomething(ArrayList[key]);
}
Object[key].onchange = doFunction;
}(key))
}

How to shift "arguments"?

Here's the script:
function runScripts() {
if (arguments.length === 0) return;
chrome.tabs.executeScript(null, {
file: arguments[0]
}, function() {
arguments.shift();
runScripts.apply(null, arguments);
});
}
It doesn't work because arguments is not actually an array, it's just array-like. So how can I "shift" it or hack off the first element so that I can apply this function recursively?
var params = Array.prototype.slice.call(arguments);
params.shift();
You can check out this blog post which explains it in further detail.
I assume you want to reference the original arguments, instead of that from the callback you're passing to chrome.tabs.executeScript.
If so, you'll need to cache it first.
function runScripts() {
if (arguments.length === 0) return;
var args = [];
Array.prototype.push.apply( args, arguments );
chrome.tabs.executeScript(null, {
file: args.shift();
}, function() {
// using the modified Array based on the original arguments object
runScripts.apply(null, args);
});
}
[].shift.call(arguments) is also valid. I'm using this in production code and it works as expected.
With this approach, your function becomes a bit more succinct:
function executeScripts() {
if (arguments.length === 0) return;
chrome.tabs.executeScript(null, {
file: [].shift.call(arguments)
}, function() {
executeScripts.apply(null, arguments);
});
}
If you look on MDN, they state that shift() was implemented with this flexibility in mind.
https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/shift
You can transform arguments into a regular array like this:
var args = Array.prototype.slice.call(arguments);
Just wanted to point out a potential problem with [].shift.call(arguments).
This seems to have the perhaps unclear intent of shifting your arguments - even for your function's named parameters - even if used prior to the shift statement.
For example,
function testShift (param1, param2) {
[].shift.call(arguments);
if (param1=="ONE") alert("ONE");
}
If you make the following call, what might you expect to happen?
testShift("ONE", "TWO");
If you expected param1 to stay "ONE", your fix is to set a var to param1 before the shift occurs. It looks like javascript is not binding param1 until the line it is called on - not when the function is called... so modifications to arguments prior to a parameter being used can have unexpected effects.
Hopefully now, you'll be able to expect it.
In ES6 you can now use Array.from() MDN ref
e.g.
const args = Array.from(arguments);
const str = args.shift();
You'll need to convert it to an array and then shift. Or, alternatively, drop the first item when converting to an array. Array.prototype.slice.call(arguments, 1) would work for this.
In newer versions of JS, we can now write:
function f(first, ...rest) { ... }
Or
function f() {
const [first, ...rest] = arguments;
}
Which is a little nicer than "shifting" off the first arg. However, if we did want to, we could first convert arguments into a proper array via Array.from or [...arguments].
Here is an article explains this really well. I copied some key points below.
http://www.javascriptkit.com/javatutors/arrayprototypeslice.shtml
For array, remember you can call the slice function to get a sub array.
var abc = [1,2,3,4,5];
abc.slice(0); //[1,2,3,4,5]
abc.slice(1,3); //[2,3]
Since the argument object is only array like, not really an array. The call() / apply() function basically just "borrow" the slice function from Array and use it on the Argument object, and you can even pass parameters into the slice function just as acting on the array.
var myobject ={ // array-like collection
length: 4,
'0': 'zero',
'1': 'one',
'2': 'two',
'3': 'three'
}
var myarray = Array.prototype.slice.call(myobject)
// returns myobject as a true array: ["zero", "one", "two", "three"]
var myarray = Array.prototype.slice.call(myobject, 1)
// returns ["one", "two", "three"]
The one remaining question is why we're calling slice() on the prototype object of Array instead of an array instance. The reason is because this is the most direct route to accessing the slice() method of Array when that's all we're interested in; we could have first created an array instance, but that's less efficient and arguably more abstruse:
var myarray = new Array().prototype.slice.call(myobject) // less efficient
You could convert the arguments to an actual array and then use that array in the rest of your logic in the function.
function runScripts()
{
var i=0, l=arguments.length, arr=[];
while(i<l)
{
arr.push(arguments[i++]);
}
...rest of your function code
Edit to add: i've had issues with prototype and call in older versions of IE, so it really depends on what support you'll need.
I went with this:
function executeScripts() {
if (arguments.length === 0) return;
var args = Array.prototype.slice.call(arguments);
chrome.tabs.executeScript(null, {
file: args.shift()
}, function() {
executeScripts.apply(null, args);
});
}
It's useful when writing Google Chrome Extensions. I wanted to use jQuery in my content script, but then you have to load it first. Turns out out by chaining calls to chrome.tabs.executeScript you can do this:
chrome.browserAction.onClicked.addListener(function(tab) {
executeScripts('jquery-1.4.4.min.js', 'content.js');
});

Categories

Resources