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.
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:
target object as usual.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)
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:
Handler will automatically inherit a sensible behavior for the new derived trap.Handler ( )
Handler.prototype.getOwnPropertyDescriptor(T, P)
Handler.prototype.defineProperty(T, P, Desc)
Handler.prototype.getOwnPropertyNames(T)
Handler.prototype.getPrototypeOf(T)
Handler.prototype.deleteProperty(T, P)
Handler.prototype.preventExtensions(T)
Handler.prototype.isExtensible(T)
Handler.prototype.apply(T, ThisArg, ArgArray)
Handler.prototype.enumerate(T)
TODO
Handler.prototype.freeze(T)
Handler.prototype.seal(T)
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
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.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.