17

Trying to get away with custom get/set functionality on ES6 Maps. Currently using Babel to transpile my code to ES5.

Chrome Version 41.0.2272.101 m

class MyMap extends Map {
    get(key) {
        if (!this.has(key)) { throw new Error(...); }
        return super.get(key);
    }

    set(key) {
        if (this.has(key)) { throw new Error(...); }
        return super.set(key);
    }
}

Not sure if I just got the syntax wrong or I'm missing an implementation of some sort. But I get the following error:

Method Map.prototype.forEach called on incompatible reciever

Nate-Wilkins
  • 5,364
  • 4
  • 46
  • 61
  • I just get `Block-scoped declarations (let, const, function, class) not yet supported outside strict mode` – thefourtheye Apr 03 '15 at 14:53
  • And I get `Uncaught SyntaxError: Unexpected reserved word`. Chrome might not really support extending built-in classes yet. – Felix Kling Apr 03 '15 at 14:57
  • Yup, in io.js I am able to compile it without any errors (though I had to have `"use strict"` at the top) – thefourtheye Apr 03 '15 at 14:58
  • @FelixKling And of course I forgot to mention that I'm using babel to transpile... – Nate-Wilkins Apr 03 '15 at 15:11
  • 1
    Ah... I don't think it's possible to have "ES5 classes" extend built-in ES6 classes, because it requires to call the parent constructor in a way that is not allowed by ES6 classes. See http://stackoverflow.com/q/28900954/218196 – Felix Kling Apr 03 '15 at 15:13
  • Crap I was afraid of that... Thanks for the help! – Nate-Wilkins Apr 03 '15 at 15:20

5 Answers5

9

Babel explictly states they do not support extending built-in classes. See http://babeljs.io/docs/usage/caveats/#classes. The reasons are not quite as simple as "limitations in ES5", however, since Map is not an ES5 feature to begin with. It appears that implementations of Map do not support basic patterns such as

Map.prototype.set.call(mymap, 'key', 1);

which is essentially what Babel generates in this case. The problem is that implementations of Map including V8 are overly restrictive and check that the this in the Map.set.call call is precisely a Map, rather than having Map in its prototype chain.

