12

I want to make an array based on two arrays - "ideaList" and "endorsements" declared globally. As ideaList and endorsements are used in other parts of the program I need them to be immutable, and I thought that .map and .filter would keep this immutability.

function prepareIdeaArray(){
var preFilteredIdeas=ideaList
        .filter(hasIdeaPassedControl)
        .map(obj => {obj.count = endorsements
                                 .filter(x=>x.ideaNumber===obj.ideaNumber)
                                 .reduce((sum, x)=>sum+x.count,0); 
                     obj.like = endorsements
                                 .filter(x=>x.ideaNumber===obj.ideaNumber && x.who===activeUser)
                                 .reduce((sum, x)=>sum+x.count,0)===0?false:true
                     obj.position = generatePosition(obj.status)
                     obj.description = obj.description.replace(/\n/g, '<br>')  
                     return obj;});
preFilteredIdeas.sort(compareOn.bind(null,'count',false)).sort(compareOn.bind(null,'position',true))
return preFilteredIdeas;
}

However, when I console.log ideaList after this function has been executed, I remark that objects of the array all have the "count", "like", "position" properties with values, which proves that the array has been mutated.

I tried by using .map only, but same result.

Would you know how I could prevent ideaList to get mutated? Also I would like to avoid to use const, as I declare ideaList globally first, and then assign to it some data in another function.

Charlie
  • 22,886
  • 11
  • 59
  • 90
Pierre
  • 219
  • 1
  • 4
  • 11
  • 1
    Make copies: `let clone = [...ideaList]` BTW `.map()` doesn't mutate it returns a copy and the original array is untouched. – zer00ne Mar 25 '19 at 08:08
  • You have at least 5 or more undefined variables are they all global? – zer00ne Mar 25 '19 at 08:14
  • ideaList, endorsements are global arrays, activeUser is a global string. hasIdeaPassedControl and compareOn are functions. – Pierre Mar 25 '19 at 08:21

8 Answers8

11

You're not mutating the array itself but rather the objects that the array contains references to. .map() creates a copy of the array but the references contained in it points to the exact same objects as the original, which you've mutated by adding properties directly to them.

You need to make copies of these objects too and add the properties to these copies. A neat way to do this is to use object spread in .map() callback:

    .map(({ ...obj }) => {
      obj.count = endorsements
                             .filter(x=>x.ideaNumber===obj.ideaNumber)
      ...

If your environment doesn't support object spread syntax, clone the object with Object.assign():

    .map(originalObj => {
      const obj = Object.assign({}, originalObj);
      obj.count = endorsements
                             .filter(x=>x.ideaNumber===obj.ideaNumber)
      ...
Lennholm
  • 7,205
  • 1
  • 21
  • 30
3

To help having immutability in mind you could think of your values as primitives.

1 === 2 // false
'hello' === 'world' // false

you could extend this way of thinking to non-primitives as well

[1, 2, 3] === [1, 2, 3] // false
{ username: 'hitmands' } === { username: 'hitmands' } // false

to better understand it, please have a look at MDN - Equality Comparisons and Sameness


how to force immutability?

By always returning a new instance of the given object!

Let's say we have to set the property status of a todo. In the old way we would just do:

todo.status = 'new status';

but, we could force immutability by simply copying the given object and returning a new one.

const todo = { id: 'foo', status: 'pending' };

const newTodo = Object.assign({}, todo, { status: 'completed' });

todo === newTodo // false;

todo.status // 'pending'
newTodo.status // 'completed'

coming back to your example, instead of doing obj.count = ..., we would just do:

Object.assign({}, obj, { count: ... }) 
// or
({ ...obj, count: /* something */ })

there are libraries that help you with the immutable pattern:

  1. Immer
  2. ImmutableJS
Hitmands
  • 13,491
  • 4
  • 34
  • 69
3

In JS, the objects are referenced. When created, in other words, you get the object variable to point to a memory location which intends to be holding a meaningful value.

var o = {foo: 'bar'}

The variable o is now point to a memory which has {foo: bar}.

var p = o;

Now the variable p too is pointing to the same memory location. So, if you change o, it will change p too.

This is what happens inside your function. Even though you use Array methods which wouldn't mutate it's values, the array elements themselves are objects which are being modified inside the functions. It creates a new array - but the elements are pointing to the same old memory locations of the objects.

var a = [{foo: 1}];     //Let's create an array

//Now create another array out of it
var b = a.map(o => {
   o.foo = 2;
   return o;
})

console.log(a);   //{foo: 2}

One way out is to create a new object for your new array during the operation. This can be done with Object.assign or latest spread operator.

a = [{foo: 1}];

b = a.map(o => {

    var p = {...o};    //Create a new object
    p.foo = 2;
    return p;

})

console.log(a);  // {foo:1}
Charlie
  • 22,886
  • 11
  • 59
  • 90
1

You use the freeze method supplying the object you want to make immutable.

const person = { name: "Bob", age: 26 }
Object.freeze(person)
The epic face 007
  • 552
  • 1
  • 8
  • 22
0

You could use the new ES6 built-in immutability mechanisms, or you could just wrap a nice getter around your objects similar to this

var myProvider = {}
function (context)
{
    function initializeMyObject()
    {
        return 5;
    }

    var myImmutableObject = initializeMyObject();

    context.getImmutableObject = function()
    {
        // make an in-depth copy of your object.
        var x = myImmutableObject

        return x;
    }


}(myProvider);

var x = myProvider.getImmutableObject();

This will keep your object enclosed outside of global scope, but the getter will be accessible in your global scope.

You can read more on this coding pattern here

0

One easy way to make "copies" of mutable objects is to stringify them into another object and then parse them back into a new array. This works for me.

function returnCopy(arrayThing) {
    let str = JSON.stringify(arrayThing);
    let arr = JSON.parse(str);
  return arr;
}
Cesarp
  • 1
  • This is less performant than just doing `[...original]` or `original.slice()`, but also, it won't work if there are things in the array that are functions, symbols, or bigints. Bigints will throw an error, and symbols and functions will just be ignored. – jimmyfever Oct 26 '21 at 18:05
0

Actually, you can use spread opreator to make the original array stay unchanged, below y is the immutable array example:

const y = [1,2,3,4,5];
function arrayRotation(arr, r, v) {
  for(let i = 0; i < r; i++) {
    arr = [...y]; // make array y as immutable array [1,2,3,4,5]
    arr.splice(i, 0, v);
    arr.pop();
    console.log(`Rotation ${i+1}`, arr);
  }
}
arrayRotation(y, 3, 5)

If you don't use the spread operator, the y array will get changed when loop is running time by time.

Here is the mutable array result:

const y = [1,2,3,4,5];
function arrayRotation(arr, r, v) {
  for(let i = 0; i < r; i++) {
    arr = y; // this is mutable, because arr and y has same memory address
    arr.splice(i, 0, v);
    arr.pop();
    console.log(`Rotation ${i+1}`, arr);
  }
}
arrayRotation(y, 3, 5)
DamonWu
  • 108
  • 3
-1

You assign these properties in your map function, you need to change this. (Just declare an empty object instead of using your current obj)