0

Consider the following data:

const filters = ["c", "1"];

const data = [
  {
    name: 'a',
    values: ["a", "b", "c"],
  },
  {
    name: "b",
    values: ["a", "z", "1"],
  },
  {
    name: "c",
    values: ["z", "y", "x"],
  }
];

In order to get all the objects in the data array where the values contain one of the filters, the following can be used:

data.filter(entry => entry.values.some(v => filters.includes(v)));

According to the rules of javascript I'm aware of, you can forego the anonymous/lambda function in the .some() call, in the following way:

data.filter(entry => entry.values.some(filters.includes));

However, this doesn't work, as we get the error TypeError: Cannot convert undefined or null to object. However, doing the following makes it work also, but is redundant and I don't understand why it's necessary:

const inc = (v) => filters.includes(v);
data.filter(entry => entry.values.some(inc));

Is this a bug in the V8 engine (I've tried this in a chrome console and in nodejs) or is it a technical limitation I'm not aware of? If so, can someone explain it to me?

  • 3
    The function includes lost the array reference (this) when you can pass it directly as the handler of the function some. – Ele May 23 '20 at 19:58
  • 1
    You can pass this directly `filters.includes.bind(filters)` to keep the `includes` method bound to the appropriate object. And, there's a syntax shortcut under development for this in a future version of Javascript which is think uses `::`. – jfriend00 May 23 '20 at 20:10
  • 1
    It's basically similar to doing `data.filter(entry => entry.values.some(Array.prototype.includes))`. [JavaScript function binding (this keyword) is lost after assignment](https://stackoverflow.com/questions/2701987). Even if you pass `filters` as `thisArg` it won't work because `includes` has a second parameter: [Why does Array.prototype.includes.bind behave unexpectedly?](https://stackoverflow.com/questions/57271679) – adiga May 23 '20 at 20:11
  • It occurs to me that things would be more efficient if you used a `Set` instead of an `Array` for `values`. Unless you need the ability to adjust the order or contain duplicates, the `Set` would give you the same container aspect, but give you the more efficient `.has()` operation. – jfriend00 May 23 '20 at 21:03
  • @jfriend00 True, I could use Set, but the question wasn't about the best structure to use, but rather why this particular way of writing the code was failing. And I've been more than answered on that! :D – Évelyne Lachance May 23 '20 at 21:21
  • Yes, well I don't just answer the literal questions asked about. I look at the overall problem and attempt to make suggestions for better ways to solve it than even what exactly you asked about. Lots of questions here are questions about the wrong solution in the first place. If your arrays are small, the `Set` won't make much difference, but if they are larger, it could make a huge difference and `.has()` is really what you're trying to do with `.includes()` anyway. – jfriend00 May 23 '20 at 21:23
  • StackOverflow doesn't tell me (or let me see) who it is that "closed" this question as duplicate 2 years and 8 months *after* it has been asked, answered, an answer was accepted, etc. But... seriously? REALLY? Why are people even looking at 3 year old questions in the first place? They're solved. Go do something more productive! – Évelyne Lachance Jan 30 '23 at 12:57
  • @ÉvelyneLachance The age of the question and the presence of answers is completely irrelevant when it comes to duplicate closure. This isn’t a forum where “topics” are “solved” and then are forever dead. _“Go do something more productive”_ — When searching for programming issues on Google, it’s a huge waste of time to dig through the same, possibly irrelevant, Q&As all the time. Closing them as duplicate helps link questions that are the same together, so that search engines can provide more useful results. How is this not productive? – Sebastian Simon Jan 30 '23 at 15:44

4 Answers4

1

The function includes lost the array reference (this) when you passed it directly as the handler of the function some.

The following code snippet implements a function fn with two functions associated to its prototype.

The function handler it's like the function Array.includes, in this case I'm using it just to print info of the enclosing scope.

window.name = "MyWindow";
const filters = ["c", "1"];
const data = [{    name: 'a',    values: ["a", "b", "c"],  },  {    name: "b",    values: ["a", "z", "1"],  },  {    name: "c",    values: ["z", "y", "x"],  }];

function fn() {
  this.name = "myFn";
}

fn.prototype.includes = function(e) {
  return filters.includes(e);
}

fn.prototype.handler = function(e) {
  console.log(this.name); // <- Here the name will be "MyWindow" because the function lost the
                          //    enclosing scope, and now will use the enclosing scope where 
                          //    the function was calling from.
  return this.includes(e);
}

let myFn = new fn();

console.log(data.filter(entry => entry.values.some(myFn.handler/*Will use the window as enclosing scope.*/)));

Now, just for illustrating, look at the following code snippet, and see how we bind the same object and everything works fine.

This is just to illustrate, don't do this

window.name = "MyWindow";
const filters = ["c", "1"];
const data = [{    name: 'a',    values: ["a", "b", "c"],  },  {    name: "b",    values: ["a", "z", "1"],  },  {    name: "c",    values: ["z", "y", "x"],  }];

function fn() {
  this.name = "myFn";
}

fn.prototype.includes = function(e) {
  return filters.includes(e);
}

fn.prototype.handler = function(e) {
  console.log(this.name); // <- Here the name will be "myFn".
  return this.includes(e);
}

let myFn = new fn();

console.log(data.filter(entry => entry.values.some(myFn.handler.bind(myFn))));
Ele
  • 33,468
  • 7
  • 37
  • 75
1

Just like mentioned in the comments, the Array.includes method lost the array reference so you'd have to do something like the following:

const filters = [...]
// ...
const includes = filters.includes.bind(filters)

const result = data.filter(entry => entry.values.some(includes));

However, this won't work properly because Array.includes method's signature takes valueToFind and an optional fromIndex as arguments:

arr.includes(valueToFind[, fromIndex])

And the callback for Array.some will pass element, index, and array as arguments. The fromIndex argument will be the index of the current element and it will result in an unexpected behavior.

arr.some(callback(element[, index[, array]])[, thisArg])

const filters = ["c", "1"];

const data = [
  {
    name: "a",
    values: ["a", "b", "c"]
  },
  {
    name: "b",
    values: ["a", "z", "1"]
  },
  {
    name: "c",
    values: ["z", "y", "x"]
  }
];

const includes = filters.includes.bind(filters);
const result = data.filter(entry => entry.values.some(includes));

console.log(result)

So unfortunately, you will have to be more explicit and only pass the relevant arguments to the Array.includes method:

const filters = ["c", "1"];

const data = [
  {
    name: "a",
    values: ["a", "b", "c"]
  },
  {
    name: "b",
    values: ["a", "z", "1"]
  },
  {
    name: "c",
    values: ["z", "y", "x"]
  }
];

const includes = filters.includes.bind(filters);
const result = data.filter(entry => entry.values.some(v => includes(v)));

console.log(result)
goto
  • 4,336
  • 15
  • 20
1

There are 2 problems here:

1) Lost this context: You call includes as someArray.includes i.e. it needs someArray as this context. With the loss of this, includes is going to throw an error just like you happen to observe.

To get around this, you could do:

const includesWithThis = [].includes.bind(filters);

2) some passes down more than one parameter e.g. item, index, array, etc.

