1

Most elegant solution required please. We all deal with objects that can be constructed incrementally throughout their lifecycle. Passing object references is the most useful way of dealing with objects, as mutations affect the original rather than just a copy.

For example:

const bigObject = fetchBigObjectFromStorage()
let myMem = bigObject.member || {}

If member exists within bigObject, myMem gets a reference to it. If not, it just receives a reference to a new empty object. So this has unpredictable results:

myMem.value = 30

No errors. myMem.value is now 30. But did the value end up where we actually wanted it?

console.log(bigObject.member.value)

This either displays 30 or gives a TypeError because bigObject.member is undefined, depending on whether or not bigObject.member existed already.

To avoid errors, we could put the new value back into bigObject, and get it put back there every time, we'd need to explicitly assign it thus:

bigObject.member = myMem

which is messy.

So we could pre-empt the possibility that bigObject.member isn't there, test for it, and insert it before we start trying to pass a reference to it around, even initialising required members with null values. So we have this code:

const bigObject = fetchBigObjectFromStorage()
if (!bigObject.hasOwnProperty("member")) bigObject.member = {value: null}
let myMem = bigObject.member
myMem.value = 30
console.log(bigObject.member.value)

... and we'll get no errors, 30 displayed, and reliable behaviour every time (well, almost... a couple more tests in the if might make it watertight). But it's as ugly as sin and too much code for something so trivial. We could refactor bits of it like that if statement into a little function, make it a generic member initialiser, and a few other approaches besides. I'm sure most of us have tried lots of techniques over the years. But what's the most elegant, most favoured solution from the assembled learned throng? Any clever little tricks?

3 Answers3

1

You can shorten that if statement using a conditional operator:

const myMem = bigObject.hasOwnProperty("member") 
  ? bigObject.member
  : (bigObject.member = {value: null});

And if the truthiness check is enough for you, then you can further simplify with a short-circuit logic operator:

const myMem = bigObject.member || (bigObject.member = {value: null});

The probably most elegant way is offered by ES6 destructuring with a default initaliser, which implicitly tests for undefined:

const { member: myMem = (bigObject.member = {value: null}) } = bigObject;
Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • @h1b9b: No, the value would be set in `myMem.value = 30;` in the following statement. The `30` would not be part of the default value (actually, `null` isn't necessary either, an empty `{}` object literal would do just as fine). – Bergi Mar 07 '18 at 20:45
  • The first suggestion, shortening using a conditional operator, is definitely something I've done. The second (short-circuit logic) I'd accept into my code if I was in full control of `bigObject`, and could be sure `bigObject.member` was either an object or absent, otherwise I'd want to introduce another test or two. I really like the third option, ES6 destructuring with default initialiser, but I find it's the least readable and most impenetrable. It'd have me answering endless questions from non ES6 team members. Definitely elegant though. –  Mar 07 '18 at 21:34
  • @jonsilver Yes, "be sure that its either an object or absent" is what I meant by "*if the thruthiness check is enough*". Usually you will know what objects you're working with, so this solution is quite common. – Bergi Mar 07 '18 at 21:35
  • Indeed, we appear to be serially agreeing with each other. And although you may enjoy the luxury of knowing what objects you're working with, it can be more unpredictable when working on old codebases written by multiple programmers each of whom followed their own variable-quality path. –  Mar 08 '18 at 16:58
0
let myMem = bigObject.member = bigObject.member || {};
myMem.value = 30;
console.log(bigObject.member.value);  // outputs 30

I think this could be one of the trick

Saikat Hajra
  • 670
  • 3
  • 12
  • This is relatively concise, but assigning an object to itself... I don't know enough about the internals of the Javascript engine to know how this would be handled internally in memory, but it seems very wasteful. especially with a large object Anyone else care to comment? –  Mar 07 '18 at 22:35
  • 1
    here if you look closely I am assigning the object (if `member` is an object) to itself so it points to the same memory location. It does not copy the content. So no matter whether it's a big object or small object time will be same for all. And if the memory for the `member` object is already allocated it will not be allocated again unless we are doing it with object literal `{}` or assigning some other `object`. – Saikat Hajra Mar 08 '18 at 08:15
  • 1
    Thank you for clarifying. Yes that makes perfect sense, given that assigning the object variable to itself is merely setting a reference to the same reference. –  Mar 08 '18 at 09:49
0

How about

const bigObject = {...defaults, ...fetchBigObjectFromStorage()}

where defaults is something like:

{member: {value: null} etc}

This way, your object always has all expected fields in place.

georg
  • 211,518
  • 52
  • 313
  • 390
  • A good use of the object spread operator. I like this because it maintains readability whilst being very abbreviated. A very elegant technique, similar to how setState is often done in React. It does, however, presume that we know in advance what the members of `bigObject` will be. If they were keyed objects dependent upon subject context, this technique wouldn't be possible. –  Mar 07 '18 at 21:38
  • @jonsilver: if your object is an arbitrary collection of key-value pairs, I'd use `Map` instead which is designed specifically for this kind of thing. – georg Mar 08 '18 at 06:59
  • Whilst I would wholeheartedly agree with you about the suitability of `Map` over `Object` for this use case, I fear that back in the real world this conversion might be an entire project in its own right, because code syntax for the one is not a drop-in replacement for the other. Data stored as `Object` targeted JSON key-value pairs isn't optimised for being loaded into or saved from a `Map` either. But that's good subject matter for a completely different thread - useful, elegant techniques for turning old codebases and data storage into modern, fully optimised JavaScript applications. –  Mar 08 '18 at 07:45
  • @jonsilver: FWIW, object spread is not part of ES6 (ES2015). It will be part of this year's release. Also, [`...` is not an operator!](https://stackoverflow.com/questions/37151966/what-is-spreadelement-in-ecmascript-documentation-is-it-the-same-as-spread-oper/37152508#37152508) – Felix Kling Mar 09 '18 at 02:37
  • Indeed Felix, I believe the availability of object spread in React is only down to Babel supporting it and transpiling it for current ES6 implementations and previous editions of the language. Certainly I've not been able to use it in the majority of my JavaScript work which is non-transpiled and based on NodeJS LTS versions prior to 8.x which have no support for object spread/rest. –  Mar 09 '18 at 04:42