Destructured object loses reference after being modified - javascript

Object reference is lost after an assignment. Any previous references are no longer relevant.
I have the following:
// lib.js
const obj = { prop: { data: { some: 'empty' } } };
function loadObject() {
obj.prop.data = { some: 'load' };
}
modules.exports = { prop: obj.prop, data: obj.prop.data, loadObject() };
and
// main.js
const { prop, data, loadObject } = require('./lib');
loadObject();
console.log(prop.data); // data changed (new reference)
console.log(data); // data not changed (old reference)
How can I modify data without losing it's initial reference?
PS:
I have a bunch of files importing this lib and it's relying on that data.
I'd rather not replace it to prop.data or reread it again in each file that uses it.

You replaced data with a new object. There’s no way to change everything that references an old object to point to a new object instead, so if you want to keep the exact same API, you have to alter the object in place instead of replacing it – maybe like you did before:
// if it’s a single property that’s the same before and after
function loadObject() {
obj.prop.data.some = 'load';
}
// if the properties before are a subset of the properties after
function loadObject() {
Object.assign(obj.prop.data, { some: 'load' });
}
// if you need to change the set of properties entirely
function loadObject() {
for (const key of Object.keys(obj.prop.data)) {
delete obj.prop.data[key];
}
Object.assign(obj.prop.data, { some: 'load' });
}

The problem is not related to different modules systems or objects de-structuring. Please see the following example.
const obj = {prop: {data: {some: 'empty'}}};
const objRef = obj.prop.data;
console.log(objRef);
obj.prop.data.some = 'not empty';
console.log(objRef);
obj.prop.data = {other: 'hello!'};
console.log(objRef);
console.log(obj);
Remember that each time that you use the bracket notation to create objects {} you are implicitly instantiating a new "independent" object. This is still valid for nested objects like the one in your example. Indeed you have multiple nested references of multiple objects.
The 'name' of the nested object (the key of the parent) acts as a variable that contains the reference of the new object that you are instantiating using {}.
I know that this may appear confusing (because it really is), but if you follow the previous snippet I'm sure you will get it. objRef keeps a reference of the "independent" object {some: 'empty'}, that at the same time is stored as a reference in all the other objects until the primary object obj. In this line: obj.prop.data = {other: 'hello!'}; we are completely erasing {some: 'empty'} reference from the primary object, but it is kept in objRef which will be the only remaining reference for that object. We are separating objRef from obj.
Summary:
Each time you use {} -> equivalent to -> new Object()
obj.prop.data.some = 'not empty'; -> Changing property value. Will be seen in all references of data: {} object.
obj.prop.data = {other: 'hello!'}; -> Completely replacing data: {} object reference in main object (destroying reference) (still kept in objRef).
If it is not clear I could try explaining it in a different way. Please let me know.

Having any variable seemingly alter itself on its own (as a consumer of the library would experience) is quite unintuitive, and is often forbidden by linters for the sake of code clarity - if that sort of logic is needed, consider exporting a function which returns the current data instead, eg
modules.exports = {
prop: obj.prop,
getData: () => obj.prop.data,
loadObject
};
(note that to export the function, you need to just reference the function, not call it)
While it would be possible to export a mutable binding if you were using ES6 modules, that's not the case here, and wouldn't be a good idea anyway.

Related

javascript variable changing value after changes in original variable

