Why isn't Map subclassable in chrome/node?

2020-04-10 01:52发布

问题:

So ES 6 is bringing us Maps (and none too soon). Wishing to subclass Map for my own nefarious purposes, I tried the following (abbreviated for clarity):

function Foo() {
    return Map.apply(this, [].slice.call(arguments));
}

var bar = new Foo();

In V8 environments this throws an Error "Map constructor not called with 'new'". Why? SpiderMonkey gets this 'right':

Map.call({}, [['foo', 'bar']]).get('foo');

will, as expected, yield 'bar'. In both SpiderMonkey and V8 attempting something like

function Foo() {};
Foo.prototype = new Map();
var bar = new Foo();
bar.set('foo', 'bar');

will fail: 'method set called on incompatible object'. So there goes that inheritance pattern. As far as I can tell from the spec (I don't have much spec-foo), actual Map objects have internal properties that are not accessible that are required for them to work properly. But why does V8 throw an error on the first pattern? This seems like an odd decision, especially when it works as expected in FF.

UPDATE: I noticed that FF and chrome both have Object.setPrototypeOf implemented. Just in case anyone stumbles across this and thinks of that, I can tell you it fails. Both of the following fail for different reasons:

//this totally fails, even for custom constructors/objects
var Foo = function(){};
Object.setPrototypeOf(Foo, Map.prototype);
var bar = new Foo(); //bar doesn't have any of the Map properties/methods

//this one has the methods but still throws the 'incompatible object'
//err. Also fails with new Map() instead of Map.prototype
var bar = Object.setPrototypeOf({}, Map.prototype); 

TL;DR

There are basically four ways to extend map (some from answers/comments below):

  1. Add methods to Map.prototype (shame on you).
  2. Factory/constructor makes objects with an internal Map instance that you delegate to
  3. Mixin fn that copies properties onto Maps:
function Foo(){this.bar = 'boo!'}
var baz = new Map();
Foo.call(baz);
baz.bar; //yields 'boo!'
baz.set('5', 5).get('5'); //yields 5

Or just wait for ES 6 classes to hit the platform(s) you care about

回答1:

In V8 environments this throws an Error "Map constructor not called with 'new'". Why?

Because new ES6 classes (including builtin ones) are supposed to be only constructable with new.

SpiderMonkey gets this 'right'

Not exactly. The spec explicitly says

Map is not intended to be called as a function and will throw an exception when called in that manner.

Wishing to subclass Map

Yes, that's the appropriate thing:

The Map constructor is designed to be subclassable. It may be used as the value in an extends clause of a class definition. Subclass constructors that intend to inherit the specified Map behaviour must include a super call to the Map constructor to create and initialize the subclass instance with the internal state necessary to support the Map.prototype built-in methods.

So you'll want to use

class Foo extends Map {
    // default constructor
}
var bar = new Foo();
bar.set('foo', 'bar');


回答2:

Actually, it doesn't really 'work' in FF, since FF also allows to create maps simply by calling Map().

However, according to http://kangax.github.io/compat-table/es6/ we do not have compatibility for subclassing in modern browser (fascinating enough, IE has some support here).

tl;dr V8/SpiderMonkey are not fully ES6 compatible yet.



回答3:

You should be able to do something like this:

function Foo() {
    return new (Map.bind(this, [].slice.call(arguments)));
}

var bar = new Foo();


回答4:

For anyone running into this in 2018:

import ES6Map from 'es6-map/polyfill'

class MyMap extends ES6Map {
  constructor () {
    super()
  }
}