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 withconst 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