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?

8
  • 3
    The function includes lost the array reference (this) when you can pass it directly as the handler of the function some. Commented May 23, 2020 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 ::. Commented May 23, 2020 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. 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? Commented May 23, 2020 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. Commented May 23, 2020 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 Commented May 23, 2020 at 21:21

4 Answers 4

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)));
Sign up to request clarification or add additional context in comments.

2 Comments

Thank you, that was the thorough and concise answer I needed to see. Great explanation of both reasons!
Glad to help :)
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))));

Comments

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)

Comments

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

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.