Virtual Objects

In a nutshell, this proposal restores the distinction between “fundamental” and “derived” traps that was lost in going from the original proxies API to the later direct proxies API.

There are use cases that require “virtual” object abstractions (i.e. proxies that are not necessarily backed by an existing Javascript object). This proposal defines a built-in Proxy Handler that eases the creation of such abstractions. Although this library can in principle be expressed in pure Javascript, it is proposed as a standard library so that it can evolve “in sync” with the language, i.e. in case the Proxy Handler API changes in future editions. Virtual object abstractions that make use of this library handler would then be as forward-compatible as practically possible.

To create a virtual object using the direct proxies API, one should use a fresh, empty object as the target of a proxy and then implement all the traps so that none of them defaults to forwarding to the target. As long as the proxy does not expose non-configurable properties or becomes non-extensible, the target object is fully ignored (except to acquire internal properties such as [[Class]]).

The Handler constructor introduced below helps programmers implement virtual objects since it already provides a full implementation of the Handler API. In particular, fundamental traps (those traps that cannot be implemented in terms of other traps) still forward to the target, but all derived traps are implemented by calling back on the fundamental traps. The expected use case is for users to “subclass” the Handler abstraction and override the fundamental traps. Programmers can also override derived traps if necessary, but this is not required.

Handler

The Handler constructor function defines a handler API that mimics the original proxies API as follows: when a Handler instance is queried for a trap:

  • If the trap is a fundamental trap, forward the operation to the target object as usual.
  • If the trap is a derived trap, the Handler implements the trap’s “default implementation” by calling back on the fundamental traps (via self-sends).

The intent is for clients to be able to “subclass” the Handler, override just the fundamental traps, and inherit all the derived trap implementations.

// fundamental traps
getOwnPropertyDescriptor
getOwnPropertyNames
getPrototypeOf
defineProperty
deleteProperty
preventExtensions
isExtensible
apply
 
// derived traps (dependent on)
seal (preventExtensions, getOwnPropertyNames, defineProperty)
freeze (preventExtensions, getOwnPropertyNames, getOwnPropertyDescriptor, defineProperty)
isSealed
isFrozen
has (getOwnPropertyDescriptor)
hasOwn (getOwnPropertyDescriptor)
get (getOwnPropertyDescriptor)
set (getOwnPropertyDescriptor, defineProperty)
enumerate (getOwnPropertyNames, getOwnPropertyDescriptor)
keys (getOwnPropertyNames, getOwnPropertyDescriptor)
construct (apply, get)

Non-normative implementation

Below is a non-normative implementation of Handler. Any references to methods defined on Object, Function.prototype or Array.prototype are assumed to refer to the built-in implementations). The function normalizeAndCompletePropertyDescriptor is defined here.

function forward(name) {
  return function(...args) {
    return Reflect[name].apply(this,args);
  };
}
 
