Javascript two-way reference issue - javascript

I am very confused about Javascript referencing. I understand when referencing an object, there is a reference made. Changing the parent changes the copy and vice versa.
What I am confused about is when reassignment changes are made to the parent, the copy retains everything. See my example
let tester = {
"hello": "how are you",
"myArrayHere": [
{ "id": 1, "name": "me" },
{ "id": 2, "name": "you" },
{ "id": 3, "name": "them" },
]
};
var something = tester.myArrayHere.find(x => x.name === "you");
console.log(something);
console.log("--------");
something.id = 99;
console.log(something);
console.log("--------");
console.log(tester.myArrayHere[1]);
console.log("--------");
tester.myArrayHere[1].id = 88;
console.log(something);
console.log("--------");
tester.myArrayHere[1] = {};
console.log(tester.myArrayHere[1]);
console.log("--------");
console.log(something)
If you run that example, something on the last line, still has the entire object, even though two lines above, its reference was re-assigned.
There are other examples of this, such as when you delete things off the parent, etc. If this is purely a reference and not a copy (such as with primitive types) why are these changes not affecting it as it should?

The something variable simply refers to a (pre-defined) object stored in the memory. It's value is the returned value (a referral to an object) of the Array#find method. Variables do not observe any specific path (e.g. tester.myArrayHere[1]), they are not observers. In other words, in this case, JavaScript interpreter doesn't care/remember how you get the object/value before the assignment.
> var a, b; a = b = {};
> a === b
true
> a = {}
> a === b
false
After executing tester.myArrayHere[1] = {};, the second element of the array refers to a new object. The something variable still refers to the old object.

Please check the explanation in a snippet
let tester = { // tester is a link to a object (not an object itself) in memory
"hello": "how are you",
"myArrayHere": [ // tester.myArrayHere is a link to a object (not an object itself) in memory
{ "id": 1, "name": "me" },
{ "id": 2, "name": "you" }, // tester.myArrayHere[1] is a link to a object (not an object itself) in memory
{ "id": 3, "name": "them" },
]
};
var something = tester.myArrayHere.find(x => x.name === "you"); // something now is the same link to the object { "id": 2, "name": "you" }, but not an object itself tester.myArrayHere[1] will be === something link is eqaul to link
console.log(something);
console.log("--------");
something.id = 99;
console.log(something);
console.log("--------");
console.log(tester.myArrayHere[1]);
console.log("--------");
tester.myArrayHere[1].id = 88;
console.log(something);
console.log("--------");
tester.myArrayHere[1] = {}; // now tester.myArrayHere[1] becomes a link to a new object, { "id": 2, "name": "you" } is still in a memory and something is link to it
console.log(tester.myArrayHere[1]);
console.log("--------");
console.log(something)

Its best to conceptualize the code you posted as "having a direct shortcut to the nested field"
const joe = { // (A) joe is an object
id: 42,
name: 'joe',
hobbies: [ // (B) hobbies is an Array, a reference type
'surfing',
'videogames'
]
}
// (C) this variable has no idea that it is part of person object from (A)
const cloneHobbies = joe.hobbies
cloneHobbies.push('boxing')
console.log('***JOE***', joe) // Joe now has a new hobby: boxing
// It also works the other way; Joe's hobbies become someone else's
joe.hobbies.push('karate')
console.log("***CLONE'S HOBBIES***", cloneHobbies) // now has 'karate'
Hope this helps you conceptualize the code.
Cheers,

Related

Javascript : Modify object and add new index to a property