I am having trouble maintaining the original value of a variable after making new changes to the original variable.
Code:
(...)
data = Illumination.calculate_N(data)
data = Illumination.calculate_pi(data)
data = Illumination.calculate_kwh(data)
data = Illumination.calculate_ca(data)
let data_base = data
let ca_base = data.ca
let kwh_base = data.kwh
let pi_base = data.pi
(...)
data = Illumination.calculate_N(data)
data = Illumination.calculate_pi(data)
data = Illumination.calculate_kwh(data)
data = Illumination.calculate_ca(data)
let data_proposto = data
let ca_proposto = data.ca
let kwh_proposto = data.kwh
let pi_proposto = data.pi
-----------------------------------
EXAMPLE:
static calculate_ai(data){
data.ai = data.areaTotal*data.au
return data
}
It was expected that the original variable (date) would have its values ​​changed, and this happens correctly, however, the variables data_base and data_proposto are not keeping their values
Both variables at the end of the calculation have the same values ​​as the variable date
The variables ca_proposto, ca_base, and the like store their values ​​correctly
Any idea?
The only interactions of the variables data_base and data_proposto were their creations with the data variable and their return of the function
OBS: If I use console.log () to view the value of the data_base variable before redoing the new calculations (Illumination.calculate_N (data)), the value of the variable appears correctly as it should, it is changed shortly after these calculations.
Because in both cases you are assigning not the object itself in the current state, but a reference to that object. What you need to do is to clone the object so the state is frozen at that point.
Simple Clone (Shallow Copy)
let data_base = Object.assign({}, data); //you get a clone of data
let data_proposto = Object.assign({}, data);
The limitation here is that it only does a shallow copy. See Deep Copy below for further explanation.
JSON Clone
This is a quick-and-dirty way to clone as it converts a JSON object to a string, and then back. i.e. you are no longer getting a reference, but a new object.
let data_base = JSON.parse(JSON.stringify(data));
let data_postero = JSON.parse(JSON.stringify(data));
But this won't work if your object is not JSON-safe.
Deep Copy
The least elegant method is probably safest. It deep copies the properties over into a new object. The key difference with Object.assign() is that it copies the values of nested properties, whereas Object.assign() copies the reference to nested objects.
So with Object.assign() any subsequent changes in your nested objects will affect all versions of your "clones". This won't happen if your clones only have property values of those nested objects at the time of cloning – these values are not affected by any changes to the nested objects.
const deepCopy = function(src) {
let target = {};
// using for/in on object also returns prototype properties
for (let prop in src) {
// .hasOwnProperty() filters out these prototype properties.
if (src.hasOwnProperty(prop)) {
target[prop] = src[prop]; //iteratively copies over values, not references
}
}
return target;
}
let data_base = deepCopy(data);
let data_postero = deepCopy(data);
#chatnoir Defined the problem very well, But I do not agree with his JSON serialization solution due to the below probleam:
You will lose any Javascript property that has no equivalent type in
JSON, like Function or Infinity. Any property that’s assigned to
undefined will be ignored by JSON.stringify, causing them to be missed
on the cloned object.
My suggestion to perform deep copy is to rely on a library that’s well
tested, very popular and carefully maintained: Lodash.
Lodash offers the very convenient clone and deepclone functions to perform shallow and deep cloning.
Lodash has this nice feature: you can import single functions separately in your project to reduce a lot the size of the dependency.
Please find the running sample code here: https://glitch.com/edit/#!/flavio-lodash-clone-shallow-deep?path=server.js:1:0
You are using the same variable data inside and outside functions.
ie; data is in the global scope.
static calculate_ai(data){
data.ai = data.areaTotal*data.au
return data
}
even though you are expecting the scope of the variable data inside the method calculate_ai to be limited to that method, it is not the case. data is in global scope and therefore, the value changes inside the method for the variable affects outside as well.
An effective solution is to use a different variable inside the method.
A variable is like an octopus tentacle, and not as a box (as it’s commonly described). In this analogy, the variable's name can be thought of as the name of a tentacle.
A variable (tentacle) holds on to a value in what’s called a binding. A binding is an association of a variable to a value: x = 1.
In JavaScript, if a variable b holds on to variable a, changing the value to which variable a holds onto, will change the value to which variable b holds onto, as b and a are referencing to the same value:
let a = {key: 1}
let b = a
console.log(`a: ${a.key}`) // -> 1
console.log(`b: ${b.key}`) // -> 1
a.key = 2
console.log(`a: ${a.key}`) // -> 2
console.log(`b: ${b.key}`) // -> 2
a = {key: 3} // This will point variable 'a' to a new object, while variable 'b' still points to the original object.
console.log(`a: ${a.key}`) // -> 3
console.log(`b: ${b.key}`) // -> 2

