As many people know, JavaScript doesn’t really have classes but you can mimic some of their behavior with the prototypal setup available in JavaScript. Still, a lot of times it is just easier when you have a function that does most of the work for you. For that reason, I wrote the classify:

/**
 * @license JS Classify (v1.1) - By Chris West - MIT License
 */
/**
 * Creates a class using the specified constructor and options.
 * @param  {!Function} constructor  The constructor function.
 * @param  {{ privateKey:string, setters:Array, getters:Array<=string>, prototype:Object, properties:Object, superClass:Function }} options
 *     Object containing the class options.  The privateKey is the name of the
 *     privateKey that will be assigned to every instance.  The setters array
 *     contains names of private data members for which setters will be setup.
 *     The getters array contains names of private data members for which
 *     getters will be setup.  The prototype object will contain the prototype
 *     values that will be attached to the class' prototype.  The superClass
 *     is the function that will act as the class' super class and all
 *     prototypes will be inherited from it.
 * @return {!Function}  The newly created class.
 */
var classify = (function(fakeClass) {
  var hasOwnProperty = {}.hasOwnProperty;
 
  function camelCase(str, delim) {
    delim = delim || ' ';
    var pos;
    while ((pos = str.indexOf(delim)) + 1) {
      str = str.slice(0, pos) + str.charAt(pos + 1).toUpperCase() + str.slice(pos + 2);
    }
    return str;
  }
 
  /**
   * Setup inheritance.
   * @param {!Function} baseClass  The base class from which the subclass
   *     inherits its prototypal values.
   * @param {!Function} subClass  Class which inherits from the base class.
   * @return {!Function}  The updated subclass.
   */
  function inherit(baseClass, subClass) {
    fakeClass.prototype = baseClass.prototype;
    var prototype = subClass.prototype = new fakeClass();
    prototype.superClass = baseClass;
    return prototype.constructor = subClass;
  }
 
  // Return classify function.
  return function(constructor, options) {
    var outerPrivateData;
 
    var privateKey = options.privateKey || '_';
 
    var realConstructor = function() {
      this[privateKey] = properties && hasOwnProperty.call(properties, privateKey)
        ? properties[privateKey]
        : {};
      if (superClass) {
        this.superClass = superClass;
      }
      try {
        return constructor.apply(this, arguments);
      }
      finally {
        if (superClass) {
          delete this.superClass;
        }
      }
    };
 
    // If the super-class is defined use it.
    var superClass = options.superClass;
    if (superClass) {
      realConstructor = inherit(superClass, realConstructor);
    }
 
    // Add class level properties.
    var properties = options.properties;
    if (properties) {
      for (var key in properties) {
        realConstructor[key] = properties[key];
      }
    }
 
    var realPrototype = realConstructor.prototype;
    var myPrototype = options.prototype || {};
 
    // Add getters.
    var getters = options.getters || [];
    for (var i = 0, len = getters.length; i < len; i++) {
      (function(name) {
        myPrototype[camelCase('get_' + name, '_')] = function() {
          return this[privateKey][name];
        };
      })(getters[i]);
    }
 
    // Add setters.
    var setters = options.setters || [];
    for (var i = 0, len = setters.length; i < len; i++) {
      (function(name) {
        myPrototype[camelCase('set_' + name, '_')] = function(newValue) {
          var privateData = this[privateKey];
          var oldValue = privateData[name];
          privateData[name] = newValue;
          return oldValue;
        };
      })(setters[i]);
    }
 
    // Add all prototypal values.
    for (var key in myPrototype) {
      realPrototype[key] = myPrototype[key];
    }
 
    return realConstructor;
  }
})(function(){});

How To Use classify()

The first parameter that you pass should be the constructor. The second parameter will be an object containing properties representing any options you want to add to the class:

  • privateKey - string
    Defaults to "_". The property name for the object which will house all of the private data for each class instance.
  • getters - Array
    An array of strings indicating the getters that should be automatically setup to retrieve the private data members with the same name. These names will be camel-cased based on underscore characters.
  • setters - Array
    An array of strings indicating the setters that should be automatically setup to set the private data members with the same name. These names will be camel-cased based on underscore characters. All setters return the previous value.
  • properties - Object
    An object containing all of the properties that will be added to the class object.
  • prototype - Object
    An object containing all of the values that should be added to the prototype of the class.
  • superClass - Function
    The super-class from which this new class will inherit. This will overwrite the superClass property of the class' prototype.

Example Classes

The following exemplifies how easy it is to create classes with classify():

// Create a simple base class.
Being = classify(
  function (species) {
    this._.species = species;
  },
  { getters: ['species'] }
);

// Create a more complex sub-class.
Human = classify(
  function (firstName, lastName) {
    this.superClass.call(this, 'Human');
    this._.firstName = firstName;
    this._.lastName = lastName;
  },
  {
    getters: [ 'firstName', 'lastName' ],
    setters: [ 'firstName' ],
    prototype: {
      toString: function() {
        return this.getFullName() + ' (' + this._.species + ')';
      },
      getFullName: function() {
        return this._.firstName + ' ' + this._.lastName;
      }
    },
    superClass: Being
  }
);

The first class that I created above is a Being class which just has one private member: species. The Being.prototype.getSpecies() function is defined for this class as well. The second class is the Human class which is a sub-class of the Being class. The species defaults to "human" while two more private members are included: firstName and lastName. There are getters for all of the private members but there is only one setter which is for the firstName. Additionally the Human.prototype.toString() and Human.prototype.getFullName() functions have been defined to provide additional features to the class.

Try Me!

The above example shows (as long as you are using a modern browser) how these newly created functions work. There is more you can do with this of course so if you want go ahead and play with the above example or copy the classify code and create your own classes. Happy coding! šŸ˜Ž


1 Comment

Chris West · April 28, 2014 at 5:12 PM

FYI, the 1.1 update basically involves making this.superClass(...) always call the super-class of the constructor from which it is being called. If called outside of the constructor it will reference the superClass defined initially. The following code will now work as expected, calling all super-class constructors:

var A = classify(
    function() {
        console.log('A constructor');
    },
    {
        prototype: {
            a: 1
        }
    }
);

var B = classify(
    function() {
        this.superClass.apply(this, arguments);
        console.log('B constructor');
    },
    {
        superClass: A,
        prototype: {
            b: 2
        }
    }
);

var C = classify(
    function() {
        this.superClass.apply(this, arguments);
        console.log('C constructor');
    },
    {
        superClass: B,
        prototype: {
            c: 1
        }
    }
);

Leave a Reply

Your email address will not be published. Required fields are marked *