Nedas Nedas - 3 months ago 9
Javascript Question

Array filter changes the main array

I've noticed some strange behavior in the array filters on node.js.
There is a simple array and a loop:

var array = [
{
name:"bob",
planet:"earth"
},
{
name:"mike",
planet:"mars"
},
{
name:"vlad",
planet:"jupiter"
}];

var filtered = array.filter(function(x){
return x.name !== "mike";
});

console.log(array); //lets print how normal array looks like
console.log("---");
console.log(filtered); //lets print how filtered one looks like

for(var i = 0; i < filtered.length; i++)
{
delete filtered[i].planet; //remove planet
filtered[i].name = filtered[i].name + "[NEW]"; //add NEW to the name
}

console.log("After replacement:");
console.log(array);//lets print how normal array looks like now
console.log("-----------");
console.log(filtered);//lets print how filtered array looks like now


In theory,
array
array should not be changed, since I did not manipulate it in any way. Hovewer, this is what I get in console:

[ { name: 'bob', planet: 'earth' },
{ name: 'mike', planet: 'mars' },
{ name: 'vlad', planet: 'jupiter' } ] //this array is normal
---
[ { name: 'bob', planet: 'earth' },
{ name: 'vlad', planet: 'jupiter' } ] //this is good behavior, since I don't need "mike"

After replacement:

[ { name: 'bob[NEW]' },
{ name: 'mike', planet: 'mars' },
{ name: 'vlad[NEW]' } ] //this should not be changed in any way
-----------
[ { name: 'bob[NEW]' }, { name: 'vlad[NEW]' } ] //this is correct


Why does this happen? I need
array
to stay the same as in the beginning.

Thanks.

Answer

Your code here:

for(var i = 0; i < filtered.length; i++)
{
    delete filtered[i].planet; //remove planet
    filtered[i].name = filtered[i].name + "[NEW]"; //add NEW to the name
}

...isn't changing either array. It's changing the state of the objects that both arrays refer to.

Simpler example:

var a = [{answer:null}];
var b = a.filter(function() { return true; }); // Just a copy, but using `filter` for emphasis
a[0].answer = 42;
console.log(b[0].answer); // 42

This line:

a[0].answer = 42;

doesn't change a or b, it changes the state of what a[0] refers to, which b[0] also refers to.

Let's throw some ASCII-art Unicode-art at it:

After this line:

var a = [{answer:null}];

this is what we have in memory (ignoring some irrelevant details);

+−−−−−−−−−−−−−−+
| variable "a" |
+−−−−−−−−−−−−−−+     +−−−−−−−−−−−−−−+
| Ref11542     |−−−−>|    array     |
+−−−−−−−−−−−−−−+     +−−−−−−−−−−−−−−+     +−−−−−−−−−−−−−−+
                     | 0: Ref88464  |−−−−>|    object    |
                     +−−−−−−−−−−−−−−+     +−−−−−−−−−−−−−−+
                                          | answer: null |
                                          +−−−−−−−−−−−−−−+

a refers to an array object, which has a 0 property, which refers to the object with the answer property. I'm using "Ref11542" and "Ref88494" to represent the object references that a and a[0] contain, but of course we never actually see the value of those references; they're private to the JavaScript engine.

Then we do this:

var b = a.filter(function() { return true; }); // Just a copy, but using `filter` for emphasis

Now we have:

+−−−−−−−−−−−−−−+
| variable "a" |
+−−−−−−−−−−−−−−+     +−−−−−−−−−−−−−−+
| Ref11542     |−−−−>|    array     |
+−−−−−−−−−−−−−−+     +−−−−−−−−−−−−−−+   
                     | 0: Ref88464  |−−+
                     +−−−−−−−−−−−−−−+  |
                                       |
                                       |  +−−−−−−−−−−−−−−+
+−−−−−−−−−−−−−−+                       +−>|    object    |
| variable "b" |                       |  +−−−−−−−−−−−−−−+
+−−−−−−−−−−−−−−+     +−−−−−−−−−−−−−−+  |  | answer: null |
| Ref66854     |−−−−>|    array     |  |  +−−−−−−−−−−−−−−+
+−−−−−−−−−−−−−−+     +−−−−−−−−−−−−−−+  |
                     | 0: Ref88464  |−−+
                     +−−−−−−−−−−−−−−+

Note that both arrays contain the same object reference (shown here as "Ref88464"); they point to the same object.

Now we do this:

a[0].answer = 42;

All that does is change the state of the object; it has no effect on a or b or the arrays they refer to:

+−−−−−−−−−−−−−−+
| variable "a" |
+−−−−−−−−−−−−−−+     +−−−−−−−−−−−−−−+
| Ref11542     |−−−−>|    array     |
+−−−−−−−−−−−−−−+     +−−−−−−−−−−−−−−+   
                     | 0: Ref88464  |−−+
                     +−−−−−−−−−−−−−−+  |
                                       |
                                       |  +−−−−−−−−−−−−−−+
+−−−−−−−−−−−−−−+                       +−>|    object    |
| variable "b" |                       |  +−−−−−−−−−−−−−−+
+−−−−−−−−−−−−−−+     +−−−−−−−−−−−−−−+  |  | answer: 42   |
| Ref66854     |−−−−>|    array     |  |  +−−−−−−−−−−−−−−+
+−−−−−−−−−−−−−−+     +−−−−−−−−−−−−−−+  |             ^
                     | 0: Ref88464  |−−+             +−−−−−−− only change is here
                     +−−−−−−−−−−−−−−+

So naturally

console.log(b[0].answer);

...outputs 42.


In a comment you've asked:

But then how do I set the state of filtered object and not the main one?

Remember, the objects are the same in both arrays. You haven't copied the objects, you've just created a new array that only has some of them in it.

If you want to be able to change those objects' properties without affecting the objects in the first array, you need to make copies of the objects.

If the objects contain only simple primitive values, that's easy; the general case is quite hard. :-)

In your case, though, since your objets just have name and planet properties, and the very next thing you do is remove the planet property and change the name, we can easily create objects as we go; see comments:

var array = [
{
    name:"bob",
    planet:"earth"
},
{
    name:"mike",
    planet:"mars"
},
{
    name:"vlad",
    planet:"jupiter"
}];

var filtered = array.filter(function(x){
    return x.name !== "mike";
}).map(function(x) {
    // Create a *new* object, setting its `name` to the `name`
    // of the original object plus [NEW], and ignoring its
    // `planet` property entirely
    return {name: x.name + "[NEW]"};
});

console.log(array);
console.log("---");
console.log(filtered);

Alternately, you might want to make just one pass through the array:

var filtered = [];
array.forEach(function(x){
    if (x.name !== "mike") {
        filtered.push({name: x.name + "[NEW]"});
    }
});