How To Create GStrings In Javascript By Extending Core Data Types
Posted February 8, 2010 at 3:17 PM
This morning, I was looking at a tutorial on how to extend built-in Javacript objects by altering the Prototype of native Javacript classes. In doing this, you can add your own methods to Javascript String and Array instances (for example). The down side to this approach is that this affects every new and existing instance of the altered data types (all instances of a given type share a common prototype). This got me thinking about Javascript object extension in general, and I wondered, rather than altering the core classes, could we create new classes that simply extend the core classes.
| | | | | |
| | | |||
| | | |
In Groovy, there is something called a GString.
| | | | | |
| | ![]() | | ||
| | | |
... no, not like that (get your mind out of the gutter)! A GString is a Groovy Object that acts like a String but has delayed evaluation of its content variables. Basically, you can create a string that is composed of variable place holders. Then, when you evaluate the string, those place holders get substituted for their string literal values. So, for instance, you can create the Groovy GString:
"Hey there, $name"
... where "$name" is a variable whose value is not substituted into the string until it is absolutely necessary.
So my thought was, can we extend the native Javascript String class to create our own Javascript GString-like implementation? After a little bit of trial and error, this is what I came up with:
Launch code in new window » Download code as text file »
- <!DOCTYPE HTML>
- <html>
- <head>
- <title>Creating A GString In Javascript</title>
- <script type="text/javascript" src="jquery-1.4.1.js"></script>
- <script type="text/javascript">
-
- // I am the GString object - I allow delayed evaluation of
- // a string based on a colleciton of name-value bindings.
- // Ala Groovy.
- function GString( value, bindings ){
- // Pass the arguments off to the parent constructor
- // in case there is any magic that the String
- // constructor is going.
- String.apply( this, arguments );
-
- // Store the value of our string.
- this.value = value;
-
- // Set up the bindings container (this is where we
- // will map variables names to value for our GString).
- this.bindings = {};
-
- // Add any initial bindings.
- this.addBindings( bindings );
- }
-
-
- // When creating our GString, we want to extend the
- // built-in String object. NOTE: When doing this, we
- // have to override the toString() and valueOf()
- // methods.
- GString.prototype = jQuery.extend(
-
- // Extend a STRING object instance.
- new String(),
-
- // Define class functions.
- {
-
- // I add the given binding to the colleciton.
- addBinding: function( name, value ){
- this.bindings[ name ] = value;
-
- // Return this object for method chaining.
- return( this );
- },
-
-
- // I add the given collection of bindings to the
- // internal collection.
- addBindings: function( collection ){
- // Loop over the given collection.
- for (var name in collection){
-
- // Add each key to the current bindings.
- this.addBinding(
- name,
- collection[ name ]
- );
-
- }
-
- // Return this object for method chaining.
- return( this );
- },
-
-
- // I apply the bindings to the current string
- // and its placehoders.
- applyBindings: function(){
- var self = this;
-
- // Replace the inline bindings with the ones
- // we have stored in the bindings collection.
- var result = this.value.replace(
- new RegExp( "\\$\\{([\\w_.-]+)\\}", "g" ),
- function( $0, $1 ){
-
- // Check to see if the binding name
- // exists in our binding collection.
- if ($1 in self.bindings){
-
- // Return the binding value.
- return( self.bindings[ $1 ] );
-
- } else {
-
- // The binding was not found, so
- // just return the original match.
- return( $0 );
-
- }
- }
- );
-
- // Return the evaluted string.
- return( result );
- },
-
-
- // I reset the binding collection.
- clearBindings: function(){
- this.bindings = {};
-
- // Return this object for method chaining.
- return( this );
- },
-
-
- // I return the value of the given binding.
- getBinding: function( name ){
- return( this.bindings[ name ] );
- },
-
-
- // I return the raw value (before evaluation).
- getValue: function(){
- return( this.value );
- },
-
-
- // I check to see if the given binding exists.
- hasBinding: function( name ){
- return( name in this.bindings );
- },
-
-
- // I update the stored value.
- setValue: function( value ){
- this.value = value ;
-
- // Return this object for method chaining.
- return( this );
- },
-
-
- // I convert the object to a string representation.
- // This needs to evaluate the string because it
- // gets used in things like the replace() method.
- toString: function(){
- return( this.valueOf() );
- },
-
-
- // I get the value of the string (in its fully
- // evaluated format with integrated bindings).
- valueOf: function(){
- return( this.applyBindings() );
- }
-
- }
- );
-
-
- // -------------------------------------------------- //
- // -------------------------------------------------- //
-
-
- // Create a new instance of the gstring string.
- var message = new GString(
- "Hello ${name}, you are looking ${adj} today!"
- );
-
- // Add the bindings for the GSTring.
- message.addBindings({
- name: "Tricia",
- adj: "wicked hot"
- });
-
-
- // -------------------------------------------------- //
- // -------------------------------------------------- //
-
-
- // Log the message as it's own object.
- console.log( message );
-
- console.log( "..." );
-
- // Log the message in a way that requires it to be
- // converted into a string (we are going to concatentate
- // it with another string).
- console.log( "Message: " + message );
-
- // Use core string methods.
- console.log( message.toUpperCase() );
-
- </script>
- </head>
- <body>
-
- <h1>
- Creating A GString In Javascript
- </h1>
-
- </body>
- </html>
Here, we are creating our GString class. When defining the class, we use an instance of the native String class as our GString prototype. In this way, we can extend the String class without permanently altering it. Using jQuery's extend() method, we then add additional functions to the prototype to override some of the core String functions. In particular, we need to override the valueOf() and the toString() methods to get our GString object to look and act like an actual string.
In our valueOf() method override, notice that before we return the string value, we are applying the cached bindings. This will take whatever bindings have been defined and try to merge them into the string. In this way, we can put off evaluating the string until it is absolutely necessary.
The only caveat here with creating new classes rather than altering the existing String class is that you have to explicitly create new instances; you cannot make use the implicit string constructor. But, since we are dealing with specialized objects, I don't see this as being an issue at all. And, when we run the above code, we get the following console output:
Message: Hello Tricia, you are looking wicked hot today!
HELLO TRICIA, YOU ARE LOOKING WICKED HOT TODAY!
Notice that in the first output, "Tricia" and "wicked hot" were successfully substituted into the string when it was being concatenated. Also notice that the GString class instance was able to use the core string method, toUpperCase(). Works like a charm!
Before this, I had never consciously tried to use a core object as a Class prototype (Object itself is a core class). It's awesome that this worked, although there's no real reason is shouldn't have. I like the idea of being able to create our own classes, that can act like core Javascript classes, without having to alter the core class definitions. Something like this could create a very nice API for HTML templates that need to be populated with Javascript and AJAX.
Download Code Snippet ZIP File
Post Comment | Ask Ben | Other Searches | Print Page
Newer Post
Making Sure Scheduled Tasks Don't Overlap In ColdFusion
Older Post
What Happens When A ColdFusion CFLock Timeout Is Exceeded Without Error?
Reader Comments
This is some impressive solution. I've to try it myself to understand the concept better and say more about it, but nice job!
PS: Wicked pictures!
@Robert,
Thanks my man. This is the first time I have ever tried to extend a core Javascript object. Technically, I have extended the "Object" class (pretty much how all prototypal inheritance works to some degree); but, this is the first time I've ever tried to extend something else.
Sick stuff Ben!
Ironically there is an interesting project called fusebox that takes on the idea of "safely" extending JavaScript.
http://github.com/jdalton/fusebox
@Garrett,
Very interesting. I'll have to give that a look. I don't think I have seen that before.
You had me at "How To Create GStrings..."
@ben, it's pretty refreshing to see people posting things like this. I think it's important to understand the consequences of modifying default JS objects and unfortunately, most people don't.
I explored this concept a bit in a blog post recently and spoke about it too. I wish I had this as an example back then!
http://www.lovemikeg.com/blog/2010/01/21/responsible-javascript-prototype-modification/
Very slick Ben.




