Saturday, May 23, 2015

A Java Developer's Perspective on the Power and Danger of JavaScript's Object Prototype

In the Anti-Patterns section of the book Learning JavaScript Design Patterns, author Addy Osmani calls "Modifying the Object class prototype" a "particularly bad anti-pattern." One of the interesting (and scary) aspects of this is that a developer can change the behavior for all JavaScript objects with one definition. This is analogous to what would be possible in Java if a Java developer was allowed to change Java's Object class.

I mentioned this risky feature briefly in my post JavaScript Objects from a Java Developer Perspective. Imagine the havoc that could be rendered if one was able to change, for example, how Java's Object's default equals(Object) implementation was implemented. In the blog post I just mentioned, I demonstrated overriding a particular Java object's toString() implementation. I mentioned, but did not demonstrate, overriding toString() for all JavaScript objects via Object.prototype. In this post, I do demonstrate this, which is the equivalent of what a Java developer could do in Java if allowed to change Java's Object's toString() implementation directly (Java developers can only extend Object and override it on per-class basis).

It's all too easy to change the default behavior of all JavaScript objects. The next code listing shows how easy it is to change the default JavaScript toString() behavior from providing the string "[object Object]" to providing the string "I'm a JavaScript object!"

Overriding All JavaScript Objects' Default toString() Implementations
Object.prototype.toString = function objectToString()
{
   return "I'm a JavaScript object!";
}

The simple four lines in the above code listing (and I could have easily had them all on a single line) change the default behavior of toString() for all JavaScript objects. I can still override this default implementation of toString on a per named object basis (there are no classes in JavaScript as of today). This was demonstrated in my previous post and is reproduced here for convenience:

Overriding toString() Implementation for Person Object Only
function Person(lastName, firstName)
{
   this.firstName = firstName;
   this.lastName = lastName;
}

Person.prototype.toString = function personToString()
{
   return this.firstName + ' ' + this.lastName;
}

The code listing above shows creation of a JavaScript Person object with a constructor function and shows overriding toString() for that newly created Person object. The next code listing demonstrates testing of the toString() implementations in such a way that the overridden default implementation and the customized Person implementations are rendered. The output of running this demonstration code is shown after the code.

Demonstrating Overridden Default toString() and Customized Person toString()
function demonstrateObjectPrototype()
{
   var indy = new Person('Jones', 'Henry');
   console.log("Indiana Jones's real name is " + indy);
   
   var solo = {};
   solo.lastName = 'Solo';
   solo.firstName = 'Han';
   console.log("Chewbacca's buddy is " + solo);
}

From the output shown above and the code listing before it, we can see that we have changed the default toString() from "[object Object]" to "I'm a JavaScript object!" and that we can still override a particular object's implementation to use its own customized behavior rather than the default behavior.

It is easy to see how this ability to easily manipulate the default behavior for all JavaScript objects can be both alluring and frightening. It wouldn't be a repeated "pattern" (even if it's an anti-pattern) if it didn't have appeal. Java's default Object.toString() implementation that provides the system identity hashcode of the object upon which it's called rarely seems helpful other than for differentiating it from other objects of the same type. It might be tempting at first, if one could easily change Java's Object's toString(), to change this implementation to use recursion to iterate over all of a given object's data members. However, there would also be significant risks and questions:

  • How would one prevent the toString() that used reflection from showing fields' values that should not be shown for security or other reasons?
  • Would class-level (static) data members be shown in addition to instance-level members?
  • Should all objects pay the reflection performance cost, especially when these objects might include collections of other objects that might lead to reflection on deep collections?
  • What would the preferred output format be?

These questions and concerns regarding overriding default Object.toString() behavior in Java are only a subset of the questions and concerns one might have and it could be argued that changing toString()'s default behavior is less risky than changing the default behavior of other Object methods such as equals(Object). One could always override the behavior in Java of changed default Object implementations, but it would need to be overridden in every extended class either directly or through its ancestor classes. Developers new to the code base might assume the JDK default Object behaviors and realize a nasty surprise when they find out that the codebase has changed default Object behaviors.

In this post, I have demonstrated how easy it is to override JavaScript's default Object behaviors via use of Object.prototype and have tried to also show why this should be rarely or never used. I have intentionally approached this from a Java developer's perspective in an effort to articulate more differences in the object models between Java and JavaScript.

2 comments:

Jonathan Raoult said...

Just wanted to clarify something here because it was really confusing to me at the beginning. Object.prototype is not the prototype of objects but a hash of properties that will be added to the prototype of a newly created object when using a constructor.

To access the prototype of the object you have to use the deprecated property __proto__ or better Object.getPrototypeOf().

All this is resumed in this SO answer: http://stackoverflow.com/a/9959753/3024554

@DustinMarx said...

Thanks Jonathan,

That's a nice explanation and differentiation and I appreciate the StackOverflow link.

Dustin