5

I want to get get unique characters from some string using ES6's Set. So let's assume that we have string var str = 'abcdeaabc'; and create set of characters from this string:

var str = 'abcdeaadbc';
var chars = new Set(str);

Now we have the set of unique characters: abcd. I was surprised that char set has forEach method and has no map method.

Set is iterable object, so why can't we use map function to iterate over set?

I tried to pass chars set and chars.values() SetIterator to Array.prototype.map this way:

Array.prototype.map.call(chars, function(element) {...});
Array.prototype.map.call(chars.values(), function(element) {...});

But every try failed.

For example, let's assume that we want to get unique symbols from string and return an array of them with preceding underscore. eg.: ['_a', '_b', '_c', '_d']

Here are my two solutions:

// First:
var result = Array.from(chars).map(function(c) {
    return '_' + c;
});

// Second:
var result = [];
chars.forEach(function(c) {
    this.push('_' + c);
}, result);

But is there a way of invoking Array.prototype's map function with Set context? And if no - why?

Yuriy Yakym
  • 3,616
  • 17
  • 30
  • Did you try using `Set.entries`? Actually, on second thought, I don't think that will work... – Mark C. May 11 '16 at 21:21
  • 2
    http://stackoverflow.com/a/35374005/5812121. Also as an alternative: `let result = [...chars].map(c => \`_${c}\`);` – timolawl May 11 '16 at 21:37

3 Answers3

6

Array.prototype.map works on array-like types that have integer indexes and a length property. Set and Map are general iterable types, but they are not array-like, so Array.prototype methods will not work on them. e.g.

var s = new Set([1, 2, 3]);
s.length === undefined;
s[0] === undefined

The main approach is Array.from kind of like your solution, but one thing to note is that Array.from takes a mapFn as the second argument, so you can do

let results = Array.from(chars, c => `_${c}`);

for a nice and short map to convert an iterable to an array.

loganfsmyth
  • 156,129
  • 30
  • 331
  • 251
  • set entries aren't accessible through index , but order is guaranteed ... I can't get over this :) – maioman May 11 '16 at 21:37
  • MDN sais that `Set` is iterable. And `Array.prototype.map`s specification sais that map gets iterable object: [MDN: map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map). So why can't I pass set as an argument to `map`? – Yuriy Yakym May 11 '16 at 21:37
  • @YuriyYakym I think it's because a Set instance cannot be indexed with numeric indices like an array can, and that's what the array functions like `.map()`, `.filter()`, `.forEach()`, etc rely on. – Pointy May 11 '16 at 21:40
  • Look at the [Polyfill](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map#Polyfill) on that same page, which is a step-by-step implementation of the spec, and you'll see it requires a `length` property, and that the values be addressable by index. – Heretic Monkey May 11 '16 at 21:40
  • @YuriyYakym That link is for `Map`, not `Array.prototype.map` – loganfsmyth May 11 '16 at 21:42
  • @loganfsmyth Oh, excuse me, I haven't noticed. Now it's getting clearer. – Yuriy Yakym May 11 '16 at 21:47
1

Because even if map is generic enough to work for arbitrary array-like objects, not all iterable objects are array-like.

map just gets the length property, and then iterates properties from 0 to length-1. That can't work with sets because they have a size instead of length, and they don't provide random access to its elements.

If you really want to use map, you will need to transform the set to an array, and then transform the result back to a set.

var newSet = new Set(Array.from(set).map(myFunc));

But there is no point in doing that, it would be better to use set operations only:

var newSet = new Set();
for(let val of set) newSet.add(myFunc(val));

Or maybe a self-invoked generator function:

var newSet = new Set(function*() {
  for(let val of set) yield myFunc(val);
}());
Oriol
  • 274,082
  • 63
  • 437
  • 513
  • Mapping over the result of `Array.from` will create a temporary array for no reason. You cal pass the callback as the second param. – loganfsmyth May 11 '16 at 21:44
1

If you are desperate you can do like this

var str = 'abcdeaadbc';
var chars = new Set(str);
document.write("<pre>" + JSON.stringify([...chars].map(c => c+"up")) + "</pre>")
Redu
  • 25,060
  • 6
  • 56
  • 76
  • Spreading and then mapping will create a temporary array for no reason. You can use `Array.from` to make an array and map at the same time. – loganfsmyth May 11 '16 at 21:45
  • 1
    @loganfsmyth : The Array.from() method creates a new Array instance from an array-like or iterable object. (this is the first line of `Array.from()` description at MDN) https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/from This is very practical if you would like to use an array method aftervards like functional programming or pass the unique elements to a function as an argument array. There is no need to create a wasteful array for that. – Redu May 11 '16 at 21:47
  • My point is `[...chars].map(fn)` and `Array.from(chars).map(fn)` both create an array, then call `.map` to return a second array, whereas you can do `Array.from(chars, fn)` to create avoid creating the first temporary array. – loganfsmyth May 11 '16 at 22:08