2

I've been checking out the Google Closure Compiler recently. I downloaded the .jar file and gave it a test drive. So far, I must say that I've been very impressed. I can certainly see its usefulness beyond minimization. Props to the Google team!

I do have one small gripe though. It seems to me that you only get two options as far as optimization goes. It's either SIMPLE_OPTIMIZATIONS or ADVANCED_OPTIMIZATIONS. The former, although adequate, is very simple IMHO. For one thing, unless I'm missing something, it leaves all property names untouched. It also does not remove unreachable code. On the other hand, the latter option is simply too destructive.

Now, I'm fairly new to JavaScript, so it's very probable that I'm missing something. If I say something stupid, feel free to school me. That said, I can understand the issues with renaming in JavaScript. The Google team recommends using the bracket notation (object['property']) instead of the dot notation (object.property) to access the properties that you do not want changed and never mixing the two uses. They also suggest 'exporting' methods by using the following pattern:

MyClass = function(name) {
  this.myName = name;
};

MyClass.prototype.myMethod = function() {
  alert(this.myName);
};

window['MyClass'] = MyClass; // <-- Constructor
MyClass.prototype['myMethod'] = MyClass.prototype.myMethod;

However, there are legitimate cases that you want to mix the two notations. Let's say we are building a game. The game's code is completely isolated inside a closure. It does not 'export' anything to the global scope, nor does it need to. In fact, it really should not touch the window object. However, it does need to read some in-game properties from XML configuration files.

Sample JavaScript:

var TheGreatAdventure = (function(window) {

    function Fighter() {
        // Private to application
        this.id        = 42;
        // Accessible to XML configuration system
        this.name      = 'Generic Jen';
        this.hitPoints = 100;
        this.onAttack  = genericFighterAttack;
        this.onSpeak   = genericFighterSpeak;
        ...
    }
    Fighter.publishedProperties = ['name', 'hitPoints', 'onAttack', 'onSpeak']

    function genericFighterAttack() {...}
    function genericFighterSpeak() {...}

    function cassieAttack() {...}
    function cassieSpeak() {...}

    ...

    EntityReader = {
        ...
        function readFromXMLNode(attributes, entityClass, entityInstance) {
            for (var i = 0; i < attributes.length; i++) {
                var attribute = attributes[i];
                if (attribute.nodeName in entityClass.publishedProperties)
                    entityInstance[attribute.nodeName] = bindContext[attribute.value];
            }
        }
        ...
    }

}(window));

Sample XML configuration file:

<Fighter name='Custom Cassie' onAttack='cassieAttack' onSpeak='cassieSpeak'/>

Not only would the above system fail to assign the properties, the functions cassieAttack and cassieSpeak would have been eliminated during minimization as dead code!

Now, there's no way I'm accessing all of the 'published' properties using the bracket notation throughout the game's code. Even if there's no run-time penalty in doing so (there should not be any), there's still a lot of extra typing involved and it's (IMO) an eyesore. With such common properties, everything would show up as a string inside a text editor, defeating the purpose of syntax highlighting!

It seems to me that a simple @preserve (or something similar) directive over those properties would allow ADVANCED_OPTIMIZATIONS to be used with minimum cost in final program size. Am I missing something?

user1127813
  • 299
  • 3
  • 8
  • I have an answer to a similar question: http://stackoverflow.com/questions/7823811/prevent-google-closure-compiler-from-renaming-settings-objects/7834912#7834912 – Stephen Chung May 04 '12 at 05:33

2 Answers2

3

This answer was completely rewritten, turns out there's a way to do what user1127813 wants.

You need to provide a property mapping file that maps some names to themselves, using the --property_map_input_file flag. Suppose you have the following original code in test.js:

/** @constructor */
function Fighter() {
    this.ID        = 42;
    this.fullName  = 'Generic Jen';
    this.hitPoints = 100;
}
Fighter.publishedProperties = ['fullName', 'hitPoints'];

var jen = new Fighter();
var bob = new Fighter();

bob.ID = 54;
bob.fullName = 'Bob the Destructor';
bob.hitPoints = 1337;

for(i = 0; i < Fighter.publishedProperties.length; i++) {
    prop = Fighter.publishedProperties[i];
    alert(prop + ' = ' + bob[prop]);
}

Compile it like so:

