Adventures in Rhino - setters and getters
From Trephine
| « Site improvements - fighting with Disqus | Simple prototypal inheritance » |
[subscribe] Recent blog entries
- Simple prototypal inheritance new!
- Adventures in Rhino - setters and getters
- Site improvements - fighting with Disqus
- JavaScript task chaining
- JavaScript string building benchmarks
- Efficient JavaScript string building
- Alternative JavaScript worker thread API
- Implementing JavaScript worker threads
- Thread safe DOM manipulation
- Site improvements - CSS sprites
- Trephine worker threads made easy
- Pitfalls of multithreaded browser development
- Site improvements - reducing dependencies
- The unsplittability of XML
Live Demos
Adventures in Rhino - setters and getters
Rather than continue to talk about what you can do in JavaScript, and what you could do with trephine if you adopted it for your project, I have decided to switch gears and instead, actually do something myself. I've had an idea for a project that I wanted to do for a long time now, but never had the right infrastructure in place to make it happen, but now, with trephine in a mature and stable state, I think it's time to (finally) move forward.
I'll talk more about what exactly it is that I'm setting out to build as I go through the motions. If all goes well, I should have something up and running by the end of the week - with an update each day explaining the progress I've made. Today, I'd like to talk about some Mozilla specific extensions to the JavaScript language which allow developers to implement message passing properties in objects.
Here's a quick example of an object which does not use the Mozilla extensions:
var bob = (function(){ // define anonymous function wrapper to create scope var name = "Bob"; // scoped variable not accessible outside return { // create a new object name: function(){ // with a name() function return name; // that returns the "private" name value } }; // return the object })(); // call the wrapper immediately
The above code creates a "private" variable called name which contains the string "Bob", then makes a new object with an accessor method called name() that returns this value.
To retrieve bob's name, we would call bob.name(). Alternatively, we could define a getter for the name property so that accessing it is done via bob.name, without the parenthesis:
var bob = (function(){ // define anonymous function wrapper to create scope var name = "Bob"; // scoped variable not accessible outside var obj = {}; // create a new object obj.__defineGetter__( // define a new getter "name", // called "name" function(){ // which refers to a function return name; // that returns the "private" name value } ); return obj; // return the object })(); // call the wrapper immediately
As you can see in this slightly more complicated example, the return object's __defineGetter__ method is used to attach a function to the "name" property. Asking for bob's name is now as simple as bob.name.
Depending on the application, sometimes it's beneficial to create setters as well. Continuing our previous example, we have:
var bob = (function(){ // define anonymous function wrapper to create scope var name = "Bob"; // scoped variable not accessible outside var obj = {}; // create a new object obj.__defineGetter__( // define a new getter "name", // called "name" function(){ // which refers to a function return name; // that returns the "private" name value } ); obj.__defineSetter__( // define a new setter "name", // also called "name" function(n){ // which refers to a function return name = n; // that overwrites the private var with a new value } ); return obj; // return the object })(); // call the wrapper immediately
The effect of the above code is to give bob a code name property which can either be retrieved or set, automatically triggering the getter and setter functions behind the scenes. In our limited case, nothing special happens when bob's name is set or read, but we now have the power to augment that behavior without modifying any other code.
That is, say we wanted to put a restriction on what kinds of names can be assigned to bob, deciding that the consumer of the API shouldn't be able to set the name property to an empty value.
In this case, we'd modify the setter as follows:
obj.__defineSetter__( // define a new setter "name", function(n){ if (!n) return; // that short-circuits for invalid input, otherwise return name = n; // overwrites the private var with a new value } );
At this point we now have a firm grasp on what __defineGetter__ and __defineSetter__ accomplish. However, the syntax for them may seem a little daunting, so Mozilla's developers added some syntax sugar to make the process a little more pithy.
The following snippet creates the same bob object as before, but using the "get" and "set" keywords instead:
var bob = (function(){ // define anonymous function wrapper to create scope var name = "Bob"; // scoped variable not accessible outside return { // create a new object get name(){ // define a new "name" getter function return name; // that returns the "private" name value }, set name(n){ // define a new "name" setter function if (!n) return; // that short-circuits for invalid input, otherwise return name = n; // overwrites the private var with a new value } }; // return the object })(); // call the wrapper immediately
And thus concludes our introduction to Mozilla getters and setters.
The problem with the above is backwards compatibility. Any time you decide to leverage a particular brand new JavaScript feature like this, your code becomes unrunnable on older systems. On the web, this means different browsers, or even older versions of the same browser. In my case, this means the default Rhino in Java 1.6.
Java 1.6 shipped with a pre-packaged Rhino implementation. This was fantastic news for dynamic languages since it demonstrated that the JVM and the Java language are continuously being thought of as different things. Unfortunately, the version of the JavaScript standard shipped with Java 1.6 is effectively static. That is, although newer versions of Java 1.6 will contain security and performance enhancements, it's unlikely that a new version will upgrade the contained Rhino version significantly.
For my most recent project, I had wanted to use env.js with trephine since that library already contains an implementation of XMLHttpRequest and the DOM (among other useful things). The maintainers of env.js chose to implement a sizable chunk of their getters and setters in the manner described above. And therein lies the problem since the Rhino which comes with Java 1.6 is sufficiently old as to not understand these directives (throws parsing errors).
One option would be to load the latest JavaScript implementation into the JVM at runtime (in much the same way the ruby demo does). This would be a coding hassle since now an otherwise pure-JS program would need to rely on external Java code to function. The better solution would be to alter env.js to meet my needs (or so I thought).
I had just started to hack away at env.js when I realized that Java 1.6/Rhino doesn't even support __defineGetter__ and __defineSetter__. As I understand it, that means that there is simply no way to turn a given function into a getter/setter. Sure you can have methods, but an actual message passing property? I'm pretty sure it can't be done given the constraints.
So now the question is, do I create a radically different env.js that doesn't rely on setters and getters? or should I focus on creating just a minimal library with the features I most immediately need? Prior to learning of Rhino's limitation, I had been leaning towards the former, but now I'm pretty certain that just rolling my own library will be the way to go.
Anyway, I'm rambling. The bottom line is that I need some way to download web pages and strip out certain elements (such as links, heading text, etc) from the trephine/Rhino context. I think the best way to get there is to just roll one myself. What do you think?
--Jim R. Wilson (jimbojw) 05:36, 12 May 2009 (UTC)