Same applies to Promise.

  • 3
    So you have to re-implement every method of Map as a proxy to an internal Map?! That seems highly screwy. Seems the Babel folks must not actually use Maps. ┻━┻ ︵ ლ(⌒-⌒ლ) – Sukima Feb 16 '16 at 22:15
  • You could implement your own methods such as `guardedSet` and guardedGet` on the `Map` prototype. –  Feb 17 '16 at 04:44
3

You should use the good old way:

function ExtendedMap(iterable = []) {
  if (!(this instanceof ExtendedMap)) {
    throw new TypeError("Constructor ExtendedMap requires 'new'");
  }

  const self = (Object.getPrototypeOf(this) === Map.prototype) 
    ? this 
    : new Map(iterable);
  Object.setPrototypeOf(self, ExtendedMap.prototype);

  // Do your magic with `self`...

  return self;
}

util.inherits(ExtendedMap, Map);
Object.setPrototypeOf(ExtendedMap, Map);

ExtendedMap.prototype.foo = function foo() {
  return this.get('foo');
}

Then use new as usual:

const exMap = new ExtendedMap([['foo', 'bar']]);
exMap instanceof ExtendedMap; // true
exMap.foo(); // "bar"

Notice that the ExtendedMap constructor ignore any this binding that isn't a Map.

See also How to extend a Promise.

Moshe Simantov
  • 3,937
  • 2
  • 25
  • 35
1

Alternatively, you can compose the Map() class within your MapWrapper object and expose your own API. The notion of object Composition was not of much in use 5 yrs ago and so, the question and answers were tied to and tangled with Inheritance.

  • Hey Murali! Thanks for the answer! I'm not sure what you mean by that? Like using composition within the class? (ie MyClass has some internal map obj that I can use?) I haven't run into this problem as I don't really think the side effects are a good idea anymore... – Nate-Wilkins May 30 '20 at 04:42
0

Yup, until Proxies arrive in full force the only way to achieve what you were trying to do is to shadow the built-in methods on the Map/Set, etc. yourself.

For instance, if you have your map like so:

var myMap = new Map([ ['key1', 'value1'], ['key2', 'value2']])

You'd have to have some wrapper to pass it into to add the built-in methods, for instance for get/set:

function proxify(obj){
    var $fnMapGet = function(key){
        console.log('%cmap get', 'color:limegreen', 'key:', key)
        if(!Map.prototype.has.call(this, key)){
            throw(new Error('No such key: '+ key))
        } else {
            return Map.prototype.get.call(this, key)
        }
    }
    var $fnMapSet = function(key, value){
        console.log('%cmap set', 'color:tomato', 'key:', key, 'value:', value)
        if(Map.prototype.has.call(this, key)){
            throw(new Error('key is already defined: ' + key))
        } else {
            if(Map.prototype.get.call(this, key) == value){
                console.log('%cmap set', 'color:tomato', '*no change')
                return this
            }
            return Map.prototype.set.call(this, key, value)
        }
    }

    Object.defineProperty(obj, 'get', {
        get(){
            return $fnMapGet
        }
    })
    Object.defineProperty(obj, 'set', {
        get(){
            return $fnMapSet
        }
    })

    return obj
}

So then:

proxify(myMap)

myMap.get('key1') // <= "value1"
myMap.get('key2') // <= "value2"
myMap.get('key3') // <= Uncaught Error: No such key: key3
myMap.set('key3', 'value3') // <= Map {"key1" => "value1", "key2" => "value2", "key3" => "value3"}
myMap.set('key3', 'another value3') // <= Uncaught Error: key is already defined: key3

That would add the ability to do your own custom set/get on the map, not nearly as nice as subclassing Map nor as straightforward as es6 proxies but it at least it works.

The full code snippet running below:

var myMap = new Map([ ['key1', 'value1'], ['key2', 'value2']])

function proxify(obj){
 var $fnMapGet = function(key){
  console.log('get key:', key)
  if(!Map.prototype.has.call(this, key)){
   throw(new Error('No such key: '+ key))
  } else {
   return Map.prototype.get.call(this, key)
  }
 }
 var $fnMapSet = function(key, value){
  console.log('set key:', key, 'value:', value)
  if(Map.prototype.has.call(this, key)){
   throw(new Error('key is already defined: ' + key))
  } else {
   if(Map.prototype.get.call(this, key) == value){
    console.log('*no change')
    return this
   }
   return Map.prototype.set.call(this, key, value)
  }
 }

 Object.defineProperty(obj, 'get', {
  get(){
   return $fnMapGet
  }
 })
 Object.defineProperty(obj, 'set', {
  get(){
   return $fnMapSet
  }
 })

 return obj
}

proxify(myMap)
myMap.get('key1')
myMap.get('key2')
try {
  myMap.get('key3')
} catch(ex){
  console.warn('error:', ex.message)
}
myMap.set('key3', 'value3')
try {
  myMap.set('key3', 'another value3')
} catch(ex){
  console.warn('error:', ex.message)
}
ragamufin
  • 4,113
  • 30
  • 32
0

Unfortunately, Babel doesn't support it. The odd thing is, you can run the following in your console:

clear();

var Store = function Store(data) {
    // var _map = new Map(data);
    this.get = function get(key) {
        console.log('#get', key);
        return S.prototype.get.call(S.prototype, key);  // or return _map.get(key);
    };
    this.set = function set(key, value) {
        S.prototype.set.call(S.prototype, key, value);  // or _map.set(key, value);
        return this;
    };
};
Store.prototype = new Map();  // we could just wrap new Map() in our constructor instead

var s = new Store();

s.set('a', 1);
s.get('a');

However, running the following with Babel is useless:

class Store extends Map {
    constructor(...args) {
        super(...args);
        return this;
    }
}

You'll throw an error trying to call (new Store(['a','1'])).get('a'). This horrifies me that something as important as Map would be utterly dismissed by the folks at Babel.

Here's what I recommend. What I've been doing for several years is create an JavaScript Class which you can tote around with you to any gig or project. Call it "Dictionary", and if your environment supports Map and you need a map, just wrap Map -- for performance sake. If you need to inherit from Map, inherit from Dictionary. I actually have my own private repo with various algorithms & data-structures that I bring everywhere I go, but you can also find public repos that accomplish the same thing. Kind of a pain, but that way you're not relying 100% on the same thing for every framework & environment.

Cody
  • 9,785
  • 4
  • 61
  • 46