(Weak){Set|Map} subclassing

Allen Wirfs-Brock allen at wirfs-brock.com
Fri Nov 30 17:40:35 PST 2012

On Nov 30, 2012, at 2:20 PM, Jason Orendorff wrote:

> On Tue, Nov 27, 2012 at 12:40 PM, Allen Wirfs-Brock <allen at wirfs-brock.com> wrote:
> On Nov 27, 2012, at 5:07 AM, David Bruant wrote:
> >>    WeakMap.call(o);
> >>    WeakSet.call(o);
> >>    Map.call(o);
> >>    Set.call(o);
> >>
> >> Currently, this works and makes o a weakmap, a weakset, a map and a set...
> Sort of.  They won't inherit from any of the corresponding prototype types so none of the relevant methods will be available on those objects.  However they could be explicitly invoked on the object.  If you want to make them all available then the individual methods would have to be installed with non-conflicting names.
> But overall, why should the above be a problem?  It is how ES "construction" works and the way things work for any user defined objects.  Why should built-ins be any different?
> I still don't care for this. It adds a level of indirection, people seem to find it surprising, and I don't see the benefit.
> Here's a counterproposal, another way to support subclassing builtins. Suppose we introduce a method, Object.prototype.@@create, that's called when you call Object.create(x) or new Object. It takes no arguments except 'this'; all it does is create a new object with 'this' as the [[Prototype]]:
>     Object.create(x)   <==>   x.@@create()
>     new C(x, y)   <==>   C.call(C.prototype.@@create(), x, y)
> Then add Map.prototype.@@create which creates a Map with empty [[MapData]]. Likewise Set.prototype.@@create and so on.

This is not unreasonable. It is actually pretty great but I think needs a little tweaking. Let me try to explain in somewhat different terms and with a slight variation.

The ordinary [[Construct]] internal method (which is what the new operator actually uses) when invoked upon a constructor function performs  two steps. It first instantiates a new object instances (and sets its [[Prototype]] internal property).  It then calls the constructor function using the new object as the this value so that the constructor can perform initialization.

In the past on this list we have discussed the utility of being able to separately over-ride the [[Construct]] and [[Call]] behavior of a function (and Proxies enable this).  What Jason is proposing is a variation of this.  Instead of exposing a @@method that completely replaces [[Construct]] he is proposing a method that replaces the allocation step of [[Construct]].  Such a method, in addition to performing actual allocation, could also perform any allocation time initialization that should be separate from the constructor functionality. This permits the constructor function to be super invoked without doing allocation-time initialization upon the subclass instance.

Overall, I like this approach. However, I don't think @@create belongs on the prototype object.  This make the @@create functionality for a particular kind of object available to anyone who gets their hands on  an instance object of the class.  This smells like a capability leak.

Instead, I would make @@create a property of the actual constructor function and I would place the default @@create on Function.prototype, which all functions inherit from.

So roughly speaking, Foo.[[Constructor]](...args) would be defined as:
1) Let creator be Foo.[[Get]](@@create)
2 ) Let newObj be creator.call(foo).  //Foo is passed as the this value to @@create
3)  Let ctorResult be Foo.[[call]](newObj,args)
4)  If Type(ctorResult) is Object, return ctorResult
5) else return newObj

The definition of the Function.prototype.@@create would be loosely

function() {
    return Object.create(this.prototype);

So, Map would be defined with a Map.@@create method that in sorta specTalk would say something like:
1) Let obj be a new ordinary object.
2) Let proto be this.[[Get]]('Prototype').
2) Set the [[Prototype]] of obj to proto.
3)  Add a [[MapData]] internal property to obj.
4) Return obj

The actual Map constructor function would be specified just like  it is currently.  It primarily deals with initialize the new object from arguments passed to the constructor.
Then somebody could code:

class MyMap extends Map{
    constructor (...args) {
        this.myAnswer = 42

And then
   new MyMap();
would produce an object that has full map functionality but also has a myAnswer property.

so, I'm in!  I should have done this originally.  I didn't because I was avoiding tackling the problem of providing separate call and construct entry points for constructors at this time.  But it really looks like that is the best way to support built-in subclassing.

> I think the reason I don't agree with the "this is how things work for user-defined objects" argument is that user-defined objects, by necessity, build their functionality compositionally out of the stuff the language and host already provide. Map and other builtin types are not like that. They exist specifically because they can't be built from other JS parts. There are already hundreds of such classes in the DOM.

Not all built-ins and DOM objects  have the characteristic that they can't be built from JS parts. We should want to reduce that hundreds to a handful.

What's nice about this solution is that applies to both built-in and self-hosted objects.

A self-hosted implementation of Set (which might be implemented using a Map) could defined its @@create as:

let setDate =  new Symbol(true);  //private name, as of this week @-name have gone away and [ ] for symbol keyed property access is back
class Set {
  constructor (args) {
     //use args to populate this object
     //likely uses setData property of a Set instance
Set[Symbols.create] = function() {     //a
      let obj = super();   //create an ordinary object inheriting from Set.prototype
      obj[setData] = new Map();
      return obj

Good job, Jason!


> -j
> _______________________________________________
> es-discuss mailing list
> es-discuss at mozilla.org
> https://mail.mozilla.org/listinfo/es-discuss

-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://mail.mozilla.org/pipermail/es-discuss/attachments/20121130/271f9ed2/attachment.html>

More information about the es-discuss mailing list