function Handler() { };
Handler.prototype = {
  // fundamental traps
  getOwnPropertyDescriptor: forward("getOwnPropertyDescriptor"),
  getOwnPropertyNames:      forward("getOwnPropertyNames"),
  getPrototypeOf:           forward("getPrototypeOf"),
  defineProperty:           forward("defineProperty"),
  deleteProperty:           forward("deleteProperty"),
  preventExtensions:        forward("preventExtensions"),
  apply:                    forward("apply"),
 
  // derived traps
  seal: function(target) {
    var success = this.preventExtensions(target);
    success = !!success; // coerce to Boolean
    if (success) {
      var props = this.getOwnPropertyNames(target);
      var l = +props.length;
      for (var i = 0; i < l; i++) {
        var name = props[i];
        success = success && this.defineProperty(target,name,{configurable:false});
      }
    }
    return success;
  },
  freeze: function(target) {
    var success = this.preventExtensions(target);
    success = !!success; // coerce to Boolean
    if (success) {
      var props = this.getOwnPropertyNames(target);
      var l = +props.length;
      for (var i = 0; i < l; i++) {
        var name = props[i];
        var desc = this.getOwnPropertyDescriptor(target,name);
        desc = normalizeAndCompletePropertyDescriptor(desc);
        if (IsAccessorDescriptor(desc)) {
          success = success &&
            this.defineProperty(target,name,{writable:false,configurable:false});
        } else if (desc !== undefined) {
          success = success &&
            this.defineProperty(target,name,{configurable:false});
        }
      }
    }
    return success;
  },
  has: function(target, name) {
    var desc = this.getOwnPropertyDescriptor(target, name);
    desc = normalizeAndCompletePropertyDescriptor(desc);
    if (desc !== undefined) {
      return true;
    }
    var proto = Object.getPrototypeOf(target);
    if (proto === null) {
      return false;
    }
    return Reflect.has(proto, name);
  },
  hasOwn: function(target,name) {
    var desc = this.getOwnPropertyDescriptor(target,name);
    desc = normalizeAndCompletePropertyDescriptor(desc);
    return desc !== undefined;
  },
  get: function(target, name, receiver) {
    var desc = this.getOwnPropertyDescriptor(target, name);
    desc = normalizeAndCompletePropertyDescriptor(desc);
    if (desc === undefined) {
      var proto = Object.getPrototypeOf(target);
      if (proto === null) {
        return undefined;
      }
      return Reflect.get(proto, name, receiver);
    }
    if (isDataDescriptor(desc)) {
      return desc.value;
    }
    var getter = desc.get;
    if (getter === undefined) {
      return undefined;
    }
    return desc.get.call(receiver);
  },
  set: function(target, name, value, receiver) {
    var ownDesc = this.getOwnPropertyDescriptor(target, name);
    ownDesc = normalizeAndCompletePropertyDescriptor(ownDesc);
    if (isDataDescriptor(ownDesc)) {
      if (!ownDesc.writable) return false;
    }
    if (isAccessorDescriptor(ownDesc)) {
      if(ownDesc.set === undefined) return false;
      ownDesc.set.call(receiver, value);
      return true;
    }
    var proto = Object.getPrototypeOf(target);
    if (proto === null) {
      var receiverDesc = Object.getOwnPropertyDescriptor(receiver, name);
      if (isAccessorDescriptor(receiverDesc)) {
        if(receiverDesc.set === undefined) return false;
        receiverDesc.set.call(receiver, value);
        return true;
      }
      if (isDataDescriptor(receiverDesc)) {
        if (!receiverDesc.writable) return false;
        Object.defineProperty(receiver, name, {value: value});
        return true;
      }
      if (!Object.isExtensible(receiver)) return false;
      Object.defineProperty(receiver, name,
        { value: value,
          writable: true,
          enumerable: true,
          configurable: true });
      return true;
    } else {
      return Reflect.set(proto, name, value, receiver);
    }
  },
  enumerate: function* (target) {
    var trapResult = this.getOwnPropertyNames(target);
    var l = +trapResult.length;
    var result = [];
    for (var i = 0; i < l; i++) {
      var name = String(trapResult[i]);
      var desc = this.getOwnPropertyDescriptor(name);
      desc = normalizeAndCompletePropertyDescriptor(desc);
      if (desc !== undefined && desc.enumerable) {
        yield name;
      }
    }
    var proto = Object.getPrototypeOf(target);
    if (proto === null) {
      return;
    }
    yield* Reflect.enumerate(proto);
  },
  keys: function(target) {
    var trapResult = this.getOwnPropertyNames(target);
    var l = +trapResult.length;
    var result = [];
    for (var i = 0; i < l; i++) {
      var name = String(trapResult[i]);
      var desc = this.getOwnPropertyDescriptor(name);
      desc = normalizeAndCompletePropertyDescriptor(desc);
      if (desc !== undefined && desc.enumerable) {
        result.push(name);
      }
    }
    return result;
  },
  construct: function(target, args) {
    var proto = this.get(target, 'prototype', target);
    var instance;
    if (Object(proto) === proto) {
      instance = Object.create(proto);        
    } else {
      instance = {};
    }
    var res = this.apply(target, instance, args);
    if (Object(res) === res) {
      return res;
    }
    return instance;
  }
}

Note how in all cases, the derived traps cause allocations that can be avoided by having the handler directly implement these traps, rather than to inherit these default implementations.

The advantages of having the Handler provide these defaults is that:

  • it reduces the burden on handler implementors, who have to implement less traps.
  • a built-in implementation of the above defaults is likely to be faster than using the above actual Javascript code.
  • it allows us to easily add new derived traps in later versions. Proxy handlers that inherit from Handler will automatically inherit a sensible behavior for the new derived trap.

Draft Specification

Handler ( )

  1. Return undefined.

Handler.prototype.getOwnPropertyDescriptor(T, P)

  1. Return Reflect.getOwnPropertyDescriptor(T,P)

Handler.prototype.defineProperty(T, P, Desc)

  1. Return Reflect.defineProperty(T,P,Desc)

Handler.prototype.getOwnPropertyNames(T)

  1. Return Reflect.getOwnPropertyNames(T)

Handler.prototype.getPrototypeOf(T)

  1. Return Reflect.getPrototypeOf(T)

Handler.prototype.deleteProperty(T, P)

  1. Return Reflect.deleteProperty(T,P)

Handler.prototype.preventExtensions(T)

  1. Return Reflect.preventExtensions(T)

Handler.prototype.isExtensible(T)

  1. Return Reflect.isExtensible(T)

Handler.prototype.apply(T, ThisArg, ArgArray)

  1. Return Reflect.apply(T, ThisArg, ArgArray)

Handler.prototype.enumerate(T)

TODO

