wyc
wyc

Reputation: 55293

How does map assign the new value in the following case?

I have an array called this.list. I wanted to map through it's items and give a new value to those items:

this.list = this.list.map(item => {
  if (item.id === target.id) {
    item.dataX = parseFloat(target.getAttribute('data-x'))
    item.dataY = parseFloat(target.getAttribute('data-y'))
  }
  return item
})

But to my surprise, this also worked:

this.list.map(item => {
  if (item.id === target.id) {
    item.dataX = parseFloat(target.getAttribute('data-x'))
    item.dataY = parseFloat(target.getAttribute('data-y'))
  }
  return item
}) 

How come map is assigning a new value to this.json if I'm not using any assignment at all?

Upvotes: 1

Views: 57

Answers (3)

Stuart Wakefield
Stuart Wakefield

Reputation: 6414

Based upon your example, comments inline:

// The function map returns a _new_ array
// containing all of the existing elements.
// Then is assigned to the existing variable
// overwriting the original array.
this.list = this.list.map(item => {

  // Here "item" refers to the actual object element in the
  // existing list any assignments to this object will affect
  // the existing item in the list.
  if (item.id === target.id) {

    // Here are your assigments.
    item.dataX = parseFloat(target.getAttribute('data-x'));
    item.dataY = parseFloat(target.getAttribute('data-y'));
  }

  // Although the item is returned and would replace the
  // existing element in the new resulting list, the item
  // returned is in fact the actual element that is _already_
  // currently in the list.
  return item;
});

Therefore in this case the following is equivalent in terms of result based upon the fact you are assigning to the original variable and updating in place the original values:

this.list.forEach(item => {
  if (item.id === target.id) {
    item.dataX = parseFloat(target.getAttribute('data-x'));
    item.dataY = parseFloat(target.getAttribute('data-y'));
  }
});

To perform this in a way that doesn't mutate the original objects, if that is what you desire:

var result = this.list.map(item => {
  return target.id === item.id ? {
    id: item.id,
    dataX: parseFloat(target.getAttribute('data-x')),
    dataY: parseFloat(target.getAttribute('data-y'))
  } : item;
});

Or if item has additional data that you need to carry across you may need to somehow copy the object: How do I correctly clone a JavaScript object?. One alternative is to model the types around this, for example:

class Item {
  constructor(id, x, y) {
    this.id = id;
    this.dataX = dataX;
    this.dataY = dataY;
  }

  // Immutability style setter, returns the result
  // as a new instance.
  withData(dataX, dataY) {
    return new Item(this.id, dataX, dataY);
  }
}

var result = this.list.map(item => {
  return item.id === target.id ? item.withData(
    parseFloat(target.getAttribute('data-x')),
    parseFloat(target.getAttribute('data-y'))
  ) : item;
});

In the above example this.list is the untouched original array containing all of the elements as they were prior to the map operation.

result contains a mix of "updated" elements (new instances of the elements that that matched target.id and original items that didn't.

Follow the instances...

If we were to number all of the instances, before the map operation in the first example using map and assigning back to this.list we might have:

this.list
  Array 1
  - Item 1
  - Item 2
  - Item 3
  - Item 4

After the map operation, the items are the same instances, and have been updated, but the array is a different instance:

this.list -> map -> this.list
  Array 1             Array 2 (NEW)
  - Item 1       ->   - Item 1
  - Item 2       ->   - Item 2
  - Item 3       ->   - Item 3
  - Item 4       ->   - Item 4

In the forEach example, both before the forEach operation and after the result is the same, the instances have been updated in place:

this.list (forEach) this.list (No change)
  Array 1             Array 1
  - Item 1            - Item 1
  - Item 2            - Item 2
  - Item 3            - Item 3
  - Item 4            - Item 4

In each of the immutable examples this.list is the same as before but result will be a different array instance and the matched items will be different instances, in the following example Item 1 was matched and updated:

this.list -> map -> result             this.list (Untouched)
  Array 1             Array 2 (NEW)      Array 1
  - Item 1            - Item 5 (NEW)     - Item 1
  - Item 2       ->   - Item 2           - Item 2
  - Item 3       ->   - Item 3           - Item 3
  - Item 4       ->   - Item 4           - Item 4

Upvotes: 1

Jan Tojnar
Jan Tojnar

Reputation: 5524

JavaScript stores objects as references so when you map over an array, you are changing the original object. You can see the same behaviour in the code bellow:

let original = [{x: 5}];
let copied = original;
copied[0].x = 6;
console.log(original[0].x); // 6

You would have to re-create the object or clone it somehow.

  this.list = this.list.map(item => {
    if (item.id === target.id) {
      item = {
        id: target.id,
        dataX: parseFloat(target.getAttribute('data-x')),
        dataY: parseFloat(target.getAttribute('data-y')),
        ...
      };
    }
    return item
  }) 

Upvotes: 1

Nhor
Nhor

Reputation: 3940

Refering to the map function docs:

map does not mutate the array on which it is called (although callback, if invoked, may do so).

Note that the item object you're using in the callback is a reference to the actual this.list element and in your case you're mutating the item object.

Upvotes: 1

Related Questions