My object is something like:
let items =
[
{
"creationTimeStamp": "2022-05-31T17:04:28.000Z",
"modifiedTimeStamp": "2022-05-31T17:04:28.000Z",
"locations": [
{
"id": "5ao",
"name": "Store1"
}
],
"typeId": "Lead"
}
]
I am trying to push the following object into the locations property:
{
"id": "9a0",
"name": "Store2"
}
I have tried doing
items1 = [];
for (var i = 0; i < items.length; i++) {
items1.id = "9a0";
items1.name = "Store2";
//9 is some static index value added
Object.assign({9 : items1}, items[i].locations);
}
If I console(Object.assign({9 : items1}, items[i].locations)); I can see 2 arrays inside it, but my items locations property is still the same.
My expectation is as below:
[
{
"creationTimeStamp": "2022-05-31T17:04:28.000Z",
"modifiedTimeStamp": "2022-05-31T17:04:28.000Z",
"locations": [
{
"id": "5ao",
"name": "Store1"
},
{
"id": "9a0",
"name": "Store2"
}
],
"typeId": "Lead"
}
]
I also tried to use items[i].locations.push(item1) but then got:
TypeError: Cannot add property 9, object is not extensible
I also tried to assign a new array to items[i].locations, but then got:
TypeError: Cannot assign to read only property 'locations' of object '#'
What can I do to get the desired result?
You seem to expect that the second argument given to Object.assign will be mutated. But it is the first argument that is mutated. That means your .locations is not mutated. Moreover, in comments you indicate that locations cannot be extended and that the property is read-only.
So that means you'll need a complete new object.
Some other remarks:
Don't initialise items1 as an array, since it is supposed to be a plain object.
Declare a variable with const, let or var and avoid implicit global declaration.
It is safer to declare the items1 object inside the loop, so you create a new object each time and don't mutate the same object. For your example code it makes no difference, but it can lead to unexpected behaviour.
As you don't need i for anything else than items[i], and you actually need a complete new structure, use .map instead.
So:
items = items.map(item => {
let obj = {
id: "9a0",
name: "Store2"
};
return {...item, locations: item.locations.concat(obj) };
});
I always think in terms of functions, and of immutability-by-default, so my approach might look like this, with addLocationToAll built atop a simpler addLocation. The code is fairly simple:
const addLocation = (newLoc) => ({locations, ...rest}) =>
({...rest, locations: locations .concat (newLoc)})
const addLocationToAll = (newLoc) => (items) =>
items .map (addLocation (newLoc))
const items = [{creationTimeStamp: "2022-05-31T17:04:28.000Z", modifiedTimeStamp: "2022-05-31T17:04:28.000Z", locations: [{id: "5ao", name: "Store1"}], typeId:"Lead"}]
const newLoc = {id: "9a0", name: "Store2"}
console .log (addLocationToAll (newLoc) (items))
.as-console-wrapper {max-height: 100% !important; top: 0}
items is an array so it must access the first position of the array, which would be the proposed object.
With this, from the proposed object you will extract thelocation attribute and since this is an array, you use the push function to insert the new object
items[0]
// ->
// {
// creationTimeStamp: '2022-05-31T17:04:28.000Z',
// modifiedTimeStamp: '2022-05-31T17:04:28.000Z',
// locations: [ { id: '5ao', name: 'Store1' } ],
// typeId: 'Lead'
// }
I try this:
items[0].locations.push({"id": "9a0", "name": "Store2" })
And now:
items[0]
//->
// {
// creationTimeStamp: '2022-05-31T17:04:28.000Z',
// modifiedTimeStamp: '2022-05-31T17:04:28.000Z',
// locations: [ { id: '5ao', name: 'Store1' }, { id: '9a0', name: 'Store2' }],
// typeId: 'Lead'
// }

IndexOf over a nested array response graph returning just []

I am trying to filter some articles from a graphql response, by articleTag. Se my structure below:
{
"id": "41744081",
"articleTitle": "text",
"articleContent": "text",
"categoryName": { "categoryName": "Company", "id": "38775744" },
"articleTags": [
{ "articleTag": "event", "id": "37056861" },
{ "articleTag": "car", "id": "37052481" },
]
},
{
"id": "41754317",
"articleTitle": "text",
"articleContent": "text",
"categoryName": { "categoryName": "Sales and Martketing", "id": "38775763" },
"articleTags": [{ "articleTag": "technology", "id": "37056753" }]
},
...
But when applying my function:
notificationFiltered () {
var articleResponse = this.response.allArticles;
var routeParam = this.$route.params.tagName; //contains the id of the tag
const filteredTag = articleResponse.filter((item) => {
return (item.articleTags.indexOf(routeParam) >= 0);
});
console.log(filteredTag);
},
When I'm "console.log" the result I get only a "[]". Not sure if is related with the way of query is being render, in the API I get the same formation but with this slightly difference
{
"data": {
"allArticles": [... the specify structure above]
}
}
while printing that with vue {{response.allArticles}} I just get the first structure, I think it shouldn't matter?
Thanks in advance for the advice
You won't be able to use indexOf for array of objects to find a matching object - only strict equality is needed, and that's hard to get in the reference land. Consider this:
const objs = [
{ foo: 'bar' },
{ foo: 'baz' },
{ foo: 'foo' } // whatever
];
const needle = { foo: 'baz' };
objs.indexOf(needle);
// -1
What? Yes, there's an object looking exactly like needle in that array - but it's a different object:
objs[1] === needle; // false
That's why indexOf just goes past that one - and gives out -1, a "not found" result.
What you should be able to use in this case is findIndex. Still you need to build the predicate to have a match. For example:
const objs = [
{ foo: 'bar' },
{ foo: 'baz' },
{ foo: 'foo' }
];
const needle = { foo: 'baz' };
objs.findIndex(el => JSON.stringify(el) === JSON.stringify(needle));
// 1
In this example, comparing results of JSON.stringify in the predicate function is a poor man's _.isEqual - just to illustrate the concept. What you should consider actually using in your code is either _.isEqual itself, or similar function available in toolkit of your choice.
Alternatively, you can just check for specific fields' values:
objs.findIndex(el => el.foo === needle.foo); // still 1
This will apparently find objects even if their other properties do not match though.