java -jar closure-compiler.jar --compilation_level ADVANCED_OPTIMIZATIONS --js test.js --property_map_output_file testprop.txt --js_output_file test2.js

You will get a new file test2.js (with contents that don't work) and another file testprop.txt that contains:

ID:a
hitPoints:c
fullName:b

Change testprop.txt so it looks like this:

ID:ID
hitPoints:hitPoints
fullName:fullName

Then recompile with testprop.txt as input instead of output:

java -jar closure-compiler.jar --compilation_level ADVANCED_OPTIMIZATIONS --js test.js --property_map_input_file testprop.txt --js_output_file test2.js

Observe the contents of test2.js:

var a=["fullName","hitPoints"],b=new function(){};b.ID=54;b.fullName="Bob the Destructor";b.hitPoints=1337;for(i=0;i<a.length;i++)prop=a[i],alert(prop+" = "+b[prop]);

The desired properties are now accessed with their original name using the dot notation and the program will correctly show popups with the published properties of bob.

jjrv
  • 4,211
  • 2
  • 40
  • 54
  • First of all, thank you. However, don't you think that simply instructing the renaming module of the compiler to leave alone a small list of identifiers would be preferable to having to use bracket notation throughout your entire program (all 40K lines of it)? In addition, what happens when/if you decide to use another minimizer? You're stuck with the strings inside the brackets. That's a case of changing something you shouldn't (the program's code) in order to induce a side-effect (i.e. have the compiler leave certain variables alone). – user1127813 May 03 '12 at 15:09
  • As for using the property map, I'm afraid it's not doable, since in the example above the XML files will be provided by the user of the application (I think I did not make that clear). – user1127813 May 03 '12 at 15:10
  • You're right about the functions. They should of course be methods of an object (represented by the bindContext variable in readFromXMLNode()). – user1127813 May 03 '12 at 15:22
  • 1
    I agree that it would be preferable to have syntax for avoiding the renaming, but this is not the right forum to have your wish fulfilled. You can send suggestions to the Google Closure team at https://www.google.com/moderator/?#16/e=49a66 and vote for your particular suggestion already submitted at http://goo.gl/mod/jJPx – jjrv May 03 '12 at 16:54
  • I just realized you can avoid the renaming! Updating answer in just a moment. – jjrv May 03 '12 at 17:14
  • Thank you! I just tested it myself and it works. The key to your answer is of course that you can use a property map file as *input* to the compiler. I did not know that. I really do appreciate the time you devoted to revising your answer (and I'm sure others will!) and I upvoted your suggestion. However, it is not very practical for real-world usage, since every time you make a change to the code and re-deploy, you would have to change the property mappings by hand all over again. I guess making a small program to automate the whole process would be necessary. A bit awkward, but it will work. – user1127813 May 04 '12 at 07:39
  • 1
    While the compiler tries to honor names in a property input map, it is not required to do so. Externs, quoted properties and goog.reflect are more appropriate solutions. – Chad Killingsworth May 04 '12 at 11:40
3

The compiler has support for this, but it is awkward, and depends on a "primitive" from the Closure Library called "goog.reflect.object":

/** @nocollapse */
Fighter.publishedProperties = goog.object.transpose(goog.reflect.object(
    Fighter, {fullName:1, hitPoints:2}));

This avoid the use of quoted properties. If this is the only thing that you use from the Closure Library everything but the "goog.object.transpose" function will be compiled out (goog.object.transpose isn't special so you are free to use an alternate implementation). This is type safe and can be used with the compiler's type based optimizations (see a discription of that here: http://code.google.com/p/closure-compiler/wiki/ExperimentalTypeBasedPropertyRenaming ).

The key is that goog.reflect.object must be used directly, taking the constructor and an object literal with the keys of the properties you need to preserve.

The other thing that you will want to be aware of this that in ADVANCED mode, if Fighter.publishedProperties is defined in global scope, it will be removed from the constructor due to namespace collapsing. @nocollapse prevents this. An alternative would be to use a helper method to add the property:

function addPublishedProperties(obj, value) {
  obj.publishedProperties = goog.object.transpose(value);
}

Ok, I've covered a lot of ground here so be sure to let me know if you would like clarification.

Chad Killingsworth
  • 14,360
  • 2
  • 34
  • 57
John
  • 5,443
  • 15
  • 21