24

If you want to use global functions and variable dynamically you can use:

window[functionName](window[varName]);

Is it possible to do the same thing for variables in the local scope?

This code works correctly but currently uses eval and I'm trying to think of how else to do it.

var test = function(){
    //this = window
    var a, b, c; //private variables

    var prop = function(name, def){
        //this = window
        eval(name+ ' = ' + (def.toSource() || undefined) + ';');    

        return function(value){
            //this = test object
            if ( !value) {
                return eval('(' + name + ')');
            }
            eval(name + ' = value;')
            return this;
        };

    };

    return {
        a:prop('a', 1),
        b:prop('b', 2),
        c:prop('c', 3),
        d:function(){
            //to show that they are accessible via to methods
            return [a,b,c];
        }
    };
}();

>>>test
Object
>>>test.prop
undefined
>>>test.a
function()
>>>test.a()
1 //returns the default
>>>test.a(123)
Object //returns the object
>>>test.a()
123 //returns the changed private variable
>>>test.d()
[123,2,3]
AnnanFay
  • 9,573
  • 15
  • 63
  • 86
  • 1
    I get why I was confused now, it's because you're naming the variables abc AS WELL AS the names for the return. You should change those names, that's what was confusing me. Anyways, better answers than mine have shown up, so I'm just going to defer to crescentfresh at this point. – Dan Lew Mar 01 '09 at 01:12
  • @Gothdo The question you link to is asking something different. The variables he is trying to access are global variables. The accepted answer uses global variables also. The linked question should be changed. – AnnanFay Jan 12 '16 at 18:37

4 Answers4

8

To answer your question, no, there is no way to do dynamic variable lookup in a local scope without using eval().

The best alternative is to make your "scope" just a regular object [literal] (ie, "{}"), and stick your data in there.

Crescent Fresh
  • 115,249
  • 25
  • 154
  • 140
7

No, like crescentfresh said. Below you find an example of how to implement without eval, but with an internal private object.

var test = function () {
  var prv={ };
  function prop(name, def) {
    prv[name] = def;
    return function(value) {
      // if (!value) is true for 'undefined', 'null', '0', NaN, '' (empty string) and false.
      // I assume you wanted undefined. If you also want null add: || value===null
      // Another way is to check arguments.length to get how many parameters was
      // given to this function when it was called.
      if (typeof value === "undefined"){
        //check if hasOwnProperty so you don't unexpected results from
        //the objects prototype.
        return Object.prototype.hasOwnProperty.call(prv,name) ? prv[name] : undefined;
      }
      prv[name]=value;
      return this;
    }
  };

  return pub = {
    a:prop('a', 1),
    b:prop('b', 2),
    c:prop('c', 3),
    d:function(){
      //to show that they are accessible via two methods
      //This is a case where 'with' could be used since it only reads from the object.
      return [prv.a,prv.b,prv.c];
    }
  };
}();
Community
  • 1
  • 1
some
  • 48,070
  • 14
  • 77
  • 93
  • What about enumerating the local variables in the function itself? In other words, like eval, but when you don't know what the locals are... – Michael Apr 26 '17 at 15:09
  • @Michael I'm sorry, but I don't understand what you mean. Could you give an example? Or maybe even write your own answer? – some Apr 27 '17 at 14:14
  • i don't have an answer, and everything i have read seems to suggest it's impossible... global variables in a browser can be enumerated with something like `for (i in window)`; would be nice if that were possible for locals in a function. – Michael Apr 27 '17 at 16:41
  • @Michael Could you give me an example of when that would be useful, and where an object can't be used? – some May 02 '17 at 17:30
  • it would be useful in some cases for introspection, for instance to serialize closures. – Michael May 02 '17 at 19:01
2

I think you actually sort of can, even without using eval!

I might be wrong so please correct me if I am, but I found that if the private variables are declared inside the local scope as arguments, instead of using var, i.e:

