JavaScript maps (the data type, not the array method) seem set up to accept data (key/value pairs) but not necessarily methods. At least they're not advertised that way. However, we can put methods onto a map instance. Intriguingly, the keyword this
works in such methods, and returns the map itself. For example:
const m = new Map();
m.set('key1', 'value1');
m.get('key1'); // returns 'value1', i.e. standard map usage
m.methodA = function(x) {console.log(x + ' to you too');};
m.methodA('hello'); // shows 'hello to you too'
m.methodB = function() {console.log(this.get('key1'));};
m.methodB(); // shows 'value1'
Is this a proper use of maps and/or methods within maps and/or this
within methods within maps? Or am I corrupting something somehow or breaking some rules by doing this? It seems fairly straight forward and reasonable, making me think it should be OK, but I've never seen or heard anything about this before which makes me nervous.
I can't create a map with a constructor the way I can create an object with a constructor. However, I can create a map factory to produce maps of a given "type". For example, I can use a factory to create maps of the "car type". I can thus also attach methods to each map of this "type" by including them in the factory:
const createCarMap = function(features) {
const carMap = new Map(features);
carMap.set('# tires', 'four (assumed)');
carMap.speakersAreFeatured = function() {
return this.has('speakers');
};
return carMap;
};
const yourCar = createCarMap([
['# cylinders', 'twelve'],
['speakers', 'awesome']
]);
const myCar = createCarMap([
['exterior', 'pearly white']
]);
yourCar.speakersAreFeatured(); // returns true
myCar.speakersAreFeatured(); // returns false
However, such a method will be attached repeatedly for every map produced. This is in contrast to how methods can be added to an object's prototype, allowing method re-usability. So, can methods be attached to a map in a way that allows method re-usability?
My more general question is: Should we be using methods on maps. If so, how? Should we think of them and use them essentially the same way we do with objects, or are there "new rules" for using them with maps? (I suppose one could ask similar questions about other new-ish data types too, e.g. sets, weakMaps, etc., but I'm limiting myself here to maps.)
3 Answers 3
Inspired by comments from @Jules after the original question, I figured out the following: If you are using ES6, you can create a new class that extends the built-in Map data type. The constructor should pass any needed parameters to a super
call, creating the map and placing it in this
. Then, any code within the class that needs to use the map, either in the constructor or in any of its methods, just uses this
.
class CarMap extends Map {
constructor(features) {
super(features);
this.set('# tires', 'four (assumed)');
}
speakersAreFeatured() {
return this.has('speakers');
}
}
const yourCar = new CarMap([
['# cylinders', 'twelve'],
['speakers', 'awesome']
]);
const myCar = new CarMap([
['exterior', 'pearly white']
]);
console.log(yourCar.speakersAreFeatured()); // --> true
console.log(myCar.speakersAreFeatured()); // --> false
(Changing from comment to answer)
Why don't you extend Map
with:
Map.prototype.speakersAreFeatured = function() {
return this.has('speakers');
};
This will define it for all instance of Map.
-
Interesting idea. However, I don't want to extend all maps with that method, only my so-called carMaps.Andrew Willems– Andrew Willems2017年01月12日 12:20:43 +00:00Commented Jan 12, 2017 at 12:20
Update: This solution does work and, as far as I can tell, doesn't break any rules. However, it is an older ES5-like solution to the problem. See my other answer for a more ES6-like solution.
I suppose one solution would be to bury the map within a normal object as one of that object's properties (e.g. mapObj
) and then attach the methods to that object rather than to the map itself. Then have the map-specific methods refer to the map as a property of the parent object, i.e. refer to this.mapObj
rather than simply this
. This seems a little convoluted/hacky as the only purpose of the parent object (instantiated from the Car
class in the example below) is to allow for code re-use of the methods, but at least it does just that.
(Just to clarify a choice of variable name below: In the example below I call the property that contains the map mapObj
instead of just map
because the latter could be confusing as there is already a standard map
method on arrays that this might otherwise get mixed up with.)
In the example below, the Car
class passes the features
into an internal map factory. Then, note the difference between the two variations of the body of the speakersAreFeatured
method:
as shown in the question:
return this.has('speakers');
as shown in the code below:
return this.mapObj.has('speakers');
Code:
class Car {
constructor(features) {
this.mapObj = this.createCarMap(features);
}
createCarMap(features) {
const carMap = new Map(features);
carMap.set('# tires', 'four (assumed)');
return carMap;
}
speakersAreFeatured() {
return this.mapObj.has('speakers');
}
}
const yourCar = new Car([
['# cylinders', 'twelve'],
['speakers', 'awesome']
]);
const myCar = new Car([
['exterior', 'pearly white']
]);
yourCar.speakersAreFeatured(); // returns true
myCar.speakersAreFeatured(); // returns false
-
While the comments after the original question contain important discussion about how to solve this problem using more modern ES6 approaches, the approach in this answer seems to have been suggested by at least one other person who says that one pre-ES6 way to extend maps is to use a "Factory/constructor [which] makes objects with an internal Map instance that you delegate to" which is, as I understand it, what I tried to do in this answer.Andrew Willems– Andrew Willems2017年01月13日 12:38:52 +00:00Commented Jan 13, 2017 at 12:38
speakersAreFeatured
method is added separately to bothyourCar
andmyCar
...is there a way of re-using such a method, like you can when you attach a method not to an object but to an object prototype?class CarMap extends Map {...}
? If so, I'm not yet clear on how to: build the constructor, usesuper.call(...)
, create the actual map (if that's different thansuper.call(...)
, attach myspeakersAreFeatured
method, etc. It would be great if you could put together an example as an answer, tho' I understand that that might be a lot of work.