baja/sys/inherit.js

/**
 * @copyright 2015 Tridium, Inc. All Rights Reserved.
 * @author Gareth Johnson
 */

/**
 * Defines class inheritance functions {@link baja.subclass},
 * {@link baja.callSuper}, and {@link baja.mixin}.
 * @module baja/sys/inherit
 */
define([], function () {
  "use strict";

  /**
   * Extends an Object by setting up the prototype chain.
   * 
   * This method can have a Function or Object passed in as the SuperCtor argument.
   * If SuperCtor Function is passed in, 'new' will be called on it to create an Object.
   * The new Object will then be set on the target Function's prototype.
   *
   * @function baja.subclass
   *
   * @see baja.mixin
   *
   * @param {Function} Ctor the subclass constructor
   * @param {Function|Object} SuperCtor the superclass constructor  
   * @param {Function} [mixIn] a function that can apply a MixIn to the subclass.
   * @returns {Function}  Ctor.
   */
  function subclass(Ctor, SuperCtor) {
      
    // So the 'super' class can be tracked, a '$super' property is
    // created and attached to the Function instance
    if (typeof SuperCtor === 'string') {
      //TODO: require registry
      SuperCtor = require('baja!').findCtor(SuperCtor);
    }
    
    if (typeof SuperCtor === 'function') {        
      Ctor.prototype = Object.create(SuperCtor.prototype);
      Ctor.$super = SuperCtor;
    } else {
      Ctor.prototype = SuperCtor;
      Ctor.$super = SuperCtor.constructor;
    }
    Ctor.prototype.constructor = Ctor;
    
    // If there are more than two arguments then assume some Mix-Ins are being passed in.
    if (arguments.length > 2) {
      var i;
      for (i = 2; i < arguments.length; ++i) {
        Ctor = mixin(Ctor, arguments[i]);
      }
    }

    return Ctor;
  }
  
  /**
   * Call the instance's super method.
   * 
   * @function baja.callSuper
   * 
   * @param {String} [methodName] the name of the method to invoke on the Super JavaScript Constructor. If not specified
   *                              then it's assumed the super JavaScript Constructor is being called.
   * @param instance the instance to the Super JavaScript Constructor will be looked up from.
   * @param {Array} [args] the arguments to invoke on the super method.
   * @returns value returned from super method.
   */
  function callSuper(methodName, Ctor, instance, args) {
    if (typeof methodName === "string") {
      return Ctor.$super.prototype[methodName].apply(instance, args || []);
    }
    
    args = instance;
    instance = Ctor;
    Ctor = methodName;
    return Ctor.$super.apply(instance, args || []);
  }
  
  /**
   * Apply a Mix-In to the Constructor/Object.
   * 
   * The Constructor passed in is extended then the MixIn function is 
   * invoked so that it can add to the newly extended object's prototype
   * chain.
   * 
   * This method can be invoked via a call to `baja.subclass`.
   *
   * @function baja.mixin
   * 
   * @param {Function} Ctor the Constructor that we're mixing into.
   * @param {Function} mixin the Mix-In Function that when invoked will append
   *                         new method's to the newly subclassed Constructor. 
   *                         When invoked, the new Constructor is passed in as the first
   *                         argument.
   * @returns {Function} the newly extended Constructor that has been extended with the Mix-In.
   */
  function mixin(Ctor, mixin) {

    if (Ctor &&
      mixin && 
        typeof Ctor === "function" && 
        typeof mixin === "function") {      
      // If we're applying a MixIn then extend
      // the prototype chain and then apply the MixIn
      // to the prototype.
      Ctor = subclass(function () {
        callSuper(Ctor, this, arguments);
      }, Ctor);
      
      // Apply the Mix-In
      mixin(Ctor);
    } else {
      throw new Error("Invalid MixIn arguments: " + Ctor + " -> " + mixin);
    }
    
    return Ctor;
  }
  
  return {
    callSuper: callSuper,
    mixin: mixin,
    subclass: subclass
  };
});