Take the 2-minute tour ×
Programmers Stack Exchange is a question and answer site for professional programmers interested in conceptual questions about software development. It's 100% free, no registration required.

I noticed some redundancy in a script I ran through Google Closure Compiler.

(function(){function g(a){var k;if(a){if(a.call)a.prototype=j,a.prototype[e]={}}else a=
{};var c=a,b,c=(a=c.call?c:null)?new a(a):c;b=c[e]||{};var f=b.extend;b=b.a;var 
d=c.hasOwnProperty("constructor")?c.constructor:b?b:f?g.b(f):new Function;if
(f)b=d.prototype,k=new h(h.prototype=f.prototype),f=k,h.prototype={},d.prototype=f,i
(d.prototype,b);i(d,a);i(d.prototype,c);return d.prototype.constructor=d}function i(a,c)
{for(var b in c)c.hasOwnProperty(b)&&"prototype"!=b&&b!=e&&(a[b]=c[b])}var h=new Function,
e="decl-data",j={extend:function(a){return(this[e].extend=a).prototype},a:function(a)
{return(this[e].a=a).prototype}};g.c=function(a){e=a};return g})().b=function(g){return 
function(){g.apply(this,arguments)}};

It looks like a big sea of prototype and constructor, doesn't it?

I was was able to get it to about 6/7 of the size of the original by storing "constructor" and "prototype" strings and using array notation everywhere to access prototypes and constructors. The size savings should grow as the size of the script grows. Here's what it looks like after the change:

(function(b,h){function i(a){a?a.call&&(a[b]=l,a[b][f]={}):a={};var d=a,c,d=(a=d.call?
d:null)?new a(a):d;c=d[f]||{};var g=c.extend;c=c.a;var e=d.hasOwnProperty(h)?d[h]:c?c:g?i.b
(g):new Function;if(g){c=e[b];var m=b,g=new j(j[b]=g[b]);j[b]={};e[m]=g;k(e[b],c)}k(e,a);k(e
[b],d);return e[b][h]=e}function k(a,d){for(var c in d)d.hasOwnProperty(c)&&c!=b&&c!=f&&(a
[c]=d[c])}var j=new Function,f="decl-data",l={extend:function(a){return(this[f].extend=a)
[b]},a:function(a){return(this[f].a=a)[b]}};i.c=function(a){f=a};return i})
("prototype","constructor").b=function(b){return function(){b.apply(this,arguments)}};

Is it worth it to damage readability to save a few bytes? It's hard to know when to stop... hasOwnProperty only appears twice, I might use it once more. So, should I replace .hasOwnProperty with [_has]? Should I just stop here? Or should I put it back the way it was, and not worry about the few extra bytes?

On the other hand, maybe this doesn't damage readability at all, and could just be looked at as convenient shortcuts.

Another alternative would be abstracting the behavior using macros; write .prototype and have it converted to [_p] at build time, of course declaring _p somewhere. But that seems overly complicated for a relatively minor size optimization. Speaking of which, I wonder why closure-compiler doesn't do something like this already?

I'm curious what the community thinks about this.


For reference, the un-minified source before the size optimization:

