Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
I am the chief technical officer at InVision App, Inc - a prototyping and collaboration platform for designers, built by designers. I also rock out in JavaScript and ColdFusion 24x7.
Meanwhile on Twitter
Loading latest tweet...
Ben Nadel at CFinNC 2009 (Raleigh, North Carolina) with: Mike Brunt

Experimenting With The Bracket Operator On JavaScript Objects

By Ben Nadel on

When it comes to using brackets in JavaScript, I typically don't give them much thought at all. They're great for array indices and for setting and getting object properties. But beyond that, their use hasn't garnered any real contemplation on my part. A few weeks ago, however, I was reading JavaScript: The Good Parts by Douglas Crockford; and, in his book, he mentioned that when using the bracket operator on objects, JavaScript will implicitly call the toString() method on the given key in order to determine the look-up value.


 
 
 

 
 
 
 
 

When I read this, I started to wonder if thoughtfully-crafted toString() override methods could provide a more intuitive way to associate data with objects. Typically, when it comes to objects, it's definitely a best practice to encapsulate data; however, from time to time, we may find ourselves in a situation in which we want to associate additional, external data with an object. In such a case, it might be kind of cool to use the object itself as the associative index value.

To see what I'm talking about, take a look at the following demo. In this code, we create object instances that have an explicit toString() method. This toString() method is then implicitly used to associate "personality" data with an existing object instance.

  • <!DOCTYPE html>
  • <html>
  • <head>
  • <title>Experimenting With The Bracket Operator</title>
  • <script type="text/javascript">
  •  
  •  
  • // I am a constructor function.
  • function Girl( id, name ){
  •  
  • // Store the properties.
  • this.id = id;
  • this.name = name;
  •  
  • // Return this object reference.
  • return( this );
  •  
  • }
  •  
  • // I provide a representiuon of this object as a string.
  • // This can be used to uniquely identify this object by
  • // a serialized value.
  • Girl.prototype.toString = function(){
  •  
  • // Because the ID property is unique, each instance
  • // of this object will be represented as a unique
  • // string value.
  • return( this.id + ":" + this.name );
  •  
  • };
  •  
  •  
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  •  
  •  
  • // Create a few instances of girls.
  • var sarah = new Girl( 1, "Sarah" );
  • var tricia = new Girl( 2, "Tricia" );
  • var anna = new Girl( 3, "Annd" );
  •  
  •  
  • // Create an object to use an associative array between the
  • // girl instances and some other personality rating.
  • var personality = {};
  •  
  • // Now, index each personality trait using the GIRL instance
  • // as the key. In JavaScript, the object parenthesis operator
  • // creates the look-up key by calling .toString() on the
  • // given value.
  • personality[ sarah ] = "Spontaneous";
  • personality[ tricia ] = "Tenacious";
  • personality[ anna ] = "Adventurous";
  •  
  •  
  • // Now, let's log the values to see if we can retreive them.
  • console.log( "Sarah:", personality[ sarah ] );
  • console.log( "Tricia:", personality[ tricia ] );
  • console.log( "Anna:", personality[ anna ] );
  •  
  •  
  • </script>
  • </head>
  • <body>
  • <!-- Left intentionally blank. -->
  • </body>
  • </html>

Notice that when we store associative data in our personality collection, our key value is not a string - it's a Girl instance. JavaScript takes this instance and implicitly calls the toString() class method in order to calculate the lookup key. This is done both for setting and getting the personality data; and, when we run the above code, we get the following console output:

Sarah: Spontaneous
Tricia: Tenacious
Anna: Adventurous

As you can see, the Girl instances were properly used to both store and then locate the associated personality data. By not having to manufacture some arbitrary look-up key, it keeps the code much more "readable" and therefore much more intuitive.

Ignoring the silliness of this particular example, the biggest problem with this approach is that you are now depending on some implicit object "contract" in order to power critical business information. If we changed this to something that had a more explicit kind of method, you might feel more comfortable:

  • personalities.getByGirlID( sarah.id )

This is inherently more soothing because we can look at it and feel confident that we don't have to worry about some fluid meaning of the "id" property; it - the ID property - is not going to change because ID values don't change on established objects. Nuff said.

