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
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.
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.
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)
}
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.
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.