Chained assignments – beware of dragons!

July 31, 2019
I try to bend the internet to my will.
Wizard 26 posts
Followers: 19 people
0

Chained assignments – beware of dragons!

I try to bend the internet to my will.
Wizard 26 posts
Followers: 19 people
July 31, 2019

From time to time I come across code like the following:

a = b = x = 0;

This is shorthand for the following:

x = 0;
b = x;
a = b;

You can also use it with operators. For example:

x = 10;
a = b = x * 2;

This may seem like nice concise code, and it can be, but it can also cause unexpected problems.

Let’s try some examples:

Example 1

x = 10;
a = b = x;

writeDump([
    a: a, // 10
    b: b, // 10
    x: x // 10
]);

Example 2

x = 10;
a = b = x / 2;

writeDump([
    a: a, // 5
    b: b, // 5
    x: x // 10
]);

That all works as we’d expect. Let’s check what happens when we change one of the variables after it’s value has been assigned.

x = 10;
a = b = x;
writeDump([
    a: a, // 10
    b: b, // 10 
    x: x // 10
]);

// check that we can mutate without affecting others...
a *= 5;
x /= 2;

writeDump([
    a: a, // 50
    b: b, // 10
    x: x // 5
]);

What about string values?

x = "Hi";
a = b = x;

writeDump([
    a: a, // Hi
    b: b, // Hi 
    x: x // Hi
]);

// check that we can mutate without affecting others...
a &= " there!";
x = "Hello";

writeDump([
    a: a, // Hi there!
    b: b, // Hi
    x: x // Hello
]);

Again, we get the expected output. So where are those dragons hiding?

x = {};
a = b = x;    
writeDump([
    a: a, // {}
    b: b, // {}
    x: x // {}
]);

// check that we can mutate a without affecting others...
a.foo = "bar";
x.foo = "choo";

writeDump([
    a: a, // {"FOO": "choo"}
    b: b, // {"FOO": "choo"}
    x: x // {"FOO": "choo"}
]);

This time, we can see that all of the variables reference the same struct, so any changes you make will apply to each of the variables. This is expected behaviour but may trip you up if you are not aware. Remember that this:

x = {};
a = b = x;

Is the same as doing this:

x = {};
b = x;
a = b;

So if you’re using primitive values then it looks like it’s fine to use chained assignment. Well, there is one more dragon hiding in the shadows. Here’s a contrived bit of code.

function cubed( n ) {
    var inner_a = inner_b = n;
    return inner_a * inner_b * n;
}

writeDump( cubed( 3 ) ); // 27

The code is dead simple and returns exactly we’d expect. However, there is a side effect you might not be aware of.

function cubed( n ) {
    var inner_a = inner_b = n;
    return inner_a * inner_b * n;
}

writeDump([
    result: cubed( 3 ), // 27
    inner_a: isNull( inner_a ), // YES
    inner_b: isNull( inner_b ) // NO
]);

Did you expect inner_b to leak outside of the function?

What is actually happening is this:

function cubed( n ) {
    inner_b = n;
    var inner_a = inner_b;
    return inner_a * inner_b * n;
}

We really don’t want variables to leak outside of the function like this and from looking at the chained assignment version of the function it’s not obvious that this is going to happen.

You can solve it by adding an extra var in the chained assignment.

function cubed( n ) {
    var inner_a = var inner_b = n;
    return inner_a * inner_b * n;
}

writeDump([
    result: cubed( 3 ), // 27
    inner_a: isNull( inner_a ), // YES
    inner_b: isNull( inner_b ) // YES
]);

Now the inner_b variable does not leak outside of the function.

Personally, I avoid chained assignment as it’s just more trouble than it’s worth, but if you do want to use it, hopefully this post will have highlighted some potential gotchas.

Comments (0)
Add your comment