1

First, thought we already have overcome the evil eval, right? Plus, I am working in pure JavaScript.

So I have an object:

var MyObj = new Object();
MyObj.myFunction = function(a, b) { return a + b; }

But this function is stored in a string, and the Function constructor ask for the parameters first, and than the body after, and I don't know about the parameters, if exists. The point is to create the function by "interpreting" (not EVALuating) the content of the string. I wish this was possible:

var MyObj = new Object();
var myFuncStr = "function(a, b) { return a + b; }";
MyObj.myFunction = new Function(myFuncStr);

I found this discussion from 11 years ago: Given a string describing a Javascript function, convert it to a Javascript function But this was a decade ago and the answer is for a specific case.

I am thinking in try to identify the parameters inside the string and try to pass to the Function constructor, something like a String.toFunction extension that will require some code (maybe I locate the first parentheses, get the slice, locate the brackets, get the content... voilá, seems a bit rust).

Is there an answer already for this situation? Any existing solutions?

EDIT: Since there is a lot of votes asking for a step back, I will post the code one level up (two will be about 3k lines)

Object.defineProperty(String.prototype, "toObject", {
enumerable: false,
configurable: false,
writable: false,
value: function(method, extended) {
    var string = this;
    if(!method) method = "typeof";
    let ini = string.indexOf('{');
    let fim = string.lastIndexOf('}');
    if(ini==-1 || fim==-1) {
        $monitor("Erro de parâmetro String.toObject","erro","O parâmetro passado a String.toObject ("+string+") não é um objeto literal.");
        return null;
    }
    var str = string.slice(ini+1, fim);
    console.log("String.toObject str...");
    console.log(str);
    var Elems = str.split(','), Terms, prop, value, isStr, val, type, dp = new DOMParser();
    var Obj = new Object();
    for(let i=0; i<Elems.length; i++) {
        Terms = Elems[i].split(':');
        prop = Terms.shift().filter('property');
        value = Terms.join(':').filter('value');
        console.log(" ...filter "+prop+" : "+value);
        isStr = (value.charAt(0)=='"' && value.charAt(value.length-1)=='"');
        switch(method) {
            case "typeof":
                val = (isStr)? value.slice(1,-1) : value ;
                type = (isStr)? "string" : val.typeof(extended) ; 
                break;
            case "string":
                val = (isStr)? value.slice(1,-1) : value ;
                type = "string";
                break;
            default:
                $monitor("Erro de parâmetro String.toObject","erro","O parâmetro 'method' ("+method+") passado a String.toObject não é válido.");
                return null;
        }
        switch(type) {
            case "null":
                Obj[prop] = null;
                break;
            case "boolean":
                Obj[prop] = (val.toLowerCase()=="true");
                break;
            case "number":
                Obj[prop] = Number.parseFloat(val);
                break;
            case "string":
                Obj[prop] = val;
                break;
            case "function":
                Obj[prop] = "StackOverflowWillGiveMeTheAnswer";
                break;
            case "xml":
                Obj[prop] = dp.parseFromString(val, "text/xml");
                break;
            case "object":
                Obj[prop] = val.toObject(extended);
                break;
        }
    }
    return Obj;
}
});

But this function depends on typeof, so here is:

Object.defineProperty(String.prototype, "typeof", {
enumerable: false,
configurable: false,
writable: false,
value: function(extended) {
    var string = this;
    if(string.length==0 && extended) return "null";
    if(string.toLowerCase()=="true" || string.toLowerCase()=="false") return "boolean";
    if(string.isNumeric()) return "number";
    if(string.replaceAll(' ','').substring(0, 9)=='function(' || string.replaceAll(' ','').substring(0, 5)=='()=>{') return 'function';
    string = string.trim();
    if(extended) {
        try {
            var DOM = new DOMParser();
            var xml = DOM.parseFromString(string, "text/html");
            return "xml";
        } catch(e) {}
    } else {
        if(string.charAt(0)=='<' && string.charAt(string.length-1)=='>') return "xml";
    }
    //console.log("String.typeof chr 0 -1: "+string.charAt(0)+" "+string.charAt(string.length-1));
    if(string.charAt(0)=='{' && string.charAt(string.length-1)=='}') return "object";
    return "string";
}
});

Both are in development, not 100% functional yet. I was avoiding to post so much code because of the downvotes, will take my chances. Just give me an answer instead of just asking why... sometimes God only knows why!

By the way, "no you can't" is an answer.