How to fix creating a new object in JSON file?

I'm trying to create a function that when called will update a specific object in json file. However, it updates the object as well as creating a new one.
I've tried many different methods in trying to get this to work, but all have failed. The closest I've got to it working is the code shown below, but it still doesn't do what is required.
This is my function:
var fs = require('fs');
var _ = require("underscore");
module.exports = {
personalUpdate: function (id, forename, surname, dob, gender, callback) {
let rawdata = fs.readFileSync('data.json');
let data = JSON.parse(rawdata);
let filtered = _.where(data['students'], { id: id });
let all = filtered[0];
all.forename = forename;
all.surname = surname;
all.dob = dob;
all.gender = gender;
data["students"].push(all);
fs.writeFileSync('data.json', JSON.stringify(data, null, 2), (err) => {
if (err) throw err;
});
callback("success");
}
}
And this is the JSON file that I want to update:
{
"teachers": [
{
"name": "",
"email": "",
"password": "",
"formGroup": "",
"id": ""
}
],
"students": [
{
"surname": "test",
"forename": "test",
"dob": "",
"homeAddress": "",
"homePhone": "",
"gender": "",
"tutorGroup": "",
"schoolEmail": "",
"grades": [
{
"french": 8,
"maths": 7
}
],
"id": ""
},
{
"surname": "test2",
"forename": "test2",
"dob": "",
"homeAddress": "test2",
"homePhone": "",
"gender": "",
"tutorGroup": "",
"schoolEmail": "",
"grades": [
{
"french": 9,
"maths": 8
}
],
"id": ""
}
]
}
I had to remove and change the objects and info inside them, as it contained confidential information.
When running this function, it finds the object that is specified in the parameter. It then updates that object, but it then creates another object at the bottom of the original JSON object, which it is not supposed to.
Also, is there a better way to update the specific objects in the JSON file?
tl;dr
The result set is duplicating because you are pushing it into the array
The change is being applied due to the variables holding the same object reference, so they are being mirrored across objects that share the same pointer.
Explanation
It creates a new one due to the data["students"].push(all); instruction.
When you manipulate objects in javascript you need to be aware of how the reference between them work, so you can avoid bugs and use them in your benefit.
For example, take this set of instructions:
let a = {"x": 1};
let b = a;
b.x = 3;
console.log(a) // it will output {"x": 3}
Notice that we:
Create an object with the prop x equal 1 and assign it to the variable a
Initialize a variable b with the value of a
Change the property x on the variable/object b
Then we can observe that the change was also reflected in the variable a, due to the object reference.
So, basically this is exactly what is happening with your instructions when you do all.forename = forename; it changes the variable all, but also the original object which it derives from.
Here is a nice reference that explains this concept more in-depth
#EDIT
I strongly advise you not using the sync version of functions like readFileSync since this blocks the event loop. Here is the official guidelines about it

If two JavaScript arrays of objects share elements, is all non-shared memory garbage-collected when one array is no longer referenced?