get dynamic property defined in prototype during JSON.stringify

I've defined an enumerable property in the prototype object and would like it to appear when I convert a prototyped object to JSON.
My first idea was to set it in toJSON but because I don't really want to keep it in the object afterwards I'll have to more or less clone the whole object in the function and set the necessary property.
Redefining the property in the target object and just proxying with the context of the current object doesn't seem to be an option as well, since I can't really use apply or call when getting dynamic properties.
Working solutions I could come up with so far seem to require quite an amount of code and aren't flexible and concise enough, so I'm wondering if there are any best practices of solving this task.
Here is an example which could seem a bit synthetic but still, I believe, conveys the idea:
function ProjectFolder() {
this.files = [];
Object.defineProperty(this, 'size', {enumerable: true, get: function() {
return this.files.length;
}});
}
function GithubProjectFolder() {
this.files = ['.gitignore', 'README.md'];
}
GithubProjectFolder.prototype = new ProjectFolder();
var project1 = new ProjectFolder();
JSON.stringify(project1);
// output: {"files":[],"size":0}
// size is present
var project = new GithubProjectFolder();
JSON.stringify(project);
// output: {"files":[".gitignore","README.md"]}
// size is absent
I'll have to more or less clone the whole object in the function and set the necessary property.
Yes, and there's nothing wrong with that. That's how .toJSON is supposed to work:
ProjectFolder.prototype.toJSON = function toJSON() {
var obj = {};
for (var p in this) // all enumerable properties, including inherited ones
obj[p] = this[p];
return obj;
};
However, there are two other points I'd like to make:
The size of a folder doesn't really need to be stored separately in the JSON when it already is encoded in the length of the files array. This redundant data seems to be superfluous, and can confuse deserialisation. Unless something requires this property to be present, I'd recommend to simply omit it.
In ProjectFolders, the .size is an own property of each instance - in GithubProjectFolders it is not. This suggest that you're doing inheritance wrong. Better:
function GithubProjectFolder() {
ProjectFolder.call(this);
this.files.puhs('.gitignore', 'README.md');
}
GithubProjectFolder.prototype = Object.create(ProjectFolder.prototype);
If you'd fix that alone, the size will appear in the serialisation of your project.

What does 'symbol-keyed' mean in `JSON.stringify`

There is a Object generated by Node.js, it looks like this when I use console.log:
{ dataValues: { a: 1, b: 2 }, fn1: function(){}, fn2: function(){} }
when I use JSON.stringify, it return this string:
{"a":1,"b":1}
I checked the mozilla developer center and found this:
All symbol-keyed properties will be completely ignored, even when using the replacer function.
I think the 'dataValues' must be the 'symbol-keyed' property, but what does 'symbol-keyed' mean?
btw, I use the sequelizejs ORM lib to generate this object.
I found the reason finally in the same page:
If an object being stringified has a property named toJSON whose value is a function, then the toJSON method customizes JSON stringification behavior: instead of the object being serialized, the value returned by the toJSON method when called will be serialized.
It runs on browser normally.
Here is the jsfiddle to run it like I asked.
Code:
function test(data) {
for(var key in data){
this[key] = data[key];
}
}
test.prototype.toJSON = function(){
return this.dataValues;
}
var a = new test({dataValues: {a:1, b:2}});
console.log(a);//the instance
console.log(JSON.stringify(a));//{"a":1,"b":1}
Nah, the relevant part to your issue is this blurb:
If undefined, a function, or a symbol is encountered during conversion
it is either omitted (when it is found in an object) or censored to
null (when it is found in an array).
So in other words, if your JSON object contains functions, they are omitted during the JSON.stringify process.
"symbol-keyed" refers to a new primitive type introduced in ecmascript6. See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/typeof for more info.
This is an example of a "symbol-keyed" property:
{[Symbol("foo")]: "foo"}
Reference for JavaScript Symbol: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol

