1

Suppose I've a Set as a lookup table.

const myset = new Set(["key1", "key2", "key3", "keepMe"]);

I wanted to filter another array of some other keys (say mykeys) which are in myset.

const mykeys = ["ignoreMe", "keepMe", "ignoreMeToo", "key2"];

Question:

Why do I have to use

const filtered = mykeys.filter(k => myset.has(k))

instead of

const filtered = mykeys.filter(myset.has)
// TypeError: Method Set.prototype.has called on incompatible receiver undefined

i.e., why do I've to create an anonymous lambda function in filter? keys.has has same signature (argument - element, return boolean). A friend told me it's related to this.

Whereas mykeys.map(console.log) works without error (although not being of much use).

I came across this article at MDN and I still don't get why "'myset' is not captured as this". I understand the workaround but not the "why". Can anyone explain it with some details and references in a human friendly way?

Update: Thank you all for the responses. Maybe I wasn't clear about what I'm asking. I do understand the workarounds.

@charlietfl understood. Here's his comment, the thing I was looking for:

Because filter() has no implicit this where as set.has needs to have proper this context. Calling it inside anonymous function and manually adding argument makes the call self contained.

Sid Vishnoi
  • 1,250
  • 1
  • 16
  • 27
  • Try `mykeys.filter(myset.has.bind(myset))` ... which is actually more to write than using the anonymous arrow function – charlietfl May 26 '18 at 15:04
  • I understand the workarounds. But I'm interested in knowing the "why" part :) – Sid Vishnoi May 26 '18 at 15:05
  • Because context of `this` in `filter()` is not the Set – charlietfl May 26 '18 at 15:06
  • Any references to understand this one better? I don't mean to be clingy, but why is the "context of `this` in `filter` is not the set"? – Sid Vishnoi May 26 '18 at 15:07
  • 1
    See if this helps https://jsfiddle.net/2n2ea6ea/1 – charlietfl May 26 '18 at 15:11
  • Thank you sir! It does help. But I'm still not clear on the "why" `this` would be window here. Maybe I need to read more (maybe the spec) – Sid Vishnoi May 26 '18 at 15:16
  • 1
    Because filter() has no implicit `this` where as set.has needs to have proper `this` context. Calling it inside anonymous function and manually adding argument makes the call self contained – charlietfl May 26 '18 at 15:29
  • @charlietfl well this one line is much more helpful than anything :) So it is how it got implemented in language. Any specific reasons? – Sid Vishnoi May 26 '18 at 15:36
  • 1
    [How does the `this` keyord work](https://stackoverflow.com/questions/3127429/how-does-the-this-keyword-work) As for "why"...who knows. Just have to know how it does work – charlietfl May 26 '18 at 15:38

2 Answers2

1

This is a fundamental design decision dating back to the first definition of the JavaScript language.

Consider an object

var myObjet = {
  someValue: 0,
  someFunction: function() {
    return this.someValue;
  }
};

Now, when you write

var myValue = myObject.someValue;

You get exactly what you have put in it, as if you had written

var myValue = 0;

Similarly, when you write

var myFunction = myObject.someValue;

You get exactly what you have put in it, as if you had written

var myFunction = (function() {
  return this.someValue;
});

...except now, you are not in an object anymore. So this doesn't mean anything. Indeed, if you try

console.log(myFunction());

you will see

undefined

exactly as if you had written

console.log(this.someValue);

outside of any object.

So, what is this? Well, JavaScript decides it as follows:

  • If you write myObject.myFunction(), then when executing the myFunction() part, this is myObject.
  • If you just write myFunction(), then this is the current global object, which is generally window (not always, there are many special cases).
  • A number of functions can inject a this in another function (e.g. call, apply, map, ...)

Now, why does it do this? The answer is that this is necessary for prototypes. Indeed, if you now define

var myDerivedObject = Object.create(myObject);
myDerivedObjet.someValue = 42;

you now have an object based on myObject, but with a different property someValue

console.log(myObject.someFunction()); // Shows 0
console.log(myDerivedObject.someFunction()); // Shows 42

That's because myObject.someFunction() uses myObject for this, while myDerivedObject.someFunction() uses myDerivedObject for this.

If this had been captured during the definition of someFunction, we would have obtained 0 in both lines, but this would also have made prototypes much less useful.

Yoric
  • 3,348
  • 3
  • 19
  • 26
1

You could use thisArg of Array#filter with the set and the prototype of has as callback.

This pattern does not require a binding of an instance of Set to the prototype, because

If a thisArg parameter is provided to filter, it will be used as the callback's this value. Otherwise, the value undefined will be used as its this value. The this value ultimately observable by callback is determined according to the usual rules for determining the this seen by a function.

const
    myset = new Set(["key1", "key2", "key3", "keepMe"]),
    mykeys = ["ignoreMe", "keepMe", "ignoreMeToo", "key2"],
    filtered = mykeys.filter(Set.prototype.has, myset);

console.log(filtered);
Nina Scholz
  • 376,160
  • 25
  • 347
  • 392
  • thanks for reminding me about existence of `thisArg`. I'll update my question as this is not what I'm looking for :) – Sid Vishnoi May 26 '18 at 15:40