When two arrays have an element which points to (references) the same object, and you change the object via one array, it changes in the other array, as I have proven to myself in the first snippet below.
My question is, if array a1 is referenced in other code, such as an event handler, and a2 is not referenced anywhere else and goes out of scope, then do all "major" browsers garbage-collect the memory used by the a2 array itself, including pointers to the objects, and the memory for the object created in the 2nd push in the snippet below. If so, then since a2 no longer has pointers to the objects in a1, then if a1 finally goes out of scope, then will all of it's associated memory, including all objects it points to, be reclaimed by the GC, assuming no other pointers, besides those associated with a1 and a2, reference those objects?
I know it seems academic for this simple example, but I am writing an algorithm (see 2nd snippet) to make a nested object from a flattened version of the same data in an array, and in doing so I use a temp array which has elements pointing to each object in the original array. I plan to call it an indefinite amount of times, and I want to make sure I'm not leaking memory. There will be many more elements in the array than what is in the snippet.
I also know that there are other SO questions which have strong similarities to this one, and the answers have been helpful, but I don't think that anyone has definitively answered each part.
var a1 = [{first: 1, second: 2}, {first: 4, second: 2}, {first: 3, second: 4}];
var a2 = [a1[2]];
a2.push(a1[1]);
a2.push({first: 5, second: 8});
console.log('a1 = ' + JSON.stringify(a1, null));
console.log('a2 = ' + JSON.stringify(a2, null));
a2[0].first = 7;
a2[1].second = 9;
console.log('a1 = ' + JSON.stringify(a1, null));
console.log('a2 = ' + JSON.stringify(a2, null));
var events = [
{
"id": 7,
"parentId": 4,
"name": "Sub7",
"expected": 400,
"actual": 100
},
{
"id": 2,
"parentId": 1,
"name": "Sub2",
"expected": 200,
"actual": 100
},
{
"id": 4,
"parentId": 1,
"name": "Sub4",
"expected": null,
"actual": 100
},
{
"id": 8,
"parentId": 1,
"name": "Sub8",
"expected": 250,
"actual": 100
},
{
"id": 1,
"parentId": null,
"name": "Main",
"expected": null,
"actual": 100
},
{
"id": 6,
"parentId": 4,
"name": "Sub6",
"expected": 300,
"actual": 100
}
];
var temp = [];
var parent;
for (var i = 0; i < events.length; i++) {
if (temp[events[i].id]) {
Object.assign(temp[events[i].id], events[i]);
} else {
temp[events[i].id] = events[i];
temp[events[i].id].children = [];
}
var parentId = events[i].parentId;
if (!parentId) {
parent = temp[events[i].id];
} else {
if (!temp[parentId]) {
temp[parentId] = {
id: parentId,
parentId: undefined,
name: undefined,
expected: undefined,
actual: undefined,
children: [temp[events[i].id]]
}
} else {
temp[parentId].children.push(temp[events[i].id]);
}
}
delete temp[events[i].id].parentId;
}
temp = undefined;
document.write('<code><pre>' + JSON.stringify(parent, null, 2) + '</pre></code>');
It's actually very simple. As soon as items are no longer reachable via code (something goes out of scope), it is marked for deletion. Whether it is garbage collected at that moment is a detail of the implementation, but it will no longer be available in code.
So, when array2 goes out of scope, it's data will go away, but if an element in array2 is used in array1, that single piece of data will not go away.
Here's more on that.
The short version is this: if no code can access it, it will be garbage collected.
You can imagine a graph that starts at the root node (think window) and each subsequent node is data which can be accessed from that node. In your case, you get something like this:
root --> event handler --> array1 ---
V
object
^
array2 ---
You can see that there is a path back to the root via array1. No such path exists for array2. Therefore, array2 will be cleaned up and any values which also do not have a link back to the root will be cleaned up as well.
object does have a path back to the root via array1. Therefore, array2s reference will be collected but the actual data which makes up object will not.

Turn javascript dictionary into JSON format

