6

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

  • 2
    You can add methods to any Javascript object, Maps are not special in this – Caleth Jan 12 '17 at 9:39
  • @Caleth, thanks for suggesting to me that adding methods to maps is a "normal JavaScript-y" way of doing things. That does address one of my earlier questions. One of my other questions still remains though: in my example, the speakersAreFeatured method is added separately to both yourCar and myCar...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? – Andrew Willems Jan 12 '17 at 12:22
  • 1
    @AndrewWillems - again, Maps aren't special. You can subclass Map and create objects using your subclass (at least, you can assuming you're using ES6). – Jules Jan 13 '17 at 3:21
  • ... and there's an actual solution for how to do it in current browsers here: 2ality.com/2013/03/subclassing-builtins-es6.html – Jules Jan 13 '17 at 3:31
  • @Jules, it'll take me some time to get my head around Raushmayer's blog that you link to, but it sounds both relevant and intriguing. Combining your comment about subclassing Map with the content of his article, are you saying that I can use class CarMap extends Map {...}? If so, I'm not yet clear on how to: build the constructor, use super.call(...), create the actual map (if that's different than super.call(...), attach my speakersAreFeatured 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. – Andrew Willems Jan 13 '17 at 12:30
2

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
0

(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 Jan 12 '17 at 12:20
0

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 Jan 13 '17 at 12:38

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.