So, how can we make our implicit approach more explicit while maintaining the readability? How about requiring the given class instances to inherit from a base class that is designed specifically to be used in this way? If our Girl objects inherited from the class, Hashable (something we're about to make up), then our use of the instances as implicit keys would become an explicit contract - as explicit as using the ID property as the associating link.

  • <!DOCTYPE html>
  • <html>
  • <head>
  • <title>Experimenting With The Bracket Operator</title>
  • <script type="text/javascript">
  •  
  •  
  • // I am the constructor for objects that are intented to be
  • // duly used as index-keys.
  • function Hashable(){
  •  
  • // Store the new hashable ID.
  • this.hashableID = ++Hashable.uuid;
  •  
  • // Return this object reference.
  • return( this );
  •  
  • };
  •  
  • // This UUID allows this object to be a obtain a unique ID
  • // amongst all Hashable objects.
  • Hashable.uuid = 0;
  •  
  • // I provide a toString() method that is meant to be used to
  • // generate the unique object look-up key.
  • Hashable.prototype.toString = function(){
  •  
  • // Return the unique hashable ID.
  • return( this.hashableID );
  •  
  • };
  •  
  •  
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  •  
  •  
  • // I am a constructor function.
  • function Girl( id, name ){
  •  
  • // Initalize super-class.
  • Hashable.call( this );
  •  
  • // Store the properties.
  • this.id = id;
  • this.name = name;
  •  
  • // Return this object reference.
  • return( this );
  •  
  • }
  •  
  • // Inherit from Hashable - this will provide our instances
  • // with an *explicit* contract to be used as implicit object
  • // look-up keys.
  • Girl.prototype = Object.create( Hashable.prototype );
  •  
  •  
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  •  
  •  
  • // Create a few instances of girls.
  • var sarah = new Girl( 1, "Sarah" );
  • var tricia = new Girl( 2, "Tricia" );
  • var anna = new Girl( 3, "Annd" );
  •  
  •  
  • // Create an object to use an associative array between the
  • // girl instances and some other personality rating.
  • var personality = {};
  •  
  • // Now, index each personality trait using the GIRL instance
  • // as the key. In JavaScript, the object parenthesis operator
  • // creates the look-up key by calling .toString() on the
  • // given value.
  • personality[ sarah ] = "Spontaneous";
  • personality[ tricia ] = "Tenacious";
  • personality[ anna ] = "Adventurous";
  •  
  •  
  • // Now, let's log the values to see if we can retreive them.
  • console.log( "Sarah:", personality[ sarah ] );
  • console.log( "Tricia:", personality[ tricia ] );
  • console.log( "Anna:", personality[ anna ] );
  •  
  •  
  • </script>
  • </head>
  • <body>
  • <!-- Left intentionally blank. -->
  • </body>
  • </html>

As you can see here, rather than providing the toString() method in the Girl class, the Girl class is inheriting it from the Hashable class which is also responsible for providing a unique hashableID for each instance. At this point, the use of Hashable's toString() method is just as explicit as the use of the Girl's ID property. Both define a contract for appropriate use; and both will cause problems if their underlying contract is violated.

Oh, and when we run the above code, we get the following output:

Sarah: Spontaneous
Tricia: Tenacious
Anna: Adventurous

This was just a fun exploration of the bracket operator in JavaScript. Since I only just learned that this was even possible, I'm not even going to pretend to tell you if this is a good idea or a bad idea. That said, I think there is something very pleasing and intuitive about passing in an object as a look-up key; it certainly emphasizes intent over implementation.



Reader Comments

@Ben, this use of toString() reminds me of something:

You know how some sites encrypt their JavaScripts? Usually a bootstrap script decrypts the other scripts and then destroys itself so you can see how they were encrypted in the View Source. People don't do that much anymore, because all you have to do is go to the site in a browser that supports functionname.toString(), right?

The thought occurred to me a while back that those sites could still define their own functionname.toString() as a way to block that technique. But given what you discovered earlier about the delete command deleting only from the current instance (not higher up in the prototype chain), they're still screwed. If they leave a script anywhere in the DOM, you can always delete functionname.toString, and fall back to native methods of dumping script contents.

Returning to the topic of this article, bracket references to property names, I like that you foresaw the need for a UUId. Often we use non-necessarily-unique identifiers on the server, forgetting that there may be typos on the browser. If a personnel page used an object's Tax ID as its toString() property name in another object (for example), a typo on one employee's form element could wipe out another employee's data. Good forethought.

Reply to this Comment

@WebManWalking,

I think I read somewhere that we actually have started to repeat Social Security Numbers (SSN) as well. Talk about something you *might* have thought would be unique... then it turns out your neighbor is using someone else's SSN and has no record older than 2 years ago :)

As for the encrypting scripts stuff, I think I vaguely remember something like that. Years ago, I think I remember seeing a script that was compiled or something and was only runnable based on some sort of runtime shenanigans.

When it comes to the Hashable stuff, I figured putting in "Interface" to the object would put people at ease. It's one thing to just assume that an object is naturally represented as a unique string (probably a bad assumption since I think most objects will present as [object]). But, if you say, "Hey, my object promises to do XYZ do to the object chain," well then, that's gotta be acceptable.

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.