Search code examples
javascripterror-handlingconstructoronerror

Resume from an error


Before I get yelled at for trying something so reckless, let me tell you that I wouldn't do this in real life and it's an academic question.

Suppose I'm writing a library and I want my object to be able to make up methods as they are needed.

For example if you wanted to call a .slice() method, and I didn't have one then the window.onerror handler would fire it for me

Anyway I played around with this here

window.onerror = function(e) {
    var method = /'(.*)'$/.exec(e)[1];
    console.log(method); // slice
    return Array.prototype[method].call(this, arguments); // not even almost gonna work 
};

var myLib = function(a, b, c) {
    if (this == window) return new myLib(a, b, c);
    this[1] = a; this[2] = b; this[3] = c;
    return this;
};

var obj = myLib(1,2,3);

console.log(obj.slice(1));

Also (maybe I should start a new question) can I change my constructor to take an unspecified amount of args?

var myLib = function(a, b, c) {
    if (this == window) return new myLib.apply(/* what goes here? */, arguments);
    this[1] = a; this[2] = b; this[3] = c;
    return this;
};

BTW I know I can load my objects with

['slice', 'push', '...'].forEach(function() { myLib.prototype[this] = [][this]; });

That's not what I'm looking for


Solution

  • As you were asking an academic question, I suppose browser compatibility is not an issue. If it's indeed not, I'd like to introduce harmony proxies for this. onerror is not a very good practice as it's just a event raised if somewhere an error occurs. It should, if ever, only be used as a last resort. (I know you said you don't use it anyway, but onerror is just not very developer-friendly.)

    Basically, proxies enable you to intercept most of the fundamental operations in JavaScript - most notably getting any property which is useful here. In this case, you could intercept the process of getting .slice.

    Note that proxies are "black holes" by default. They do not correspond to any object (e.g. setting a property on a proxy just calls the set trap (interceptor); the actual storing you have to do yourself). But there is a "forwarding handler" available that routes everything through to a normal object (or an instance of course), so that the proxy behaves as a normal object. By extending the handler (in this case, the get part), you can quite easily route Array.prototype methods through as follows.

    So, whenever any property (with name name) is being fetched, the code path is as follows:

    1. Try returning inst[name].
    2. Otherwise, try returning a function which applies Array.prototype[name] on the instance with the given arguments to this function.
    3. Otherwise, just return undefined.

    If you want to play around with proxies, you can use a recent version of V8, for example in a nightly build of Chromium (make sure to run as chrome --js-flags="--harmony"). Again, proxies are not available for "normal" usage because they're relatively new, change a lot of the fundamental parts of JavaScript and are in fact not officially specified yet (still drafts).

    This is a simple diagram of how it goes like (inst is actually the proxy which the instance has been wrapped into). Note that it only illustrates getting a property; all other operations are simply passed through by the proxy because of the unmodified forwarding handler.

    proxy diagram

    The proxy code could be as follows:

    function Test(a, b, c) {
      this[0] = a;
      this[1] = b;
      this[2] = c;
    
      this.length = 3; // needed for .slice to work
    }
    
    Test.prototype.foo = "bar";
    
    Test = (function(old) { // replace function with another function
                            // that returns an interceptor proxy instead
                            // of the actual instance
      return function() {
        var bind = Function.prototype.bind,
            slice = Array.prototype.slice,
    
            args = slice.call(arguments),
    
            // to pass all arguments along with a new call:
            inst = new(bind.apply(old, [null].concat(args))),
            //                          ^ is ignored because of `new`
            //                            which forces `this`
    
            handler = new Proxy.Handler(inst); // create a forwarding handler
                                               // for the instance
    
        handler.get = function(receiver, name) { // overwrite `get` handler
          if(name in inst) { // just return a property on the instance
            return inst[name];
          }
    
          if(name in Array.prototype) { // otherwise try returning a function
                                        // that calls the appropriate method
                                        // on the instance
            return function() {
              return Array.prototype[name].apply(inst, arguments);
            };
          }
        };
    
        return Proxy.create(handler, Test.prototype);
      };
    })(Test);
    
    var test = new Test(123, 456, 789),
        sliced = test.slice(1);
    
    console.log(sliced);               // [456, 789]
    console.log("2" in test);          // true
    console.log("2" in sliced);        // false
    console.log(test instanceof Test); // true
                                       // (due to second argument to Proxy.create)
    console.log(test.foo);             // "bar"
    

    The forwarding handler is available at the official harmony wiki.

    Proxy.Handler = function(target) {
      this.target = target;
    };
    
    Proxy.Handler.prototype = {
      // Object.getOwnPropertyDescriptor(proxy, name) -> pd | undefined
      getOwnPropertyDescriptor: function(name) {
        var desc = Object.getOwnPropertyDescriptor(this.target, name);
        if (desc !== undefined) { desc.configurable = true; }
        return desc;
      },
    
      // Object.getPropertyDescriptor(proxy, name) -> pd | undefined
      getPropertyDescriptor: function(name) {
        var desc = Object.getPropertyDescriptor(this.target, name);
        if (desc !== undefined) { desc.configurable = true; }
        return desc;
      },
    
      // Object.getOwnPropertyNames(proxy) -> [ string ]
      getOwnPropertyNames: function() {
        return Object.getOwnPropertyNames(this.target);
      },
    
      // Object.getPropertyNames(proxy) -> [ string ]
      getPropertyNames: function() {
        return Object.getPropertyNames(this.target);
      },
    
      // Object.defineProperty(proxy, name, pd) -> undefined
      defineProperty: function(name, desc) {
        return Object.defineProperty(this.target, name, desc);
      },
    
      // delete proxy[name] -> boolean
      delete: function(name) { return delete this.target[name]; },
    
      // Object.{freeze|seal|preventExtensions}(proxy) -> proxy
      fix: function() {
        // As long as target is not frozen, the proxy won't allow itself to be fixed
        if (!Object.isFrozen(this.target)) {
          return undefined;
        }
        var props = {};
        Object.getOwnPropertyNames(this.target).forEach(function(name) {
          props[name] = Object.getOwnPropertyDescriptor(this.target, name);
        }.bind(this));
        return props;
      },
    
      // == derived traps ==
    
      // name in proxy -> boolean
      has: function(name) { return name in this.target; },
    
      // ({}).hasOwnProperty.call(proxy, name) -> boolean
      hasOwn: function(name) { return ({}).hasOwnProperty.call(this.target, name); },
    
      // proxy[name] -> any
      get: function(receiver, name) { return this.target[name]; },
    
      // proxy[name] = value
      set: function(receiver, name, value) {
       this.target[name] = value;
       return true;
      },
    
      // for (var name in proxy) { ... }
      enumerate: function() {
        var result = [];
        for (var name in this.target) { result.push(name); };
        return result;
      },
    
      // Object.keys(proxy) -> [ string ]
      keys: function() { return Object.keys(this.target); }
    };