loading...

Pure Array Modifications: Plain JavaScript vs. Modern JavaScript

gsto profile image Glenn Stovall Originally published at peanutbutterjavascript.com on ・3 min read

The post Pure Array Modifications: Plain JavaScript vs. Modern JavaScript appeared first on Glenn Stovall - Growth Engineer.

When working with Redux or other state managers, you want to keep your code pure. That means no side effects. Instead of updating an array instead you want to return a new copy of the array with the changes applied. This is tricky with JavaScript arrays since it isn’t clear which native functions change the array, and which ones return a new copy.

Pop Quiz: array.slice() and array.splice(). How many of them affect the original array?

a) Slice is pure, splice is impure.

b) Slice is impure, splice is pure.

c) Both are pure.

d) Neither are pure.

Scroll to see the result:

...

...

...

a) Slice is a pure function, but splice changes the array.

Confusing, right?

To keep your array edits safe, let’s look at some functions that perform pure operations on arrays. Examples are included in both ES2018 and older versions of JavaScript. These examples make heavy use of the spread operator, so if you aren’t familiar with it this may also help you get a stronger understanding of how it works.

1. How to add an element to the end of an array without modifying the original:

//ES 2018
const purePush = (arr, el) => [...arr,el]

//ES5
function purePushClassic(arr, el) {
  return arr.concat(el)
}

2. How to add an element to the beginning of an array without modifying the original:

//ES 2018
const pureUnshift = (arr, el) => [el, ...arr]

//ES5
function pureUnshiftClassic(arr, el) {
  return [el].concat(arr)
}

3. How to Combine two Arrays into a Third

//ES2018
const pureCombine = (arr1, arr2) => [...arr1, ...arr2]

//ES5
function pureCombineClassic(arr1, arr2) {
  return arr1.concat(arr2)
}

4. Remove an element from the end of the array, Without modifying the original:

//ES 2018
const pureShift = (arr) => arr.slice(1)

//ES5
function pureShiftClassic(arr){ return arr.slice(1) }

5. Remove an element from the beginning of the array, Without modifying the original:

//ES2018
const purePop = (arr) => arr.slice(0,-1)

//ES5
function purePopClassic(arr) { return arr.slice(0,-1) }

6. Insert an element at a specific index without mutating the array:

//ES2018
const pureInsert = (arr, el, i) => [...arr.slice(0,i), el, ...arr.slice(i)]

//ES5
function pureInsertClassic(arr, el, i) {
return arr.slice(0,i)
  .concat(el)
  .concat(arr.slice(i++))
}

7. Replace an element at a specific index without mutating the array:

//ES2018
const pureReplace = (arr, el, i) => [...arr.slice(0, i), el, ...arr.slice(++i)]

//ES5
function pureReplaceClassic(arr, el, i) {
  var copy = arr.slice()
  copy[i] = el
  return copy
}

8. Delete an element at a specific index without mutating the array:

//ES2018
const pureDelete = (arr, i) => [...arr.slice(0,i),...arr.slice(i+1)]

//ES5
function pureDeleteClassic(arr, i) {
  return arr.slice(0,i).concat(arr.slice(i+1))
}

Bonus tip: turn any impure array method into a pure one

All you have to is make a copy of the array first. You’ve seen it in the above examples. In ES2018, you can use the spread operator. In old JavaScript, you can use splice. For example, here’s a way to convert the reverse method into a pure operation:

Want to know more? Here’s some further reading

Discussion

pic
Editor guide
Collapse
sloths_r_cool profile image
Robin Kim

Nice article! Just have a few comments:

The examples for #4 and #5 look like they're switched.

For #6, the ++ in arr.slice(i++) doesn't actually do anything. The original value of i will be returned to slice prior to incrementing, and even if it did, the resulting array would be inaccurate. I would just stick with:

const pureInsert = (arr, el, i) => [...arr.slice(0, i), el, ...arr.slice(i)]

For #7, your pureReplace method works great but can be cleaned up to look more consistent with #6:

const pureReplace = (arr, el, i) => [...arr.slice(0, i), el, ...arr.slice(++i)]

pureReplace([1,2,3,4,5], 'foo', 2)   // [1,2,'foo',4,5]

Consistency aside, the outright return will also avoid opening up an unnecessary closure into your function. Defining an array via let copy and handing it off to another scope will delay garbage collection of your functional context, as any mutations to the array will need to reference the original copy variable. (I wouldn't be making such a fuss if you just returned a primitive, pass-by-value type!)

Collapse
gsto profile image
Glenn Stovall Author

Thanks for the great feedback! I've made the suggested changes to the article

Collapse
renatomachado profile image
Renato Machado

Isn't the ++ operator changing the parameter value?

Collapse
sloths_r_cool profile image
Robin Kim

It will only change the parameter after the line. For example:

let num = 5
console.log(num++)  // still logs 5!
console.log(num)    // logs 6

Notice how the number stays at 5 on the second line, and the ++ doesn't take into effect until the next line. To increment in place, you'd have to move the ++ to the left of the operand:

let num = 5
console.log(++num)  // logs 6
console.log(num)    // logs 6

Knowing this, the ++ in example #6 of the article doesn't actually do anything:

const pureInsert1 = (arr, el, i) => [...arr.slice(0,i), el, ...arr.slice(i++)]
const pureInsert2 = (arr, el, i) => [...arr.slice(0,i), el, ...arr.slice(i)]

const nums = [1,2,4,5]

pureInsert1(nums, 3, 2)  // [ 1, 2, 3, 4, 5 ]
pureInsert2(nums, 3, 2)  // [ 1, 2, 3, 4, 5 ]

If anything the ++ is confusing because it looks as if the user is intending to increment i. If the parameter actually changed, we would get a defective insert:

const pureInsert = (arr, el, i) => [...arr.slice(0,i), el, ...arr.slice(++i)]

const nums = [1,2,4,5]

pureInsert(nums, 3, 2)  // [ 1, 2, 3, 5 ]
Thread Thread
renatomachado profile image
Renato Machado

I understend How the ++ operator Works, but I believe by changing a parameter value, the function will no longer be a pure function. I think that the ++ operator Will result in a side effect to the third parameter.

Thread Thread
sloths_r_cool profile image
Robin Kim

Nope, the function will remain pure because primitive types in JavaScript are passed by value. This means when i is passed in as an argument, pureInsert creates a brand new functional context that contains its own copy of i as opposed to referencing the original variable that was passed in. Since the function cannot mutate the original variable, purity is maintained.

let myNum = 1

const attemptMutation = (i) => {
  console.log(++i)
}

console.log(myNum)      // logs 1
attemptMutation(myNum)  // logs 2
console.log(myNum)      // still logs 1

If you really wanted to make the ++ operator "impure", you could use a higher-order function.

Thread Thread
renatomachado profile image
Renato Machado

I didn't know that. Thanks for the clarification.

Thread Thread
sloths_r_cool profile image
Robin Kim

Sure thing! :)