How To Set a Property's Property While Creating the Object

I have an object and one of the properties is an array. I want to set a new property on that array (customProp = 0). However, I want to do so inside myObject's declaration.
myObject = {
someString: "text",
someArray: ["one","two"],
/* What I've Tried */
someArray: { customProp: 0 } // Overwrites my array with a new object
someArray["customProp"]: 0 // Invalid syntax
someArray: customProp: 0 // Also invalid
}
If I can't create the array then set a property on it, can I do so in one fell swoop (again, staying within the confines of this object)?
I have another (small) question: How can I reference one of the properties still inside the declaration. I want to set otherProp = someString, how would I do so?
myObject = {
someString: "text",
otherString: someString, // someString is undefined
otherString: myObject["someString"], // myObject is undefined
otherString: this["someString"] // Just... undefined
}
I may need to split this into a separate question, but hopefully whoever answers the first will know the answer to the second.
Unfortunately neither of your requests are possible. Object literals in JavaScript are convenient but come with drawbacks, mostly what you've discovered.
When you are inside an object literal, the object doesn't quite exist yet. The JavaScript interpreter hasn't finished ingesting it. So inside the literal, this points to the object just outside the literal, and the literal has no way to refer to other parts of itself.
Fortunately, you can do both of what you want by just doing it after the literal declaration.
myObject = {
someString: 'text',
someArray: ['one', 'two']
};
myObject.someArray.customProp = 0;
myObject.otherString = myObject.someString;
Or if you want, you can wrap all of this inside a constructor function and create your object with new.
function MyObject() {
this.someArray = ['one', 'two'];
this.someArray.otherProp = 0;
this.otherString = this.someString = 'text';
}
var myObject = new MyObject();
Well, arrays are numeric-based, so
someArray["customProp"]: 0
wouldn't work. It should be a Javascript Object {} for string-based keys to work. And then you could just say
someArray: {0:"one",1:"two","customProp":0},
For your second question: I don't think that's possible. The object is not yet initialized, so you can't yet read out of it...
You can set properties on the Array but they will not get iterated using native functions. Your alternatives are an object (as suggested) or an array with an object as one of the members:
["one", "two", {customProp: 0}]

"too much recursion" error when calling JSON.stringify on a large object with circular dependencies

I have an object that contains circular references, and I would like to look at the JSON representation of it. For example, if I build this object:
var myObject = {member:{}};
myObject.member.child = {};
myObject.member.child.parent = myObject.member;
and try to call
JSON.stringify(myObject);
I get the error "too much recursion", not surprisingly. The "child" object has a reference to its "parent" and the parent has a reference to its child. The JSON representation does not have to be perfectly accurate, as I'm only using it for debugging, not for sending data to a server or serializing the object into a file or anything like that. Is there a way to tell JSON.stringify to just ignore certain properties (in this case the parent property of the child object), so that I would get:
{
"member" : {
"child" : {}
}
}
The closest I can think of is to use getChild() and getParent() methods instead of just members, because JSON.stringify ignores any properties that are functions, but I'd rather not do that if I don't have to.
You can pass a function as the second argument to stringify.
This function receives as arguments the key and value of the member to stringify.
If this function returns undefined, the member will be ignored.
alert(JSON.stringify(myObject, function(k, v) {
return (k === 'member') ? undefined : v;
}));
...or use e.g. firebug or use the toSource()-method, if you only want to see whats inside the object.
alert(myObject.toSource());
From the crockford implementation (which follows the ECMA specification):
If the stringify method sees an object that contains a toJSON method, it calls that method, and stringifies the value returned. This allows an object to determine its own JSON representation.
Then something like this should work just fine:
var myObject =
{
member: { child: {} }
}
myObject.member.child.parent = myObject.member;
myObject.member.child.toJSON = function ()
{
return 'no more recursion for you.';
};
console.log(JSON.stringify(myObject));​
http://jsfiddle.net/feUtk/

Categories