0

I have a bizarre issue happening here. I am retrieving data from a remote source and I need to reformat it for better internal use

The data structure is like the following:

clubs = [
   0: {
      propA: "blah",
      probB: "bar",
      events: [
         0: {
           data1: "foo",
           data2: "bar"
           }
         1: {
           data1: "this",
           data2: "that"
         }
     1: {
        propA: "hello",
        probB: "bye",
        events [
           0: { ...}
           1: {...}
     ...
     ... 

I am looping through clubs and I want to assign the values of each club into its own events as property clubInfo. When I do that though, clubInfo becomes and endless chain of itself. So I delete the new clubInfo.events - and then everything but the root club[0] gets wiped out. I hope I am being clear, its hard to explain.

If I do this:

  for (var i=0; i<clubs.clubs.length;i++) {
    var clubInfo = clubs.clubs[i] ;
    var events = clubs.clubs[i].events ;
    for (var j=0; j<events.length; j++) {
    }
  }

Then clubs becomes:

   clubs [
       0: events [
           clubInfo [
               0: events [
                   clubInfo [
                       0: events [
                          ....and goes on seemingly forever
                          ]
                       ]
                   ]
               ]   
            ]
        ]

If I do:

  for (var i=0; i<clubs.clubs.length;i++) {
    var clubInfo = clubs.clubs[i] ;
    delete clubInfo.events ;
    var events = clubs.clubs[i].events ; // events is now empty
    for (var j=0; j<events.length; j++) {  // errors here because there is no length after the delete
    }
  }

Then all that remains of clubs is, in fact none of the other properties that have arrays events or others (several of them) are all gone.:

   clubs [
       0: { 
          propA: "blah",
          probB: "bar"
          }
       ]

Instead of delete, i have even tried just nulling clubInfo.events = null - but the same issue happens, everything gets wiped out.

rolinger
  • 2,787
  • 1
  • 31
  • 53
  • The syntax in your first bit of sample code is invalid; it's hard to help when the code won't even parse. *edit* and the "events" array doesn't have a closing `]`; is that really how it looks? – Pointy Jan 08 '19 at 21:50
  • @Pointy - which part? my data sets aren't perfect, just examples of structure. – rolinger Jan 08 '19 at 21:52
  • 1
    That is because `clubInfo` and `clubs.clubs[i]` are just references to the same object. – Titus Jan 08 '19 at 21:52
  • @titus ok, thats what I was thinking...but how can I create a new object so that clubInfo stands alone and is not a reference. – rolinger Jan 08 '19 at 21:52
  • Array literals don't have explicit indexes, so the whole `0:` thing in the `clubs` array is incorrect. And there's the problem with the "events" arrays not having closing `]`. Also the code mentions `clubs.clubs[i]` but there is no "clubs" property in what you posted as the value of the "clubs" variable. – Pointy Jan 08 '19 at 21:53
  • 2
    You should create a clone [more details here](https://stackoverflow.com/q/122102/1552587) – Titus Jan 08 '19 at 21:55
  • @Titus - thanks. I thought this may be happening but wasn't certain. Good reference now that I understand the issue. Kudos! – rolinger Jan 08 '19 at 21:58
  • @titus - wow...spent hours on this. This fixed it in 2 seconds: `var clubInfo = JSON.parse(JSON.stringify(clubs.clubs[i])) ` – rolinger Jan 08 '19 at 22:04
  • 1
    @rolinger Right, `JSON.stringify(obj)` is more or less what you're looking for. For more information check out my answer. I modified your code with that in mind, and there are a few other gotchas listed. Happy coding! – zfrisch Jan 08 '19 at 22:32

1 Answers1

3

Oh boy, you've snagged one of JavaScript's current, and most obvious flaws by effectively using this:

 clubs[i].event[j].clubInfo = clubs[i];

You're creating an infinite reference - what do I mean by that? It's better displayed through an Array, if you'll oblige me:

let a=[]; a.push([a]);

This creates an infinite level array through self-reference, creating an incalculable depth. You see, though there's a 32(2^32-1) bit limit to an Array's length. This can be demonstrated easily:

 Array(2**32); //Uncaught RangeError: Invalid array length

Presumably this was done to prevent browser memory from shorting but, strangely, there was never any consideration to the depth an array may contain. A minor side effect of this is that there is no depth property, but a major side effect is that there is no protection from an infinite self-referencing array.

Getting Around It

The best way to get around this type of situation is to construct a new Object and assign properties from the old Object to the new. You can think of this as cloning. To do this you can utilize the assign method:

Object.assign(constructor, **Array**/**Object**)

Example:

let a = []; a.push(Object.assign([], a)); 

Problem solved, right? uhhh... not quite Even though this can sometimes work, this still won't fix the issue of an Array or Object with more than shallow references. To get around that you have to use a combination of:

  JSON.stringify(obj);

to break references

  JSON.parse(JSON);

to remake your object, and

  delete obj[deepReference];

deletion to stop any unforeseen issues with any superfluous data/references

None of this is ideal, but currently there is no way to completely separate all references inside of an object or array without recursive iteration.

To give you an example - In your case you're going to want to do something like this:

for (var i = 0; i < clubs.length; i++) {
  var clubInfo = clubs[i];
  var events = clubs[i].events;
  for (var j = 0; j < events.length; j++) {
    let jsonTranslation = Object.assign({}, clubs[i]);
    delete jsonTranslation.events;
    jsonTranslation = JSON.stringify(jsonTranslation);
    clubs[i].events[j].clubInfo = JSON.parse(jsonTranslation);
  }
}

let clubs = [{
  propA: "blah",
  probB: "bar",
  events: [{
      data1: "foo",
      data2: "bar"
    },
    {
      data1: "this",
      data2: "that"
    }
  ]
}];


for (var i = 0; i < clubs.length; i++) {
  var clubInfo = clubs[i];
  var events = clubs[i].events;
  for (var j = 0; j < events.length; j++) {
    let jsonTranslation = Object.assign({}, clubs[i]);
    delete jsonTranslation.events;
    jsonTranslation = JSON.stringify(jsonTranslation);
    clubs[i].events[j].clubInfo = JSON.parse(jsonTranslation);
  }
}

console.log(clubs);

Additional Info: Other watch outs

Similarly there are other issues in the language. A badly implemented Array constructor method. Array(n) returns an Array with n members. Why's that an issue? Everything in JavaScript that is declared and not instantiated is undefined except the members of a freshly constructed array. They return empty. The issue with that is this means they have no mappable values. Ergo, all those sweet new functional ES Array methods are useless until the Array is filled. As an example:

Array(3).map((m, i) => i);

This results in well... the same thing you started with — when clearly it should provide a numbered array from 0-2. This is not as big of a deal as an infinite reference because you can work around it like this:

Array(3).fill(0).map((m,i) => i);

But it's effectively a wasted method call to take care of a problem that should be handled within construction.

Lastly, the fill method — give it an object or an Array and it doesn't create n individual object members. It creates n references to a singular array or object and stuffs them into one array. At base logic, it sort of makes sense. As @FelixKling pointed out in the comments, it is fairly consistent with what you would expect, a.e. fill this array with this one thing. I still would debate a bit about it's functionality for two reasons.

  1. In what situation would anyone need n references stored in an Array to the same place in memory? Probably never. How often do people need an Array of similarly constructed Objects? All the time.

  2. When passing an Object Literal for instance ( .fill({a:1}) )I can see the logic of filling the Array with references, even if it may not be wholly intuitive. If passed a Constructor- I would contest that it might make more sense to instantiate each member individually.

So there are many nuances, and inconsistencies with JavaScript that require knowledge to work around — and sadly, infinite reference, is one of them - but on the plus side the only way to typically realize these issues exist is to run into them headlong, so be thankful it wasn't in production!

Hope this helps! Happy Coding!

zfrisch
  • 8,474
  • 1
  • 22
  • 34
  • 1
    @FelixKling haha, yeah, sorry. I'm passionate. I did edit it to remove some of the disdain :) – zfrisch Jan 08 '19 at 22:37
  • Also there are applications where circular references are useful, e.g. graphs or doubly linked lists. It's just something you need to be aware of, but I guess that comes with reference-type values. – Felix Kling Jan 08 '19 at 22:44
  • 1
    *"So there are many problems and inconsistencies with JS that require knowledge to work around "* The `.fill` method is very consistent with how the rest of JavaScript works. It would be rather inconsistent that passing a single object to a function would result in multiple objects being created. I agree with the `Array(n)` stuff though. But that is one of the reasons it is advised to avoid it. – Felix Kling Jan 08 '19 at 22:47
  • @FelixKling I'm curious if you have something in mind for circular references being a good thing? A more specific example I mean. I'm having a hard time thinking of one. As far as `.fill` - like I said, it makes sense in a basic way. I'd consider it more of an unfortunate nuance, I suppose. It's not that I think passing an Object literal straight into `.fill` should cause multiple constructed objects, but I guess my want would be that if I passed in a constructor it would be wise enough to separately instantiate in each member of n. I do see where you're coming from though. – zfrisch Jan 08 '19 at 22:53
  • A lot of data is circular. Basically any parent <-> child relationship. The DOM is already circular. Imagine you cannot access `element.parentNode` (or `element.children` on the flip-side) because of JavaScript limitations. Sure, maybe the same DOM node could be exposed in JavaScript via multiple object instances. But then it would be weird that `element.children[0].parentNode === element` is `false`. But maybe JavaScript should allow custom comparison implementations (more complication). Everything is trade offs. – Felix Kling Jan 08 '19 at 23:05
  • I think part of what you are uncomfortable with here is actually solved with immutable values. But I don't know how languages who have first class immutable (complex) values handle circular data (maybe they don't). Have to do some research there. – Felix Kling Jan 08 '19 at 23:08
  • @FelixKling - Ah, sorry. I made the transposition when you said "circular" you meant "self-referencing". Nodes referencing one another isn't an issue at all, and of course I'm with you in saying that's a perfectly fine thing. haha, I have a feeling I'd have to be mental not to agree with that considering that's how a lot of technology works on even a fundamental level, but self-referencing? I think that it should be innately prevented, either by throwing an error or having the language intuitively create a singular deep clone to place within itself. – zfrisch Jan 08 '19 at 23:17
  • Yeah, self-references are certainly a strange case (but note that `clubs[i].event[j].clubInfo = clubs[i];` in your example is not a self-reference). What comes to mind are state machines where some transitions lead back to the same state (not that these things couldn't be modeled differently, but it's one possible case). – Felix Kling Jan 08 '19 at 23:27
  • I admit it's a bit paradoxical - but I do consider it a self-reference. Something in the model is referencing the model. if you had `{a: { b: {} }}` and said `a.b.c = a` it's debatable that it's second-hand, but still something in the model is referencing the model. -- If you meant that something in the code is literally off - it's possible. The original data the OP gave and the object manipulation he was trying to pull off didn't match up, so I edited his code to make it fit his original data. – zfrisch Jan 09 '19 at 00:13
  • But how is that different than `element.children[0].parentNode === element` (except that the browser created it for you?). I'm just saying that this is a valid case, no matter what it is called. – Felix Kling Jan 09 '19 at 00:17
  • @FelixKling haha, I knew that was coming up. DOM nodes obviously reference each other, but it's clearly not an infinite circle. It's a Tree from window, to document, to everything else. If you look at the node prototype it's clear there's a lot of information, and references to other nodes, but there isn't an infinity to this. At some point a `parentNode` will be null, for instance, but at no point will you find the document with a child reference, stuffed within the child that is referenced. . – zfrisch Jan 09 '19 at 00:32
  • You can do `element.children[0].parentNode.children[0].parentNode.children[0]...` as long as you want. It's exactly the same situation. That's what circular (and self) references are. – Felix Kling Jan 09 '19 at 00:36
  • @FelixKling I understand what you're saying, but I think it's probably a miscommunication in my use of terminology. I agree with you, but I suppose what I'm trying to convey in simple terms is that a node does not have an infinitely self-referencing property. Sorry for that. – zfrisch Jan 09 '19 at 18:27