5

Is it possible to call a method from an object using a string?

var elem = $('#test');             //<div id="test"></div>
var str = "attr('id')";  

//This is what I'm trying to achieve
  elem.attr('id');                 //test

//What I've tried so far  
  elem.str;                        //undefined
  elem.str();                      //Object [object Object] has no method 'str'
  var fn = eval(str);              //attr is not defined
  eval(elem.toString()+'.'+str);   //Unexpected identifier

//Only solution I've found so far, 
//but is not an option for me 
//because this code is in a function 
//so the element and method call
//get passed in and I wouldn't know
//what they are
  eval($('#test').attr('id'));     //test
Aust
  • 11,552
  • 13
  • 44
  • 74

3 Answers3

4

UPDATE

This is my final, working answer:
After running this code in the console

theMethod = 'attr("id","foo")'.match(/^([^(]+)\(([^)]*)\)/);
jQuery('#post-form')[theMethod[1]].apply(jQuery('#post-form'),JSON.parse('['+theMethod[2]+']'));

The post-form element now has a new ID, no problems at all. This works for methods that take multiple arguments, a single argument or no arguments at all. Recap:

theMethod = theInString.match(/^\.?([^(]+)\(([^)]*)\)/);
//added \.? to trim leading dot
//made match in between brackets non-greedy
//dropped the $ flag at the end, to avoid issues with trailing white-space after )
elem[theMethod[1]].apply(elem,JSON.parse('['+theMethod+']'));

That's the safest, most reliable approach I can think of, really


What ever you do DON'T USE EVAL:

var theMethod = 'attr(\'id\')';
//break it down:
theMethod = theMethod.match(/^([^(]+)\(.*?([^)'"]+).*\)$/);
//returns ["attr('id')", "attr", "id"]
elem[theMethod[1]](theMethod[2]);//calls the method

It's the same basic principle as you'd use with any objects (remember that functions are objects all on their own in JS - and jQuery objects are, well, objects, too). This means that methods can be accessed in the exact same way as properties can:

$('#foo').attr('id') === $('#foo')['attr']('id');

So just break the string apart, and use the method name like you would an object property and you're all set to go.

Just remember: When all you have is the eval hammer, everything looks like your thumb.
Brendan Eich


If there is a chance of multiple arguments being passed to whatever method, you can sort of work your way around that, too (I think - well: logic dictates, but it's rather late and logic is getting beat up by Gin pretty bad now):

theMethod = theMethod.match(/^([^(]+)\(([^)]+)\)$/);
//["attr('id','foo')", "attr", "'id','foo'"] --> regex must now match quotes, too
elem.theMethod[1].apply(elem,JSON.parse('['+theMethod[2]+']'));

This applies the method of whatever element/object you're dealing with to itself, thus not changing the caller context (this will still point to the object within the method) and it passes an array of arguments that will be passed to the called method.

Elias Van Ootegem
  • 74,482
  • 9
  • 111
  • 149
  • 2
    You can't reliably split args on the comma since there could be other commas within the arguments. `'func(greet("Elias", "Hi, how are you"), /(.*?),.*/)'` – gray state is coming Aug 28 '12 at 20:20
  • Fair enough, that was just a wild stab in the dark, with a bit of work, and a lot less of drink I could write an expression to to split the arguments reliably, though. Nevertheless, you could just `JSON.parse('["foo","bar"]')` in that case: `elem,theMethod[2].apply(elem,JSON.parse('['+theMethod[2]+']'));` would work just fine, wouldn't it? Ha, just thought of it, it's even better than splitting... I'm editing my answer! Hooray for GIN! – Elias Van Ootegem Aug 28 '12 at 20:25
  • JSON data represents only a narrow subset of possible syntax that could appear. Things like `undefined`, identifiers, regexs, anonymous functions and function calls will break it. I think it will come down to either parsing the JavaScript, or using `eval`. – gray state is coming Aug 28 '12 at 20:29
  • How likely is it that you'll start of with a string, that contains a function definition/declaration that you'll then need to pass as an argument to a jQ method? if the answer to this is _very_, then I'm sorry but there's something wrong with the way your tackling your problem... But that's just what how I feel about that, and yes, then eval becomes pretty much a necessity. Though you're jumping right into the most dangerous of usages of eval: eval-ing to create new functions... ouch – Elias Van Ootegem Aug 28 '12 at 20:32
  • While you're on this site, type this in your console `eval('$ = function(){return null;}')` and check to see what functionality is lost by killing jQuery... – Elias Van Ootegem Aug 28 '12 at 20:35
  • Thanks Elias. Instead of JSON.parse() I ended up using a 2nd RegExp: `/(?:'(.*?)(?:(?:'\s*,\s*)|(?:'\s*\))))|(?:"(.*?)(?:(?:"\s*,\s*)|(?:"\s*\))))/` and removed any nulls and undefineds from the result and assigned that to an array that I passed to apply. – Aust Aug 29 '12 at 20:07
  • I was thinking about this just this morning, on my way to work. JSON.parse was definitely going to be tricky when dealing with single vs double quotes. just a thought on your expression though: why not use `/?:['"](.*?)(?:(?:['"]\s*,\s*)|(?:['"]\s*\)))/`, instead of repeating the same expression twice? – Elias Van Ootegem Aug 30 '12 at 07:10
  • I did it like that to make it more reliable. When they're together `['"]` a string such as `'"Hey", he said.'` would return `"Hey` but if there are 2 seperate patterns seperated by an OR, it returns `"Hey", he said.` So it's long and annoying but at least it works. :) I also changed the RegExp to accept numbers not in any quotes. `/(?:'(.*?)(?:(?:'\s*,\s*)|(?:'\s*\))))|(?:"(.*?)(?:(?:"\s*,\s*)|(?:"\s*\))))|(?:([^\('"].*?)(?:(?:\s*,\s*)|(?:\s*\))))/` But if there is a way to shrink this down, I'm all ears. ;) – Aust Aug 30 '12 at 15:44
1

You should use one of these methods:

  • apply

    var result = function.apply(thisArg[, argsArray]);

  • call

    var result = fun.call(thisArg[, arg1[, arg2[, ...]]]);

Here is the sample:

var Sample = function() {
var that = this;

this.sampleMethod = function() {
    return alert("Hello!");
};

this.sampleMethod2 = function(){

    that["sampleMethod"].apply(that);
};  
};

var objImpl = new Sample();

objImpl.sampleMethod2(); //you will get a message from 'sampleMethod()'
Sergii
  • 1,320
  • 10
  • 10
  • And where are you starting from a string formatted like `.attr('arg','arg')`? – Elias Van Ootegem Aug 28 '12 at 20:42
  • that["sampleMethod"].apply(that); – Sergii Aug 28 '12 at 20:45
  • 1
    I think you misunderstood. The OP is looking for a eval-less way to start from a string like `var someStr = 'setAttr("id","foobar")';` and use it to call the `setAttr` method on a jQuery object and pass the arguments correctly, yours doesn't do that – Elias Van Ootegem Aug 28 '12 at 20:48
  • Another thing: why are you using `that`, rather then this? I don't see the point in that/`that` here, in both methods, `this` will reference the `Sample` object anyway – Elias Van Ootegem Aug 28 '12 at 21:04
  • I agree, in a particular case 'that' is redundant. It is actual when 'Sample' class contains, for example, jQuery event bindings ($(document).click(function(){...});) to call public functions of 'Sample' class. – Sergii Aug 28 '12 at 21:36
  • I thought as much, `that` is definitely (and sadly) a requirement when using closures – Elias Van Ootegem Aug 28 '12 at 22:10
0

Eval does what you want to do. Eval is evil, however, because you should not do what you want to do.

Why is using the JavaScript eval function a bad idea?

Community
  • 1
  • 1
Nate
  • 4,718
  • 2
  • 25
  • 26