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.

10
  • 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. Commented May 17, 2021 at 20:15
  • 1
    "interpreting" (not EVALuating) what's the actual difference? Commented May 17, 2021 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)? Commented May 17, 2021 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. Commented May 17, 2021 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? Commented May 17, 2021 at 21:18

2 Answers 2

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.

Sign up to request clarification or add additional context in comments.

13 Comments

What's the advantage of doing it the complicated way?
@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 or Comlink) but I don't know why the OP wants to do it.
What exactly is so bad about eval or new Function? (rhetorical question, but still)
@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.
@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.
|
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.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.