3

In JavaScript I want to do the following:

var pi = {}; pi[0]['*']['*'] = 1;

of course this throws a "Cannot read property '*' of undefined" error. Clearly I can define p[0] = {}, but that's kind of a pain as I will be sticking lots of different values in the different attributes, e.g.

pi[2]['O']['I-GENE'] = 1;

etc. The first key into the hash is just an integer, so I guess I could use an array at the top level instead of a hash, and then default initialize like in this post:

default array values

but that doesn't handle my need for default initialization of the other hashes.

It does seem like what I am trying to do is running up against the ECMAScript spec which indicates that undefined attributes of an object (which is a hash in JavaScript) should return undefined, as mentioned here:

Set default value of javascript object attributes

which interestingly includes a dangerous work around.

In other places where I'm trying use nested hashes like this I am finding myself writing long bits of ugly code like this:

function incrementThreeGramCount(three_grams,category_minus_two,category_minus_one,category){
    if(three_grams[category_minus_two] === undefined){
      three_grams[category_minus_two] = {};
    }
    if(three_grams[category_minus_two][category_minus_one] === undefined){
      three_grams[category_minus_two][category_minus_one] = {};
    }
    if(three_grams[category_minus_two][category_minus_one][category] === undefined){
      three_grams[category_minus_two][category_minus_one][category] = 0;
    }
    three_grams[category_minus_two][category_minus_one][category]++;
}

which I'd really like to avoid here, or at least find some good way of adding to the functionality of the Hash through the prototype method. However it seems like since the Hash and Object in JavaScript are just the same thing, we can't really play around with default hash behaviour in JavaScript without impacting a lot of other things ...

Maybe I should be writing my own Hash class ... or using Prototypes:

http://prototypejs.org/doc/latest/language/Hash/

or someone elses:

http://www.daveperrett.com/articles/2007/07/25/javascript-hash-class/

or mootools:

http://mootools.net/docs/more/Types/Hash

argh, so many choices - wish I knew the best practice here ...

Community
  • 1
  • 1
Sam Joseph
  • 4,584
  • 4
  • 31
  • 47
  • Heh - this is reminding me of a controversial PR on mootools last year: https://github.com/mootools/mootools-core/pull/2191 - Object.get/set methods (not .prototype) which did that automatically. Did not land and Daniel stopped contributing since :D - his implementation was quite reasonable - https://github.com/csuwldcat/mootools-core/blob/master/Source/Types/Object.js#L23-L43 - can work easily w/o mootools – Dimitar Christoff Apr 11 '13 at 18:36

4 Answers4

2

This can be done using ES6 proxies. You'd define a proxy on an object with a get handler. When a get is performed on a key with an undefined value or for which the object doesn't have an own property, you set it to a new proxy that uses the same get handler and return that new proxy.

Additionally, this would work without the need for the bracket syntax:

var obj = ...;
obj.a.b.c = 3;

Unfortunately, being an ES6 feature, their support is limited to Firefox and they can be enabled in Chrome with an experimental flag.

Justin Summerlin
  • 4,938
  • 1
  • 16
  • 10
  • 1
    unfortunately proxy objects are currently only supported by firefox. Maybe you chould put this in as a note – Moritz Roessler Apr 11 '13 at 09:38
  • They're supported in Chrome stable with the `Enable Experimental JavaScript` flag enabled as they've been in V8 for a very long time. I'm sure when the spec is finalized they'll be enabled in the next stable push. – Justin Summerlin Apr 11 '13 at 17:46
  • Nice, Thx I didn't knew chrome supports proxies as experimental +1 – Moritz Roessler Apr 12 '13 at 05:55
1

I would do something like that:

Object.prototype.get = function(v){ 
    if(!this[v]){
        this[v] = {} 
    } 

    return this[v];  
}

and then, instead object["x"]["y"].z = 666 use object.get("x").get("y").z = 666.

IProblemFactory
  • 9,551
  • 8
  • 50
  • 66
  • this looks cool, but doesn't it have dangerous side effects on other libraries that might be relying on the default get behaviour that returns 'undefined'? What I'd love is this mod, but restricted to the local code - hence a Hash class seems safer, no? – Sam Joseph Apr 11 '13 at 12:05
  • well, there is a chance some lib also defines `get` method, so try use more unique name like `get_or_initialize`... or consider less verbose name like `_` :) – IProblemFactory Apr 11 '13 at 12:10
  • ah got you - sorry I missed that you were recommending changing the syntax to the .get("x") - yeah, wish I could stick with ['x'] so much less verbose - I guess ._('x') is not bad too. And any custom Hash Class I made would have to change syntax too - I guess I can't easily override the [] operator like Object.prototype.[] = function(v){ ... – Sam Joseph Apr 11 '13 at 12:14
0

You could write a simple helper to do this. e.g

function setWDef() {
    var args = [].slice.call(arguments);
    var obj = args.shift();
    var _obj = obj;
    var val = args.pop();
    if (typeof obj !== "object") throw new TypeError("Expected first argument to be of type object");
    for (var i = 0, j = args.length - 1; i < j; i++) {
        var curr = args[i];
        if (!_obj[curr]) _obj[curr] = {};
        _obj = _obj[curr];
    }
    _obj[args.pop()] = val;
    return obj;
}   

Now just set the value using the function

var pi ={}
console.log (setWDef(pi,"0","*","a*",1)) //{"0": {"*": {"a*": 1}}}

Heres a Demo on JSBin

Moritz Roessler
  • 8,542
  • 26
  • 51
0

With the drafted logical OR assignment operator ||= you can do the following:

 const pi = [];
 ((((pi[0]||={})['*'])||={})['*'] = 1);
 console.log(JSON.stringify({pi}));

Output:

{"pi":[{"*":{"*":1}}]}

You can also initialize some levels as arrays or define default property values:

const pi = [];
((((((pi[0]||={})['*'])||=[])[3])||={'#':2})['*'] = 1);
console.log(JSON.stringify({pi}));

Output:

{"pi":[{"*":[null,null,null,{"#":2,"*":1}]}]}
loop
  • 825
  • 6
  • 15