I have a javascript dictionary:
{
"a": {
"b": {
"c": null,
"d": null
}
}
}
How can I turn it into a JSON object which I can specify the name and children property? Is there any elegant way to do it?
The JSON object could be:
{
name:"a"
children: [{
name:"b",
children: [{
name:"c",
children: null
},{
name:"d",
children: null}]
}]
}
You could create a recursive function for generating your output:
var x = {
"a": {
"b": {
"c": null,
"d": null
}
}
};
function generate(item, key) {
var result = {
name: key,
children: []
};
for (var _ in item)
result.children.push(generate(item[_], _))
if (result.children.length == 0)
result.children = null;
return (key == undefined) ? result.children : result;
}
console.log(JSON.stringify(generate(x), null, 1));
Output:
[
{
"name": "a",
"children": [
{
"name": "b",
"children": [
{
"name": "c",
"children": null
},
{
"name": "d",
"children": null
}
]
}
]
}
]
The above generate function returns a list instead of a dictionary, because it's possible to have more than one name at the root level. But if we are sure that we have only one name at the root name, we can generate the json like this:
console.log(JSON.stringify(generate(x)[0], null, 1));
Here's my solution. It's similar to JuniorCompressor's.
function generate(obj) {
// Return primitives as-is
if (!(obj instanceof Object)) return obj;
// Loop through object properties and generate array
var result = [];
for (var key in obj) {
result.push({
name: key,
children: generate(obj[key])
});
}
// Return resulting array
return result;
}
As mentioned, the resulting object will actually be an array (in case there is more than one root-level property in the original object). If you really need the resulting object to be an object with only properties name and value, then you should access the first element of the resulting array, like this:
generate(obj)[0]
Solution
You need a recursive function, which calls itself for children. Note that in your example, there is only one top-level child (a). I instead use the assumption that the top-level 'name' refers to the name of the actual object itself. If you want to get results exactly like you demonstrate, from an object called 'obj', run toJSON(obj).children[0]. For the overall function, try something like the following:
function toJSON(obj, name) {
var subTree = {
name: name,
children: []
};
if (obj !== null && Object.keys(obj).length >= 1) {
for (var child in obj) {
subTree.children.push(toJSON(obj[child], child));
}
} else {
subTree.children = null;
}
return subTree;
}
Results of toJSON(obj).children[0]:
{
"name": "a",
"children": [{
"name": "b",
"children": [{
"name": "c",
"children": null
},{
"name": "d",
"children": null
}]
}]
}
Results of toJSON(obj, 'obj'):
{
"name": "obj",
"children": [{
"name": "a",
"children": [{
"name": "b",
"children": [{
"name": "c",
"children":null
},
{
"name": "d",
"children": null
}]
}]
}]
}
Here's a line-by-line explanation:
Declares the function, which expects two arguments: the object, and it's name. If you're going to be using toJSON(obj).children[0], though, don't bother with the second argument. It won't affect the result.
Declares the result, an object containing information about the current level and all levels below in the object. If you consider the object a tree, this result contains information about the current branch, and all it's branches.
Declares the property 'name', containing the name/key of the object at the current level. When you call the function, you need to include the name as second argument because there is no way of dynamically finding the name of a variable. They're passed into functions by value. As described above, though, if you're looking for results EXACTLY like those in your example, you're going to use toJSON(obj).children[0], instead of toJSON(obj, 'obj'), and then don't need to bother with the second argument.
Declares the children array, to be filled below
Terminates the declaration begun on Line 2
Checks if the object ISN'T null, and that it has children, using a handy method of the Object built-in object, running Lines 7, 8 and 9 if so
Iterates over the children of the object, running Line 8 for each child
Recursively runs the toJSON() function for each child, to get it's subTree. Because the children can't dynamically figure out their own names, it passes those in as well.
Terminates the for loop begun at Line 7
If there are no children, run Line 11. This is only run if Lines 7, 8 and 9 are not.
Sets children to null (only run if there are no children, as checked by Line 6)
Terminates the else started at line 10
Returns the current subTree, either to the function if called recursively by the function, or to you if you called it yourself
Terminates the function
Information about the Previous Version, Pre-edit
The original function only used one argument, whereas that above has another argument for 'name'. This is because the original tried to figure out the name of each level within that same level, which I have since realized isn't possible in Javascript. Basically, the original didn't work, and an extra argument had to be added to make it work. For records' sake, though, here was the original function:
// THIS FUNCTION DOESN'T WORK. IT'S HERE ONLY FOR HISTORICAL ACCURACY:
function toJSON(obj) {
var subTree = {
name: obj.constructor.name, // This should get the object key
children: []
};
if (Object.keys(obj).length >= 1) { // If there is at least one child
for (var child in obj) {
subTree.children.push(toJSON(obj[child]));
}
} else {
subTree.children = null;
}
return subTree;
}

Categories