Handler.prototype.freeze(T)

  1. Let O be the this value
  2. If Type(O) is not Object throw a TypeError exception.
  3. Let preventExtensions be the result of calling O.[[Get]](”preventExtensions”)
  4. If IsCallable(preventExtensions) is false, throw a TypeError
  1. Let result be the result of calling the [[Call]] method of preventExtensions, passing O as the this value and T as the sole argument.
  2. Let success be ToBoolean(result)
  3. If success is true,
    • a. Let getOwnPropertyNames be the result of calling O.[[Get]](”getOwnPropertyNames”)
    • b. If IsCallable(getOwnPropertyNames) is false, throw a TypeError
    • c. Let ownProps be the result of calling the [[Call]] method of getOwnPropertyNames passing O as the this value and T as the sole argument.
    • d. Let len be the result of calling ownProps.[[Get]](”length”)
    • e. Let l be ToNumber(len)
    • f. Let i be 0
    • g. Repeat, while i < l, do
      • i. Let P be the result of calling ownProps.[[Get]](ToString(i))
      • ii. Let getOwnPropertyDescriptor be the result of calling O.[[Get]](”getOwnPropertyDescriptor”)
      • iii. If IsCallable(getOwnPropertyDescriptor) is false, throw a TypeError
      • iv. Let descObj be the result of calling the [[Call]] method of getOwnpropertyDescriptor passing O as the this value, T as the first argument and ToString(P) as the second argument.
      • v. Let desc be the result of calling normalizeAndCompletePropertyDescriptor(descObj)
      • vi. Let defineProperty be the result of calling O.[[Get]](”defineProperty”)
      • vii. If IsCallable(defineProperty) is false, throw a TypeError
      • viii. If IsDataDescriptor(desc),
        1. Let definePropertyResult be the result of calling the [[Call]] method of defineProperty, passing O as the this value and T as the first, ToString(P) as the second and the property descriptor FromPropertyDescriptor({[[Writable]]:false, [[Configurable]]:false}) as third argument.
        2. If ToBoolean(definePropertyResult) is false, set success to false.
      • ix. Else, If desc is not undefined
        1. Let definePropertyResult be the result of calling the [[Call]] method of defineProperty, passing O as the this value and T as the first, ToString(P) as the second and FromPropertyDescriptor({[[Configurable]]:false}) as third argument.
        2. If ToBoolean(definePropertyResult) is false, set success to false.
      • x. Increase i by 1
  4. Return success

Handler.prototype.seal(T)

  1. Let O be the this value
  2. If Type(O) is not Object throw a TypeError exception.
  3. Let preventExtensions be the result of calling O.[[Get]](”preventExtensions”)
  4. If IsCallable(preventExtensions) is false, throw a TypeError
  5. Let result be the result of calling the [[Call]] method of preventExtensions, passing O as the this value and T as the sole argument.
  6. Let success be ToBoolean(result)
  7. If success is true,
    • a. Let getOwnPropertyNames be the result of calling O.[[Get]](”getOwnPropertyNames”)
    • b. If IsCallable(getOwnPropertyNames) is false, throw a TypeError
    • c. Let ownProps be the result of calling the [[Call]] method of getOwnPropertyNames passing O as the this value and T as the sole argument.
    • d. Let len be the result of calling ownProps.[[Get]](”length”)
    • e. Let l be ToNumber(len)
    • f. Let i be 0
    • g. Repeat, while i < l, do
      • i. Let P be the result of calling ownProps.[[Get]](ToString(i))
      • ii. Let defineProperty be the result of calling O.[[Get]](”defineProperty”)
      • iii. If IsCallable(defineProperty) is false, throw a TypeError
      • iv. Let definePropertyResult be the result of calling the [[Call]] method of defineProperty, passing O as the this value and T as the first, ToString(P) as the second and FromPropertyDescriptor({[[Configurable]]:false}) as third argument.
      • v. If ToBoolean(definePropertyResult) is false, set success to false.
      • vi. Increase i by 1
  8. Return success

Handler.prototype.isSealed(T)

TODO

Handler.prototype.isFrozen(T)

TODO

Handler.prototype.has(T, P)

TODO

Handler.prototype.hasOwn(T, P)

TODO

Handler.prototype.keys(T)

TODO

Handler.prototype.get(T, P, O)

TODO

Handler.prototype.set(T, P, V, O)

TODO

Handler.prototype.construct(T, argArray)

TODO

Open issues

  • Consider making Handler.prototype inherit from the @reflect module instance instead of from Object.prototype. The Handler would then automatically inherit the default forwarding behavior for fundamental traps from this module. See proposal by D. Bruant on es-discuss.

History

  • In a previous version, the fundamental traps of the VirtualHandler were “abstract” methods that threw an exception when called. At the July TC39 Meeting TomVC proposed to change the fundamental traps into forwarding methods, the advantage being that “subclasses” can then just override those fundamental traps they are interested in. All derived traps will then use the overridden version. Non-overridden fundamental traps keep doing the same thing as on a normal handler that doesn’t inherit from Handler.prototype.
  • Handler was previously called VirtualHandler.
 
harmony/virtual_object_api.txt · Last modified: 2012/10/02 14:11 by tomvc
 
Recent changes RSS feed Creative Commons License Donate Powered by PHP Valid XHTML 1.0 Valid CSS Driven by DokuWiki