Defining Setter/Getter for an unparented local var

2019-02-01 17:15发布

There's a few previous questions on StackOverflow questioning how one goes about accessing local variables via the scope chain, like if you wanted to reference a local variables using bracket notation and a string, you'd need something like __local__["varName"]. Thus far I haven't found even the hackiest method for accomplishing this, and haven't come up with a method after hours of exploiting every trick I know.

The purpose for it is to implement getters/setters on arbitrary unparented variables. Object.defineProperties or __defineGet/Setter__ require a context to be called on. For properties in the global or window contexts you can accomplish the goal of having a setter/getter for direct references to the object.

Object.defineProperty(this, "glob", {get: function(){return "direct access"})
console.log(glob); //"direct access"

Even in my tests with a custom extension I compiled into a modified Chromium that runs prior to any window creation where the context is the actual global context, and even trying to call this directly in the global context crashes my program, I can pull this off without a hitch:

Object.defineProperty(Object.prototype, "define", {
    value: function(name, descriptor){
        Object.defineProperty(this, name, descriptor);
    }
};
define("REALLYglobal", {get: function(){ return "above window context"; }});

And it is then available in all frames created later as a global routed through the specified getter/setter. The old __defineGet/Setter__ also works in that context without specifying what to call it on (doesn't work in Firefox though, the method above does).

So basically it's possible to define get/set guards for any variable on an object, including the window/global context with direct call to the object (you don't need window.propname, just propname). This is the issue with being unable to reference unparented scoped variables, being the only type that can be in an accessible scope but have no addressable container. Of course they're also the most commonly used too so it's not an edge case. This problem also transcends the current implementation of Proxies in ES6/Harmony since it's a problem specifically with being unable to address a local object's container with the language's syntax.

The reason I want to be able to do this is that it's the only barrier to allow overloading of most math operators for use in complex objects like arrays and hashes and deriving a complex resulting value. I need to be able to hook into the setter in cases where a value is being set on an object type I've set up for overloading. No problem if the object can be global or can be a contained in a parent object, which is probably what I'll just go with. It's still useful with a.myObject, but the goal is to make it as transparently usable as possible.

Not only that, but it'd just be really useful to be able to accomplish something like this:

var point3d = function(){
    var x, y, z;
    return {
        get: function(){ return [x, y, z]; },
        set: function(vals){ x=vals[0]; y=vals[1]; z=vals[2]; }
    };
};

(That is similar to ES6's destructuring but has more general applications for implementing functionality attached to getting/setting and not just transporting complex values). Even this basic code will completely fail:

var x = {myname: "intercept valueOf and :set: to overload math ops!", index: 5};
x++; //x is now NaN if you don't implement a setter somehow

I don't care how hacky the solution is, at this point it's just an intense curiosity for me as to whether it can be accomplished, even if it requires breaking every best practice that exists. I've crashed Firefox and Chrome a few hundred times in pursuit of this so far by doing things like redefining/intercepting/modifying Object.prototype.valueOf/toString, Function.prototype Function.prototype.constructor, Function.prototype.call/apply, arguments.callee.caller, etc. with infinite recursion errors and whatnot in attempts to jury rig contexts retroactively. The only thing that I've been able to make work is wrapping basically the whole thing with eval and dynamically building code chunks, which is a bridge too far for me to ever actually use. The only other remotely successful route was in using with combined with pre-defining all local variables on a container, but that's obviously very intrusive on top of the issues with using with.

3条回答
做个烂人
2楼-- · 2019-02-01 17:36

This is currently possible in environments with Proxies. That would be node > 0.6 run as node --harmony_proxies or >0.7 with node --harmony. Chromium Canary (not sure if it's out of that yet) in about:flags at the bottom, experimental javascript. Firefox has had it for a while with no flags.

So this probably won't work when ES6 becomes more official, but it works to an extent now.

  var target = (function(){
    var handler = Proxy.create(Proxy.create({
      get: function(r, trap){
        return function(name,val,c,d){
          if (trap === 'get' || trap === 'set') {
            name = val;
            val = c;
          }
          console.log('"'+trap + '" invoked on property "'+name+'" ' + (val?' with value "'+val+'"':''));
          switch (trap) {
            case 'get': return target[name];
            case 'set': return target[name] = val;
            case 'has': return name in target;
            case 'delete': return delete target;
            case 'keys': return Object.keys(target);
            case 'hasOwn': return Object.hasOwnProperty.call(target, name);
            case 'getPropertyDescriptor':
            case 'getOwnPropertyDescriptor': return Object.getOwnPropertyDescriptor(target, name);
            case 'getPropertyNames':
            case 'getOwnPropertyNames': return Object.getOwnPropertyNames(target);
            case 'defineProperty': return Object.defineProperty(target, name, val);
          }
        }
      }
    }))

    var target = {
      x: 'stuff',
      f: { works: 'sure did' },
      z: ['overwritten?']
    };


    with (handler){
      var z = 'yes/no';
      if (x) {
        //x
      } else {
        x = true;
      }
      console.log(f.works);
      if (f.works) {
        f.works = true;
        delete f;
      }

    }
    return target
  })()
   // "getPropertyDescriptor" invoked on property "z" 
   // "getPropertyDescriptor" invoked on property "z" 
   // "getPropertyDescriptor" invoked on property "x" 
   // "get" invoked on property "x" 
   // "getPropertyDescriptor" invoked on property "console" 
   // "getPropertyDescriptor" invoked on property "f" 
   // "get" invoked on property "f" 
   // sure did
   // "getPropertyDescriptor" invoked on property "f" 
   // "get" invoked on property "f" 
   // "getPropertyDescriptor" invoked on property "f" 
   // "get" invoked on property "f" 
   // "getPropertyDescriptor" invoked on property "f" 

   target: { x: 'Stuff', f: { works: true },  z: ['overwritten?'] }

Hit or miss and you need to take care not to blow up your browser by simply looking at a Proxy in the debugger. I had to wrap that thing in a closure to keep the proxy from ending up in the global scope or it crashed the frame every single time. Point is that it works to some extent, where nothing else does.

查看更多
女痞
3楼-- · 2019-02-01 17:36

Since you state you want similar behavior to window/global, I assumed you want this within a given context other that window/global. An easy way to do this is by using the with statement in combination with a local object and a define function which implement Object.defineProperty with local as target. You than simply place your own code within the with block.

IMPORTANT: with overloads the native local variables (var, let, const). Because of this it's very important to keep clear code, and to prevent duplicate names within the scope and parent/child contexts.

Lets start of with the context, in this case I use a closure, but this could also be a function, constructor or any other context.

// This closure represents any function, class or other scoped block.
(function (){

}());

Next we add the storage container and the define function. This is basically what you should always start with if you want access the local properties from anywhere in your code (within this scope).

// This is where we store the local property. (except: var, let, const)
const local = {};

// The define function is used to declare and define the local properties.
function define(name, descriptor){ Object.defineProperty(local, name, descriptor); }

Now you can place any code before the with statement but for this example we'll only add code that requires local in some way so the next step is creating the with statement.

// This with statement extends the current scope with local.
with(local){

    // This is where your code goes.

}

Now the outer structure of the with statement is ready, and we can start adding code inside the with statement.

All code placed within the with statement's block has access to the properties of local as if they where defined with for instance var, including properties defined within the with statement.

There are several ways to work with the properties of local. The easiest way to define a property is by setting it within 'local' directly. This only needs to be done once, after that the property is accessable by just it's name.

local.setDirectly = "directly set value";

console.log(setDirectly);    // logs "directly set value"

An other way to define a property, but than with support for get/setters as well as options on enumerabiliy and write access, is to use the define function. Expect the same behavior as from Object.defineProperty.

You could for instance add a time property that returns the current time.

define("time", {
    get: function(){
        var date = new Date();
        return date.getHours() + ":" + ("0" + date.getMinutes()).substr(-2);
    }
})

console.log(time);

Or you could create a counter property that increments each time it's accessed, placed within a nested closure to protect the counters own variable from unwanted changes.

(function (){
    var counterValue = 0;
    define("count", {get: function(){ return counterValue++ }});
}());

console.log(count);          // logs 0
console.log(count);          // logs 1

When you combine all this you will get something similar to the following code

// This closure represeents any function, class or other scoped block.
(function(){
    // This is where we store the local property. (except: var, let, const)
    const local = {};

    // The define function is used to declare and define the local properties.
    function define(name, descriptor){ Object.defineProperty(local, name, descriptor); }

    // This with statement extends the current scope with local.
    with(local){
        // This is where your code goes.

        // Defining a variable directly into local.
        local.setDirectly = "directly set value";
        console.log(setDirectly);    // logs "directly set value"
        // Defining local properties with the define function
        // For instance a time variable that return the current time (Hours:Minutes)
        define("time", {
            get: function(){
                var date = new Date();
                return date.getHours() + ":" + ("0" + date.getMinutes()).substr(-2);
            }
        })
        console.log(time);           // logs HH:MM

        // Or a counter property that increments each time it's been accessed.
        (function (){
            var counterValue = 0;
            define("count", {get: function(){ return counterValue++ }});
        }());
        console.log(count);          // logs 0
        console.log(count);          // logs 1
        console.log(count);          // logs 2
        console.log(count);          // logs 3
    }
}());

Like I mentioned before, it is important to understand the implications of using the with statement. More information on with can be found at MDN - with. As the question states, it's a search to how you could, not how you should. Use the information on MDN to see if it fits your situation.

查看更多
戒情不戒烟
4楼-- · 2019-02-01 18:02

It looks like the answer is No. I have been searching for behavior like this for quite a while. I have not been able to come up with any passable solution. This SO question seems similar. Python has the nice locals keyword.

查看更多
登录 后发表回答