1

I'm getting inconsistent results when passing objects between modules in a Node JS application, or probably just misunderstanding what is going on!

In my app, written out in pseudo-code below, a lib file is first required into the application root, followed by the other services used by the application, which also require in the same library. The library is then configured in the application root - this sets a class variable (this.foo) which was initialized as an empty object in the library - and finally a method in one of the services is called.

The foo property is destructured at the top of the service file and if I log it out straight away I get an empty object (as expected). When I call the service method, after foo has been configured elsewhere, and reference the destructured property I still get an empty object. If instead, I don't destructure the property and just require in the library (Lib), then in the method I access Lib.foo I see the configured value (i.e. as expected).

My suspicion is that the destructured variable is a value that is not being updated and the required library is a reference, but everything I've read suggests that nothing is passed by value. Any pointers would be appreciated!

// main.js
// ========
const lib = require('./lib');
const service = require('./service');
lib.configure({ foo: "bar"});
service.run();


// service.js
// ========
const Lib = require('./lib');     // This is for the example only
const { foo } = require('./lib'); // I'd prefer to just do this

console.log(Lib.foo); // {} as expected
console.log(foo);     // {} as expected

class Service {
    run() {
        console.log(foo);    // {} - should be "bar"
        console.log(Lib.foo) // "bar" as expected
    }
}
module.exports = new Service();


// lib.js
// ======
class Lib {
    constructor() {
        this.foo = {};
    }
    configure({ foo }) {
        this.foo = foo;
    }
}
module.exports = new Lib();
nem035
  • 34,790
  • 6
  • 87
  • 99
Toomy
  • 318
  • 2
  • 10

2 Answers2

4

That is the correct behavior.

What the = operator (the assignment) does is it updates the pointer to new memory location containing a new value, it doesn't change the value we are currently pointing to.

In case of primitives such as strings, the general implementation is to actually copy the entire value over during variable re-assignment.

Extra: this is usually called String interning

Here's an example:

var x = "123";  // point the variable named "x" to a memory location containing the string "123"
var y = x;      // point the variable named "y" to a new memory location into which we copy the data from the memory location that "x" is pointing to
x = "456";      // point the variable named "x" to a new memory location that that contains the value "456"
console.log(y); // still "123", "y" still points to a memory location containing the copy of "123"

And here's a diagram of what is happening.

var x = "123";

case 1

var y = x;

case 2

x = "456";

case 2

Note: the initial "123" is still left in memory except nothing is pointing to it anymore so the garbage collector will clean it up eventually.

In the case of objects, the situation is a bit different. Instead of copying the value, we are copying the pointer to the memory location holding that value but re-assignment behaves the same.

var x = {};          // point the variable named "x" to a memory location containing the empty object
var y = x;           // point the variable named "y" to the same memory location that "x" is pointing to
x = { name: "foo" }; // point the variable named "x" to a new memory location that contains the object { name: "foo" }
console.log(y);      // still {}, "y" still points to a memory location containing the empty object

Here's a diagram of what is happening:

var x = {};   

case 1

var y = x;

case 2

x = { name: "foo" };

case 3

When you destructure foo out of an instance of Lib, it is not the same foo as the one internal to that instance, it is a new variable pointing to the same memory location as the internal variable. This location contains {}.

When you call .configure, you are updating the memory location where the internal value is pointing to but the destructured variable is still pointing to old location containing {}.

If you would update the object foo is pointing to instead of the reference itself, everything would work as you expected it:

configure({ foo }) {
    Object.assign(this.foo, foo);
}

I would suggest not doing the destructuring for persistent getters like above that you expect to hold state across updates. Having these extra variables increases complexity (because it increases the number of references to maintain) and can also lead to memory leaks (stale references point to unused data which prevents garbage collection).

Furthermore, destructuring methods out of a class can lead to bugs like the one above as well as unintended behavior of this.

You will save yourself a few headaches if you always call Lib.foo instead.

nem035
  • 34,790
  • 6
  • 87
  • 99
  • Thanks for such an in-depth response and more so one that I could understand! I'll stick with the Lib.foo approach rather than updating the object, but worth bearing in mind. – Toomy Oct 29 '18 at 22:44
  • Glad to help out mate – nem035 Oct 29 '18 at 23:21
0

So I think you're mixing up references and values. The reason why the value of { foo } doesn't change while the value of Lib.foo does is because in the case of foo, you are assigning a copy of the value of Lib.foo at that time. If you want to mutate the data of Lib and have it spread, you will need to work with the object (and not deconstruct the argument like you are currently doing).

Hope this helps!

  • Thanks Jack - same point as nem and easier to digest, although accepted nems answer because of the additional detail. – Toomy Oct 29 '18 at 22:46