Incidentally, includes accepts 2 parameters i.e. item and fromIndex

So you're essentially calling filters.includes(value, index) where index will be 0 the first time and will increment with every iteration done by some method, which will cause your fromIndex to be broken - you always want it to be 0.


So this is going to make your code fail anyway (even with the workaround for the 1st problem). To illustrate try doing:

filters[0] = "a"; // Something that's available after `fromIndex`
const includesWithThis = [].includes.bind(filters);
data.filter(entry => entry.values.some(includesWithThis));

Doing above fix is not recommended at all. You're better off doing the original solution only i.e.:

data.filter(entry => entry.values.some(v => filters.includes(v)));
chiragrtr
  • 902
  • 4
  • 6
0

No. You can not use Array#includes, like

data.filter(entry => entry.values.some(Array.prototype.includes, filters));
//                                     borrowed function
//                                                               thisArg

because of the second parameter of includes, which is udes to seach with a fromIndex:

The position in this array at which to begin searching for valueToFind.

The first character to be searched is found at fromIndex for positive values of fromIndex, or at arr.length + fromIndex for negative values of fromIndex (using the absolute value of fromIndex as the number of characters from the end of the array at which to start the search).

Defaults to 0.

Finally you need to use the value directly for checking.

const
    filters = ["c", "1"];
    data = [{ name: 'a', values: ["a", "b", "c"] }, { name: "b", values: ["a", "z", "1"] }, { name: "c", values: ["z", "y", "x"] }];

console.log(data.filter(entry => entry.values.some(v => filters.includes(v))));
Community
  • 1
  • 1
Nina Scholz
  • 376,160
  • 25
  • 347
  • 392