function (a, b, c) { ...

instead of

function () { var a, b, c; ...

it means that those variables/arguments, will be bound together with the function's arguments object if any values are given to them in the function's invocation, i.e:

function foo (bar) {
    arguments[0] = 'changed...';
    console.log(bar); // prints 'changed...'

    bar = '...yet again!';
    console.log(arguments[0]); // prints '..yet again!'
}

foo('unchanged'); // it works (the bound is created)
// logs 'changed...'
// logs '...yet again!'

foo(undefined); // it works (the bound is created)
// logs 'changed...'
// logs '...yet again!'

foo(); // it doesn't work if you invoke the function without the 'bar' argument
// logs undefined
// logs 'changed...'

In those situations (where it works), if you somehow store/save the invoked function's arguments object, you can then change any argument related slot from arguments object and the changes will automatically be reflected in the variables themselves, i.e:

// using your code as an example, but changing it so it applies this principle
var test = function (a, b, c) {
    //this = window
    var args = arguments, // preserving arguments as args, so we can access it inside prop
        prop = function (i, def) {
            //this = window

            // I've removed .toSource because I couldn't apply it on my tests
            //eval(name+ ' = ' + (def.toSource() || undefined) + ';');
            args[i] = def || undefined;   

            return function (value) {
                //this = test object
                if (!value) {
                    //return eval('(' + name + ')');
                    return args[i];
                }

                //eval(name + ' = value;');
                args[i] = value;
                return this;
            };
        };

    return {
        a: prop(0, 1),
        b: prop(1, 2),
        c: prop(2, 3),
        d: function () {
            // to show that they are accessible via to methods
            return [a, b, c];
        }
    };
}(0, 0, 0);

If the fact that you can pass the values as arguments into the function annoys you, you can always wrap it with another anonymous function, that way you really don't have any access to the first defined values passed as arguments, i.e:

var test = (function () {
    // wrapping the function with another anomymous one
    return (function (a, b, c) {
        var args = arguments, 
            prop = function (i, def) {
                args[i] = def || undefined;   

                return function (value) {
                    if (!value) {
                        return args[i];
                    }

                    args[i] = value;
                    return this;
                };
            };

        return {
            a: prop(0, 1),
            b: prop(1, 2),
            c: prop(2, 3),
            d: function () {
                return [a, b, c];
            }
        };
    })(0, 0, 0);
})();

Full Dynamic Access Example

We can map all argument variable names into an array by getting the function itself (arguments.callee) as a string, and filtering its parameters using a regex:

var argsIdx = (arguments.callee + '').replace(/function(\s|\t)*?\((.*?)\)(.|\n)*/, '$2').replace(/(\s|\t)+/g, '').split(',')

Now with all the variables in an array, we can now know the corresponding variable name for each function's arguments slot index, and with that, declare a function (in our case it's prop) to read/write into the variable:

function prop (name, value) {
    var i = argsIdx.indexOf(name);

    if (i === -1) throw name + ' is not a local.';
    if (arguments.hasOwnProperty(1)) args[i] = value;

    return args[i];
}

We can also dynamically add each variable as a property, like in the question's example:

argsIdx.forEach(function (name, i) {
    result[name] = prop.bind(null, name);
});

Finally we can add a method to retrieve variables by name (all by default), and if true is passed as the first argument, it returns the hard-coded array with all the variables by their identifiers, to prove that they are being changed:

function props (flgIdent) {
    var names = [].slice.call(arguments.length > 0 ? arguments : argsIdx);

    return flgIdent === true ? [a, b, c, d, e, f] : names.map(function (name) {
        return args[argsIdx.indexOf(name)];
    });
} 

The prop and props functions can be made available as methods inside the returned object, in the end it could look something like this:

var test = (function () {
    return (function (a, b, c, d, e, f) { 
        var argsIdx = (arguments.callee + '').replace(/function(\s|\t)*?\((.*?)\)(.|\n)*/, '$2').replace(/(\s|\t)+/g, '').split(','), 
            args = arguments, 
            result = {
                prop: function (name, value) {
                    var i = argsIdx.indexOf(name);

                    if (i === -1) throw name + ' is not a local.';
                    if (arguments.hasOwnProperty(1)) args[i] = value;

                    return args[i];
                },
                props: function (flgIdent) {
                    var names = [].slice.call(arguments.length > 0 ? arguments : argsIdx);

                    return flgIdent === true ? [a, b, c, d, e, f] : names.map(function (name) {
                        return args[argsIdx.indexOf(name)];
                    });
                }
            };

        args.length = argsIdx.length;
        argsIdx.forEach(function (name, i) {
            result[name] = result.prop.bind(null, name);
        });

        return result;
    })(0, 0, 0, 0, 0, 0);
})();

Conclusions

It's impossible to read/write a function's local scope variable without eval, but if those variables are function's arguments and if they're given values, you can bound those variable identifiers to the function's arguments object and indirectly read/write into them from the arguments object itself.

Daniel
  • 46
  • 1
  • 3
0

Hopefully I'm not over-simplifying, but what about something as simple as using an object?

var test = {
    getValue : function(localName){
        return this[localName];
    },
    setValue : function(localName, value){
        return this[localName] = value;
    }
};

>>> test.a = 123
>>> test.getValue('a')
123
>>> test.a
123

>>> test.setValue('b', 999)
999
>>> test.b
999
  • If you're going to just access the object's local variables directly, what's the point of even defining a getter or setter? – Dan Lew Feb 28 '09 at 23:50
  • There isn't one. I just added that in so the usage would be identical to Annan's example. I agree, it would be completely stupid in practice. –  Feb 28 '09 at 23:55
  • The reason for the code is to emulate how getters and setters work in a cross browser way. Therefore my original code was incorrect (now fixed) as the variables prop() created weren't available to the rest of the object. Your way allows direct access and manipulation of properties. – AnnanFay Mar 01 '09 at 00:11
  • Reading other people's code is a good reason. Even if publicly exposed 'someProperty' doesn't tell me if something is meant to be used by any old function or if the author of the object was expecting another object to change it. – Erik Reppen Mar 21 '12 at 17:10