var decl = (function(){

var Clone = new Function(), // dummy function for prototypal cloning
    /** dataKey

        The name of the property where declaration objects' 
        metadata will be stored. If you want to pass objects to decl 
        instead of functions, put the metadata (parent, partial, etc.) 
        in this property.
    */
    dataKey = 'decl-data',

    /** proto

        This object is used as a prototype for declaration objects,
        so all properties are available as properties of `this`
        inside the body of each declaration function.

    */
    proto = {

      /** extend

          Perform prototypal inheritance by calling `this.extend(ParentCtor)`
          within your decalration function.

          @param {Function} ctor to extend.

          @return {Object} prototype of parent ctor.
      */
      extend: function (ctor) { 
        return (this[dataKey].extend=ctor).prototype; 
      },

      /** augment

          Finish a partial declaration.
          TODO: test for bugs, possibly retroactively fix child classes when augmenting parent.

          @param {Function} ctor to augment.

          @return {Object} prototype of partial ctor.
      */
      augment: function (ctor) { 
        return (this[dataKey].augment=ctor).prototype; 
      }

    };


/** decl

    Create a prototype object and return its constructor.

    @param {Function|Object} declaration
*/
function decl (declaration) {
  if (!declaration) {
    declaration = {};
  }
  else if (declaration.call) {
    declaration.prototype=proto;
    declaration.prototype[dataKey]={};
  }
  return getCtor(declaration);
}

/** setDataKey

    Sets the name of the property where declaration objects' 
    metadata will be stored. If you want to pass objects to decl 
    instead of functions, put the metadata (parent, partial, etc.) 
    in this property.

    @param {String} String value to use for dataKey
*/
decl.setDataKey = function (value) { dataKey=value; };

/** clone

    Create a copy of a simple object.

    @param {Object} obj

    @return {Object} clone of obj.

*/
function clone (object) {
  var r=new Clone(Clone.prototype=object);
  Clone.prototype={};
  return r;
};

/** merge

    Merge src object's properties into target object.

    @param {Object} target object to merge properties into.

    @param {Object} src object to merge properties from.

    @return {Object} target for chaining.

*/
function merge (target, src) { 
  for (var k in src) {
    if (src.hasOwnProperty(k) && k!='prototype' && k!=dataKey) {  
      target[k] = src[k];
    }
  }
  return target;
};

/** getCtor

    Prepare a constructor to be returned by decl.

    @param {Function|Object} declaration

    @return {Function} constructor.

*/
function getCtor (declaration) {    
  var oldProto,
      declFn = declaration.call ? declaration : null,
      declObj = declFn ? new declFn(declFn) : declaration, 
      data = declObj[dataKey] || {},
      parent = data.extend, partial = data.augment, 
      ctor =  // user-defined ctor 
              declObj.hasOwnProperty('constructor') ? declObj.constructor : 
              // ctor already defined (partial)  
              partial ? partial : 
              // generated wrapper for parent ctor
              parent ? decl.wrap(parent) : 
              // generated empty function
              new Function(); 

  // If there's a parent constructor, use a clone of its prototype
  // and copy the properties from the current prototype.
  if (parent) {
    oldProto = ctor.prototype;
    ctor.prototype = clone(parent.prototype);
    merge(ctor.prototype, oldProto);
  }

  // Merge the declaration function's properties into the constructor.
  // This allows adding properties to `this.constructor` in the declaration function
  // without defining a constructor, or before defining one.
  merge(ctor, declFn);

  // Merge the declaration objects's properties into the prototype.
  merge(ctor.prototype, declObj);

  // Have the constructor reference itself in its prototype, and return it.
  return (ctor.prototype.constructor=ctor);
};

return decl;

}());

// This is outside of the main closure so wrapper functions
// will have as short a lookup chain as possible.

/** wrap

    Generate wrapper for parent constructor.

    @param {Function} parent constructor to wrap.

    @return {Function} child constructor.

*/
decl.wrap = function (parent) {
  return function(){ parent.apply(this, arguments); };
};

And after:

var decl = (function(_p, _c){

var Clone = new Function(), // dummy function for prototypal cloning

    /** dataKey

        The name of the property where declaration objects' 
        metadata will be stored. If you want to pass objects to decl 
        instead of functions, put the metadata (parent, partial, etc.) 
        in this property.
    */
    dataKey = 'decl-data',

    /** proto

        This object is used as a prototype for declaration objects,
        so all properties are available as properties of `this`
        inside the body of each declaration function.

    */
    proto = {

      /** extend

          Perform prototypal inheritance by calling `this.extend(ParentCtor)`
          within your decalration function.

          @param {Function} ctor to extend.

          @return {Object} prototype of parent ctor.
      */
      extend: function (ctor) { 
        return (this[dataKey].extend=ctor)[_p]; 
      },

      /** augment

          Finish a partial declaration.
          TODO: test for bugs, possibly retroactively fix child classes when augmenting parent.

          @param {Function} ctor to augment.

          @return {Object} prototype of partial ctor.
      */
      augment: function (ctor) { 
        return (this[dataKey].augment=ctor)[_p]; 
      }

    };


/** decl

    Create a prototype object and return its constructor.

    @param {Function|Object} declaration
*/
function decl (declaration) {
  if (!declaration) {
    declaration = {};
  }
  else if (declaration.call) {
    declaration[_p]=proto;
    declaration[_p][dataKey]={};
  }
  return getCtor(declaration);
}

/** setDataKey

    Sets the name of the property where declaration objects' 
    metadata will be stored. If you want to pass objects to decl 
    instead of functions, put the metadata (parent, partial, etc.) 
    in this property.

    @param {String} String value to use for dataKey
*/
decl.setDataKey = function (value) { dataKey=value; };

/** clone

    Create a copy of a simple object.

    @param {Object} obj

    @return {Object} clone of obj.

*/
function clone (object) {
  var r=new Clone(Clone[_p]=object);
  Clone[_p]={};
  return r;
};

/** merge

    Merge src object's properties into target object.

    @param {Object} target object to merge properties into.

    @param {Object} src object to merge properties from.

    @return {Object} target for chaining.

*/
function merge (target, src) { 
  for (var k in src) {
    if (src.hasOwnProperty(k) && k!=_p && k!=dataKey) {  
      target[k] = src[k];
    }
  }
  return target;
};

/** getCtor

    Prepare a constructor to be returned by decl.

    @param {Function|Object} declaration

    @return {Function} constructor.

*/
function getCtor (declaration) {    
  var oldProto,
      declFn = declaration.call ? declaration : null,
      declObj = declFn ? new declFn(declFn) : declaration, 
      data = declObj[dataKey] || {},
      parent = data.extend, partial = data.augment, 
      ctor =  // user-defined ctor 
              declObj.hasOwnProperty(_c) ? declObj[_c] : 
              // ctor already defined (partial)  
              partial ? partial : 
              // generated wrapper for parent ctor
              parent ? decl.wrap(parent) : 
              // generated empty function
              new Function(); 

  // If there's a parent constructor, use a clone of its prototype
  // and copy the properties from the current prototype.
  if (parent) {
    oldProto = ctor[_p];
    ctor[_p] = clone(parent[_p]);
    merge(ctor[_p], oldProto);
  }

  // Merge the declaration function's properties into the constructor.
  // This allows adding properties to `this.constructor` in the declaration function
  // without defining a constructor, or before defining one.
  merge(ctor, declFn);

  // Merge the declaration objects's properties into the prototype.
  merge(ctor[_p], declObj);

  // Have the constructor reference itself in its prototype, and return it.
  return (ctor[_p][_c]=ctor);
};

return decl;

}('prototype', 'constructor'));

