Replace element at specific position in an array without mutating it
Asked Answered
H

9

61

How can the following operation be done without mutating the array:

let array = ['item1'];
console.log(array); // ['item1']
array[2] = 'item2'; // array is mutated
console.log(array); // ['item1', undefined, 'item2']

In the above code, array variable is mutated. How can I perform the same operation without mutating the array?

Housley answered 27/6, 2016 at 18:23 Comment(1)
If you want something fast https://mcmap.net/q/321616/-replace-element-at-specific-position-in-an-array-without-mutating-it ,;)Touraine
N
117

You can use Object.assign(target, source1, source2, /* …, */ sourceN):

Object.assign([], array, {2: newItem});
Nucleolus answered 27/6, 2016 at 18:55 Comment(8)
@DanPrince Not sure about how these sharing structures are implemented. But slice copies the full array so that case is also O(n).Nucleolus
Any documentation on Object.assign?Sidman
That said, copying an array with slice might be faster because length is copied first, so preallocation is possible. Object.assign(array.slice(), {2: newItem}); is another possibility.Nucleolus
@AlexanderDixon See Object.assign in MDN and the specNucleolus
Not sure if this is a one-off fix in a program the original poster is writing or the end result will be a application accessed across multiple devices and browsers but... there is no support for internet explorer or android. You may want to add a disclaimer in your answer and reference a polyfill perhaps.Sidman
@cemper93 It can work, you just need to use this specific syntax: Object.assign([], array, { [i]: newItem })Risky
This will replace the item if it contains a previously existing item in the array. Object.assign([], [1,2,3,4], {2: 5}); ===> [1, 2, 5, 4]Fulllength
Not sure but this might be very bad for engines performance optimization when assigning to indexes that are >= than arrays length?Chaddie
T
19

The fast way

function replaceAt(array, index, value) {
  const ret = array.slice(0);
  ret[index] = value;
  return ret;
}

See the JSPerf (thanks to @Bless)

Related posts:

Touraine answered 22/12, 2017 at 15:26 Comment(1)
Yes. +1. Here is a perf comparison - jsperf.com/…Housley
G
14

You can simply set up a new array as such:

const newItemArray = array.slice();

And then set value for the index which you wish to have a value for.

newItemArray[position] = newItem

and return that. The values under the indexes in-between will have undefined.

Or the obviously alternative would be:

Object.assign([], array, {<position_here>: newItem});
Gompers answered 27/6, 2016 at 18:32 Comment(0)
B
12

Here is how I'd like to do it:

function update(array, newItem, atIndex) {
    return array.map((item, index) => index === atIndex ? newItem : item);
}

Generally, Array-spread operation produces few temporary arrays for you, but map doesn't, so it can be faster. You can also look at this discussion as a reference

Bogart answered 29/6, 2016 at 8:8 Comment(1)
Nice but the redux discussion is about removing an element, I think the fastest way to replace an element is https://mcmap.net/q/321616/-replace-element-at-specific-position-in-an-array-without-mutating-it ;)Touraine
S
7

Well, technically this wouldn't be replacing as there isn't an item at the index you're changing.

Look at how it's handled in Clojure—a language that's built around canonical implementations for immutable data structures.

(assoc [1] 2 3)
;; IndexOutOfBoundsException

Not only does it fail, but it crashes too. These data structures are designed to be as robust as possible and when you come up against these kinds of errors, it's generally not because you've discovered an edge case, but more likely that you're using the wrong data structure.

If you are ending up with sparse arrays, then consider modelling them with objects or maps instead.

let items = { 0: 1 };
{ ...items, 2: 3 };
// => { 0: 1, 2: 3 }

let items = new Map([ [0, 1] ]);
items(2, 3);
// => Map {0 => 1, 2 => 3}

However, Map is a fundamentally mutable data structure, so you'd need to swap this out for an immutable variant with a library like Immutable.js or Mori.

let items = Immutable.Map([ [0, 2] ]);
items.set(2, 3);
// => Immutable.Map {0 => 1, 2 => 3}

let items = mori.hashMap();
mori.assoc(items, 2, 3);
// => mori.hashMap {0 => 1, 2 => 3}

Of course, there might be a perfectly good reason for wanting to use JavaScript's arrays, so here's a solution for good measure.

function set(arr, index, val) {
  if(index < arr.length) {
    return [
      ...arr.slice(0, position),
      val,
      ...arr.slice(position + 1)
    ];
  } else {
    return [
      ...arr,
      ...Array(index - arr.length),
      val
    ];
  }
}
Stroke answered 27/6, 2016 at 18:48 Comment(0)
F
7

Another way could be to use spread operator with slice as

let newVal = 33, position = 3;
let arr = [1,2,3,4,5];
let newArr = [...arr.slice(0,position - 1), newVal, ...arr.slice(position)];
console.log(newArr); //logs [1, 2, 33, 4, 5]
console.log(arr); //logs [1, 2, 3, 4, 5]
Frenzy answered 27/4, 2019 at 13:52 Comment(1)
Note that position in this snippet is not zero based. If you have a zero based index, it is [...arr.slice(0, index), newVal, ...arr.slice(index + 1)]Cyler
H
3

There's a new tc39 proposal, which adds a with method to Array that returns a copy of the array and doesn't modify the original:

Array.prototype.with(index, value) -> Array

Example from the proposal:

const correctionNeeded = [1, 1, 3];
correctionNeeded.with(1, 2); // => [1, 2, 3]
correctionNeeded; // => [1, 1, 3]

(note that a RangeError will be thrown if the first argument to with is outside the bounds of the array, so the specific example in the question will not work)

As it's currently in stage 3, it will likely be implemented in browser engines soon, but in the meantime a polyfill is available here or in core-js.

Horary answered 27/4, 2022 at 11:12 Comment(0)
W
2

var list1 = ['a','b','c'];
var list2 = list1.slice();
list2.splice(2, 0, "beta", "gamma");
console.log(list1);
console.log(list2);

Is this what you want?

Wainwright answered 27/6, 2016 at 18:36 Comment(0)
B
2

How about this:

const newArray = [...array]; // make a copy of the original array
newArray[2] = 'item2'; // mutate the copy

I find the intent a bit more clear than this one-liner:

const newArray = Object.assign([...array], {2: 'item2'});
Burbage answered 21/2, 2021 at 9:14 Comment(1)
I love the latter form, it really reads well!Paugh

© 2022 - 2024 — McMap. All rights reserved.