ImaginativeThinking.ca


A developers blog

What the Heck is the deal with no-param-reassign

By: Brad

Hey Brad I’m getting a linter error no-param-reassign what is up with that?

function foo(bar) {
  bar = 4;
  return bar;
}

The thing to remember about ECMAScript is that it passes by reference.

So when you pass a variable into a function you are not passing the variable value but a reference to the location in memory where the value is located.

const u = 5;
// u <== a memory location (0xaf445d) which holds the value 5

foo(u); // passing ref 0xaf445d into the function

When a variable holds a primitive (Number or String) and you re-assign its value your pointing it to a new memory location.

let u = 5;
// u <== a memory location (0xaf445d) which holds the value 5

u = 10;
// u <== a memory location (0xfab223c) which holds the value 10

When you modify a variable which holds a primitive value you are changing the memory location stored in the variable.

let u = 5;
// u <== a memory location (0xaf445d) which holds the value 5
u++
// u <== a memory location (0xbcd321c) which holds the value 6

When you assign the value of one variable to another you are assigning to the second variable the value of the memory location that the first variable is pointing to.

const u = 5;
// u <== a memory location (0xaf445d) which holds the value 5
const x = u;
// x <== a memory location (0xaf445d) which holds the value 5

Modifying the second variable will change the memory location the variable points to without affecting the first variable.

const u = 5;
// u <== a memory location (0xaf445d) which holds the value 5
let x = u;
// x <== a memory location (0xaf445d) which holds the value 5
x++
// u <== a memory location (0xaf445d) which holds the value 5
// x <== a memory location (0xbcd321c) which holds the value 6

But with Objects or Arrays the variable points to a memory location which it self holds references to other memory locations. Often its the child memory locations you end up modifying and not the reference to the object. This is where you can start getting into trouble because you end up changing the value of the callers variable.

const u = { hello: 'world' };
// u <== a memory location (0xaf445d) which holds an object
// u.hello <== a memory location (0xff438a) which holds the value 'world'
const x = u;
// x <== a memory location (0xaf445d) which holds an object

Notice here that x and u point to the same object reference. Thus if we modify one of x's properties it means we will also be modifying u's property.

x.hello = 'foo';

// u <== a memory location (0xaf445d) which holds an object
// u.hello <== a memory location (0xabc432f) which holds the value 'foo'

// x <== a memory location (0xaf445d) which holds an object
// x.hello <== a memory location (0xabc432f) which holds the value 'foo'

When dealing with Arrays or Objects you need to remember ECMAScript passes by reference.

What this means when it comes to functions is that when you pass a value into a function which you stored in a variable the function invocation will copy the reference address of your object into a parameter variable to be used by the function. If the value is an object or an array and you modify that parameter you will be modifying the callers variable.

function foo(user) {
  user.newProp = 'tada';
  return user;
}

const u = {
  name: 'billy',
};
const x = foo(u);

In the above both u and x will point to the same object and both will have the newProp property since the parameter user was pointing to the same memory location as u.

How to Make a Function immutable

The first thought when people see the no-param-reassign error is to copy the parameter value into a locally scoped variable.

function foo(userP) {
  const user = userP;
  user.newProp = 'tada';
  return user;
}

This does not work as we discussed above; both user and userP will be pointing to the same memory location. It will 100% remove the no-param-reassign error but you will still be mutating the callers variable.

How do we ensure we are not modifying the origin value? We need to clone the object.

function foo(userP) {
  const user = Object.assign({}, userP);
  user.newProp = 'tada';
  return user;
}

Above user and userP will point to two different memory locations. The user variable will point to a different memory location which has an object in it and that object will have properties which point to the same memory location as the properties on the object referenced by userP.

In ES2018 they add the Spread operator for Object Literals so instead of using Object.assign() you can perform shallow copies with const user = {...userP};

Because we now have a completely different object we can add properties to it and update the values in existing properties (we'll update the properties attached to our object to point to different memory locations) without affecting the callers object.

const u = {
  woohoo: 'tada',
};
const x = foo(u);

// u === { woohoo: 'tada' }
// x === { woohoo: 'tada', newProp: 'tada' }

The thing to keep in mind here is that we just did what is known as a shallow copy. A shallow copy just copies the values from the properties on the parent object and does not copy values of nested objects.

If for example we had:

const u = {
   obj: {
     hello: 'world',
   },
};

const x = Object.assign({}, u);

The variables u and x will point to different memory locations however u.obj and x.obj will point to the same location. We can reassign x.obj to a new value (change the memory location it points to) without affecting u, however; attaching new properties to x.obj or changing x.obj.hello will affect both u and x.

// u <== 0xacf1345b
// u.obj <=== 0xbb33c223
// x <== 0xff746400
// x.obj <=== 0xbb33c223

In cases where the object in question has nested object and you need to modify the nested objects you need to perform a deep clone which will generate new object in memory for the entire object hierarchy. In our case that would mean we would get different objects for both u and u.obj assigned to x and x.obj respectively. Just keep in mind that deep clones have a higher performance hit then shallow clones as one might expect so best to only do deep clones if you really need to otherwise use shallow clones if you can get away with it.

So that is what the deal is with the no-param-reassign linter error. It warns you that your function might cause unexpected side effects in the callers code because you could be modifying a shared memory location due to ECMAScript's pass by reference design.

Until next time think imaginatively and design creatively

Brad

My interest in computer programming started back in high school and Software Development has remained a hobby of mine ever since. I graduated as a Computer Engineering Technologist and have been working as a Software Developer for many years. I believe that software is crafted; understanding that how it is done is as important as getting it done. I enjoy the aesthetics in crafting elegant solutions to complex problems and revel in the knowledge that my code is maintainable and thus, will have longevity. I hold the designation Certified Technician (C.Tech.) with the Ontario Association of Computer Engineering Technicians and Technologists (OACETT), have been certified as a Professional Scrum Master level 1 (PSM I) and as a Professional Scrum Developer level 1 (PSD I) by Scrum.org as well as designated as an Officially Certified Qt Developer by the Qt Company. For more on my story check out the about page here

Feel free to write a reply or comment.