// This is outside of the main closure so wrapper functions
// will have as short a lookup chain as possible.

/** wrap

    Generate wrapper for parent constructor.

    @param {Function} parent constructor to wrap.

    @return {Function} child constructor.

*/
decl.wrap = function (parent) {
  return function(){ parent.apply(this, arguments); };
};
share|improve this question
    
You should compare the gzipped file size difference. They are likely roughly the same size –  Juan Mendes Apr 10 '13 at 19:00
add comment

2 Answers

up vote 2 down vote accepted

I would suspect the shortcuts to be slower, which is probably why the minifier does not apply such transformation. At least google chrome has optimizing byte-code compiler for javascript and might be able to take a shortcut or two when it knows the member name is constant, which it does not when variable is used.

I would definitely not recommend doing this manually, but if you really wanted it, you should do it as part of the minification. Damaging readability is not worth it.

share|improve this answer
    
In this case the code is generating constructor functions, and it's only using the shortcuts when doing so. The performance hit would only happen once, at startup. I doubt it would be a big deal in this case. I'll run a test to see how array notation performs compared to property notation. Can you explain what you mean by "do it as part of the minification?" Do you mean with do it with something like m4 macros, before I pass it to closure-compiler? –  GGG Jan 13 '12 at 9:37
    
Array notation is a bit slower. I get 37 mops to dot notation's 41 mops. In this case, that shouldn't be an issue at all... the real issue is readability, I think. –  GGG Jan 13 '12 at 9:49
    
@GGG: Well, it would be best to teach closure to do it, but you could do it with some regular expressions and substitutions (m4 macros would be in the code and hinder readability as well). –  Jan Hudec Jan 13 '12 at 9:50
    
I am thinking about actually declaring .prototype and .constructor as m4 macros and having them rewritten as [_p] and [_c] or whatever. I'm not sure I can get the dots in there, though. I don't have any knowledge of closure's source, or I'd probably take that route. –  GGG Jan 13 '12 at 9:52
add comment

Bear in mind that any decently configured server is going to send out the script using compression, which will cancel out the size savings for frequently used strings, but still leave the minor cost of execution and the significant cost of readability and maintenance. So I would say its not worth it.

share|improve this answer
    
I see your point, but the script is designed to be deployed in unknown environments, it's not just for my own use. My servers all use gzip compression, but I can't control the servers of the users of my script. What if I extend your point above and say "I don't really need to minify it either, because these patterns of repeating letters are exactly the kind of thing gzip is good at, and good webservers run gzip?" I'm not sure it works like that. –  GGG Jan 13 '12 at 21:04
    
True enough for unknown environments. I think minify has a clearer value because there is no run-time performance cost and the size reduction can be significant even with compression. –  Seth Noble Jan 13 '12 at 22:00
    
That's a good point. This whole question might be overly subjective now that I think about it. Is reduction to 6/7 of the size significant? Is performance decrease of 41 mops to 37 mops significant? Is damage to readability significant? How do you balance the tradeoff between the three? Maybe there is no clear answer. :( –  GGG Jan 13 '12 at 22:22
    
To me the clearer answer is that you should only do it if the gzipped version is a lot smaller, which is true in the minified code, but I don't think creating a string to point to a property name will make it faster because that's one the things gzipping does, create a table of strings that are repeated in the file. Since the gain is not substantial, let users who don't gzip it take a small hit, not worth it in my opinion –  Juan Mendes Apr 10 '13 at 19:03
add comment

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

Not the answer you're looking for? Browse other questions tagged or ask your own question.