Gustavo
  • 1,673
  • 4
  • 24
  • 39
  • 5
    But *why* do you have a string for a function? If you're *here* your options are extremely limited. But if you are able to take a step back, it's maybe possible to give more and better options. – VLAZ May 17 '21 at 20:15
  • 1
    *"interpreting" (not EVALuating)* what's the actual difference? – Guerric P May 17 '21 at 20:19
  • Follow up question: how are you planing to utilize those functions down the road, which are stored as strings? Are you planning to execute them somehow (e.g. via `eval`)? – Andrew Ymaz May 17 '21 at 20:21
  • This is part of much bigger code. In this part, an object is created from string and will use type definitions to set each property type, based in content (a xml is parsed as E4X XML object). There is not difference between interpreting and evaluating, just following the advising of not using eval. The solution will made my extension String.toObject more complete, I can load a string that defines an object with functions. – Gustavo May 17 '21 at 20:39
  • If you have type definitions for your objects already, why not create an object of the right type (which has its method defined the conventional way) straight ahead? Why store the method bodies in the XML? – Bergi May 17 '21 at 21:18
  • "*thought we already have overcome the evil eval, right?*" - you'll want to read [When is JavaScript's eval() not evil?](https://stackoverflow.com/q/197769/1048572) – Bergi May 17 '21 at 21:30
  • @Bergi: I can't go inside this right now, have a lot of code to make. You might try an answer as you thought, just remember that the result must be a Function type. – Gustavo May 17 '21 at 22:32
  • @Gustavo The answer is to use `eval` if you have no reasons to avoid it. – Bergi May 17 '21 at 22:33
  • The code you posted doesn't actually explain the "why". It just shows you have some some code that tries to convert a string to a function (well, to an object but one of the properties to a function). Not the reason for actually doing all of that. – VLAZ May 18 '21 at 05:00
  • I didn't knew a reason was necessary to have an answer from you guys, but it's clear at the bottom that "it can't be done" is an answer, specially in this case: convert string to object as better as possible. It's generic. It's clear that you guys don't know or don't want to share what you know. – Gustavo May 18 '21 at 10:56

2 Answers2

0

Yes, you can do this without eval (whether you should is another discussion), using URL.createObjectURL to basically create a file that contains the JS code you want to execute...

// Object as string including function definition
const objectAsString = `{ 
    a: 1, 
    b: 2, 
    c: function(x) { 
        return this.a + this.b + x; 
    }
}`;

// Wrap in script to assign to global variable
const source = `
var objectFromStrWithFunc = ${objectAsString}`;
const script = document.createElement("script");

// Create a Blob from the source and create a file URL
script.src = URL.createObjectURL(new Blob([source], { type: 'text/javascript' }));

// When it loads call the function on the global variable
script.addEventListener(
    'load', 
    () => alert(objectFromStrWithFunc.c(3)));

document.head.appendChild(script);

This will run with some content security policies that will block eval (specifically those that allow blob but not unsafe-eval) and will work slightly better with some debugging tools (as the browser sees it as a file).

However, from the comments it looks like you're using E4X, which hasn't been supported by any mainstream browsers for over a decade now, but is really easy to port to JSX (the syntax is extremely similar). You may be better off using any of the many JSX tools (it's the basis of React) to output the JS you want at build time or on the server.

Converting E4X to text that you deserialise to JS on the client is a fairly unique solution - you'll have to write everything yourself.

Converting JSX to JS that runs in the browser is what every React app is doing, and has massive support and probably already has solutions written for most problems.

Keith
  • 150,284
  • 78
  • 298
  • 434
  • What's the advantage of doing it the complicated way? – Bergi May 17 '21 at 21:15
  • @bergi It's not `eval` or `new Function`? The browser sees it as if you had loaded another JS file. I've used this to send functions to a `Worker` (see [Greenlet](https://github.com/developit/greenlet) or [Comlink](https://github.com/GoogleChromeLabs/comlink)) but I don't know why the OP wants to do it. – Keith May 17 '21 at 21:23
  • What exactly is so bad about `eval` or `new Function`? (rhetorical question, but still) – Bergi May 17 '21 at 21:28
  • @bergi isn't that a bit off topic? I mean google it or take it up with the OP? This has some advantages over `eval` in that it will run in some CSP contexts that `eval` won't and it's more debuggable, but whether that is worth the additional complexity? Dunno, probably depends on a context that I don't know. OP wanted to run a function in a string without `eval`, this is a way to do that, I suspect the debate of relative ways to do this is a whole other question. – Keith May 17 '21 at 21:38
  • "*This has some advantages over `eval` in that it will run in some CSP contexts that eval won't and it's more debuggable*" - that should be part of the answer imo. I don't even know whether the statement is true, but you need *some* reasons to weigh in against the overhead. (And yes, I suspect the OP is helped better with a frame challenge, but that's not related to your answer) – Bergi May 17 '21 at 21:43
  • 1
    @Bergi Eh? what does _"some reasons to weigh in against the overhead"_ mean? If you think a better answer is to "just use `eval`" then put that in your own answer. – Keith May 17 '21 at 21:46
  • I think "*just use `eval`*" is the default answer, avoiding `eval` for the sake of avoiding it is no good. So if you have a different approach, it needs to be evaluated against `eval`, and the benefits (supposed or actual) of your answer are not immediately clear. – Bergi May 17 '21 at 21:50
  • @Bergi so you're downvoting me because you think your answer is the better one, but you haven't actually answered? I've updated my answer to reflect that I'm not actually making a recommendation about `eval`, just answering the question. The OP hasn't actually asked whether they _should_ use `eval`, but feel free to answer that or take it up with them. Or better yet, here are some actual questions about whether `eval` is [good](https://stackoverflow.com/questions/197769) or [bad](https://stackoverflow.com/questions/86513), maybe take this discussion there? – Keith May 17 '21 at 22:02
  • When I did my question I tried to make it stright into the point. I don't know exactly why eval should not be used, but I thought is consensual this point. The @Keith answer is interesting but out of context - to create a Function as property's value in an object, not just to run the script. Also, I tried something similar years ago but browser won't run because of security violation. E4X was just an example because the documentation says is the XML Object will be created. – Gustavo May 17 '21 at 22:27
  • @Gustavo you can do that here too, with either `eval` or the blob method here you can do `eval("{ a: 1, b: 2, c: function(x) { alert(a + b+ x); } }").c(3) === 6` – Keith May 18 '21 at 07:10
  • @Keith I started to do some experiments. Trought new Function is working for a traditional function declaration! As eval is still supported by MDN, I will let an option for the coder to use eval or new Function. But I have to support all current function notations, and I didn't get your example. Do you have some link to some syntax definition? – Gustavo May 18 '21 at 13:37
  • @Gustavo I used [`Blob`](https://developer.mozilla.org/en-US/docs/Web/API/Blob) to turn the JS string into a binary object, and [`createObjectURL`](https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL) to turn that binary stream into a file that could be loaded into a ` – Keith May 18 '21 at 17:47
  • @Gustavo I've extended the example in the question. – Keith May 18 '21 at 17:55
0

I will share my own solution to the question. I have created a method for String instances:

Object.defineProperty(String.prototype, "toFunction", {
enumerable: false,
configurable: false,
writable: false,
value: function(useEval) {
    if(!$params(["String.toFunction", 
        {nome:"useEval",valor:useEval,tipos:"boolean"}
        ])) {
        $monitor("Erro de parâmetro String.toFunction","erro","O parâmetro passado a String.toFunction ("+useEval+") não é válido.");
        return null;
    }
    var string = this, Func, params, body;
    if(useEval) {
        Func = eval(string);
        if(typeof(Func)!="function") {
            $monitor("Erro de parâmetro String.toFunction","erro","O parâmetro passado a String.toFunction ("+string+") não é interpretável como funcção por eval.");
            return null;
        }
    } else {
        if(string.split("=>").length==2) { // Arrow Function
            let Terms = string.split("=>");
            params = Terms[0].filter('value');
            body = Terms[1].filter('value');
        } else if(string.substring(0,8)=='function') {
            let Terms = string.split(')');
            params = Terms[0].split('(')[1];
            Terms.shift();
            body = Terms.join(')').filter('value');
        } else {
            $monitor("Erro de parâmetro String.toFunction","erro","String não reconhecida como função.");
            return null;
        }
        if(params.charAt(0)=='(') params = params.slice(1,-1);
        params = params.replaceAll(' ','');
        if(body.charAt(0)=="{") body = body.slice(1, -1);
        Func = new Function(params, body);
    }       
    return Func;
}
});

I did some tests and the code seems promising. I didn't test the eval option, but will leave it available, who knows. The 'filter' method is a function that returns the string filtered. In the 'value' option, it strips spaces and text marks (tab, enter, etc) before and after the content.

This is the testing code:

var MyObj = new Object();
var Funcs = ["function (a, b) { return a + b; }",
    "(a, b) => { return a + b; }"];
MyObj.myFunc = Funcs[0].toFunction();
console.log("String.toFunction test: "+MyObj.myFunc(1, 2).toString());
MyObj.arrFunc = Funcs[1].toFunction();
console.log("String.toFunction test: "+MyObj.arrFunc(1, 2).toString());

Should log 3 and 3.

Gustavo
  • 1,673
  • 4
  • 24
  • 39