2010-03-24 82 views
14

John Resig (de jQuery fame) proporciona una implementación concisa de Simple JavaScript Inheritance. Su enfoque inspiró mi intento de mejorar las cosas aún más. He reescrito función original Class.extend de Resig para incluir las siguientes ventajas:Mejora de la herencia de JavaScript simple

  • Rendimiento - menos sobrecarga durante la definición de la clase, la construcción objeto y método de la clase base llama

  • Flexibilidad - optimizado para los navegadores más nuevos compatibles con ECMAScript 5 (p. ej., Chrome), pero proporciona un "shim" equivalente para navegadores antiguos (p. ej. IE6)

  • Compatibilidad - valida en modo estricto y proporciona una mejor compatibilidad de herramienta (p. comentarios VSDoc/jsdoc, Estudio de IntelliSense Visual, etc.)

  • Simplicidad - usted no tiene que ser un "ninja" para entender el código fuente (y es aún más sencillo si pierde las características de ECMAScript 5)

  • Robustez - pasa más "caso límite" pruebas unitarias (por ejemplo primordiales toString en IE)

Debido a que casi parece demasiado bueno para ser verdad, quiero asegurar mi lógica no lo hace tener cualquier defecto fundamental o bu gs, y ver si alguien puede sugerir mejoras o refutar el código. Con eso, presento la función classify:

function classify(base, properties) 
{ 
    /// <summary>Creates a type (i.e. class) that supports prototype-chaining (i.e. inheritance).</summary> 
    /// <param name="base" type="Function" optional="true">The base class to extend.</param> 
    /// <param name="properties" type="Object" optional="true">The properties of the class, including its constructor and members.</param> 
    /// <returns type="Function">The class.</returns> 

    // quick-and-dirty method overloading 
    properties = (typeof(base) === "object") ? base : properties || {}; 
    base = (typeof(base) === "function") ? base : Object; 

    var basePrototype = base.prototype; 
    var derivedPrototype; 

    if (Object.create) 
    { 
     // allow newer browsers to leverage ECMAScript 5 features 
     var propertyNames = Object.getOwnPropertyNames(properties); 
     var propertyDescriptors = {}; 

     for (var i = 0, p; p = propertyNames[i]; i++) 
      propertyDescriptors[p] = Object.getOwnPropertyDescriptor(properties, p); 

     derivedPrototype = Object.create(basePrototype, propertyDescriptors); 
    } 
    else 
    { 
     // provide "shim" for older browsers 
     var baseType = function() {}; 
     baseType.prototype = basePrototype; 
     derivedPrototype = new baseType; 

     // add enumerable properties 
     for (var p in properties) 
      if (properties.hasOwnProperty(p)) 
       derivedPrototype[p] = properties[p]; 

     // add non-enumerable properties (see https://developer.mozilla.org/en/ECMAScript_DontEnum_attribute) 
     if (!{ constructor: true }.propertyIsEnumerable("constructor")) 
      for (var i = 0, a = [ "constructor", "hasOwnProperty", "isPrototypeOf", "propertyIsEnumerable", "toLocaleString", "toString", "valueOf" ], p; p = a[i]; i++) 
       if (properties.hasOwnProperty(p)) 
        derivedPrototype[p] = properties[p]; 
    } 

    // build the class 
    var derived = properties.hasOwnProperty("constructor") ? properties.constructor : function() { base.apply(this, arguments); }; 
    derived.prototype = derivedPrototype; 
    derived.prototype.constructor = derived; 
    derived.prototype.base = derived.base = basePrototype; 

    return derived; 
} 

Y el uso es casi idéntica a la de Resig excepto por el nombre del constructor (constructor vs init) y la sintaxis para las llamadas a métodos de la clase base.

/* Example 1: Define a minimal class */ 
var Minimal = classify(); 

/* Example 2a: Define a "plain old" class (without using the classify function) */ 
var Class = function() 
{ 
    this.name = "John"; 
}; 

Class.prototype.count = function() 
{ 
    return this.name + ": One. Two. Three."; 
}; 

/* Example 2b: Define a derived class that extends a "plain old" base class */ 
var SpanishClass = classify(Class, 
{ 
    constructor: function() 
    { 
     this.name = "Juan"; 
    }, 
    count: function() 
    { 
     return this.name + ": Uno. Dos. Tres."; 
    } 
}); 

/* Example 3: Define a Person class that extends Object by default */ 
var Person = classify(
{ 
    constructor: function(name, isQuiet) 
    { 
     this.name = name; 
     this.isQuiet = isQuiet; 
    }, 
    canSing: function() 
    { 
     return !this.isQuiet; 
    }, 
    sing: function() 
    { 
     return this.canSing() ? "Figaro!" : "Shh!"; 
    }, 
    toString: function() 
    { 
     return "Hello, " + this.name + "!"; 
    } 
}); 

/* Example 4: Define a Ninja class that extends Person */ 
var Ninja = classify(Person, 
{ 
    constructor: function(name, skillLevel) 
    { 
     Ninja.base.constructor.call(this, name, true); 
     this.skillLevel = skillLevel; 
    }, 
    canSing: function() 
    { 
     return Ninja.base.canSing.call(this) || this.skillLevel > 200; 
    }, 
    attack: function() 
    { 
     return "Chop!"; 
    } 
}); 

/* Example 4: Define an ExtremeNinja class that extends Ninja that extends Person */ 
var ExtremeNinja = classify(Ninja, 
{ 
    attack: function() 
    { 
     return "Chop! Chop!"; 
    }, 
    backflip: function() 
    { 
     this.skillLevel++; 
     return "Woosh!"; 
    } 
}); 

var m = new Minimal(); 
var c = new Class(); 
var s = new SpanishClass(); 
var p = new Person("Mary", false); 
var n = new Ninja("John", 100); 
var e = new ExtremeNinja("World", 200); 

Y aquí están mis pruebas QUnit el que pasan todos:

equals(m instanceof Object && m instanceof Minimal && m.constructor === Minimal, true); 
equals(c instanceof Object && c instanceof Class && c.constructor === Class, true); 
equals(s instanceof Object && s instanceof Class && s instanceof SpanishClass && s.constructor === SpanishClass, true); 
equals(p instanceof Object && p instanceof Person && p.constructor === Person, true); 
equals(n instanceof Object && n instanceof Person && n instanceof Ninja && n.constructor === Ninja, true); 
equals(e instanceof Object && e instanceof Person && e instanceof Ninja && e instanceof ExtremeNinja && e.constructor === ExtremeNinja, true); 

equals(c.count(), "John: One. Two. Three."); 
equals(s.count(), "Juan: Uno. Dos. Tres."); 

equals(p.isQuiet, false); 
equals(p.canSing(), true); 
equals(p.sing(), "Figaro!"); 

equals(n.isQuiet, true); 
equals(n.skillLevel, 100); 
equals(n.canSing(), false); 
equals(n.sing(), "Shh!"); 
equals(n.attack(), "Chop!"); 

equals(e.isQuiet, true); 
equals(e.skillLevel, 200); 
equals(e.canSing(), false); 
equals(e.sing(), "Shh!"); 
equals(e.attack(), "Chop! Chop!"); 
equals(e.backflip(), "Woosh!"); 
equals(e.skillLevel, 201); 
equals(e.canSing(), true); 
equals(e.sing(), "Figaro!"); 
equals(e.toString(), "Hello, World!"); 

¿Alguien ve algo malo en mi acercamiento vs John Resig de original approach? Sugerencias y comentarios son bienvenidos!

NOTA: El código anterior se ha modificado significativamente desde que inicialmente publiqué esta pregunta. Lo anterior representa la última versión. Para ver cómo ha evolucionado, verifique el historial de revisión.

+0

Recomendaría 'Object.create' y [traitsjs] (http://traitsjs.org/). La herencia no es buena en javascript, usa la composición de objetos – Raynos

+0

Quizás todavía no estoy acostumbrado, pero la sintaxis de los rasgos me da vueltas. Creo que pasaré hasta que obtenga un seguimiento ... – Will

Respuesta

4

No tan rápido. Simplemente no funciona.

considerar:

var p = new Person(true); 
alert("p.dance()? " + p.dance()); => true 

var n = new Ninja(); 
alert("n.dance()? " + n.dance()); => false 
n.dancing = true; 
alert("n.dance()? " + n.dance()); => false 

base es simplemente otro objeto inicializado con los miembros predeterminados que le hizo pensar en que funciona.

EDIT: para que conste, aquí está mi propia (aunque de forma más detallada) ejecución de Java como herencia en JavaScript, elaborado en 2006 en el momento me inspiré por Dean Edward's Base.js (y estoy de acuerdo con él cuando he says John's version is just a rewrite of his Base.js). You can see it in action (and step debug it in Firebug) here.

/** 
* A function that does nothing: to be used when resetting callback handlers. 
* @final 
*/ 
EMPTY_FUNCTION = function() 
{ 
    // does nothing. 
} 

var Class = 
{ 
    /** 
    * Defines a new class from the specified instance prototype and class 
    * prototype. 
    * 
    * @param {Object} instancePrototype the object literal used to define the 
    * member variables and member functions of the instances of the class 
    * being defined. 
    * @param {Object} classPrototype the object literal used to define the 
    * static member variables and member functions of the class being 
    * defined. 
    * 
    * @return {Function} the newly defined class. 
    */ 
    define: function(instancePrototype, classPrototype) 
    { 
    /* This is the constructor function for the class being defined */ 
    var base = function() 
    { 
     if (!this.__prototype_chaining 
      && base.prototype.initialize instanceof Function) 
     base.prototype.initialize.apply(this, arguments); 
    } 

    base.prototype = instancePrototype || {}; 

    if (!base.prototype.initialize) 
     base.prototype.initialize = EMPTY_FUNCTION; 

    for (var property in classPrototype) 
    { 
     if (property == 'initialize') 
     continue; 

     base[property] = classPrototype[property]; 
    } 

    if (classPrototype && (classPrototype.initialize instanceof Function)) 
     classPrototype.initialize.apply(base); 

    function augment(method, derivedPrototype, basePrototype) 
    { 
     if ( (method == 'initialize') 
      &&(basePrototype[method].length == 0)) 
     { 
     return function() 
     { 
      basePrototype[method].apply(this); 
      derivedPrototype[method].apply(this, arguments); 
     } 
     } 

     return function() 
     { 
     this.base = function() 
        { 
         return basePrototype[method].apply(this, arguments); 
        }; 

     return derivedPrototype[method].apply(this, arguments); 
     delete this.base; 
     } 
    } 

    /** 
    * Provides the definition of a new class that extends the specified 
    * <code>parent</code> class. 
    * 
    * @param {Function} parent the class to be extended. 
    * @param {Object} instancePrototype the object literal used to define 
    * the member variables and member functions of the instances of the 
    * class being defined. 
    * @param {Object} classPrototype the object literal used to define the 
    * static member variables and member functions of the class being 
    * defined. 
    * 
    * @return {Function} the newly defined class. 
    */ 
    function extend(parent, instancePrototype, classPrototype) 
    { 
     var derived = function() 
     { 
     if (!this.__prototype_chaining 
      && derived.prototype.initialize instanceof Function) 
      derived.prototype.initialize.apply(this, arguments); 
     } 

     parent.prototype.__prototype_chaining = true; 

     derived.prototype = new parent(); 

     delete parent.prototype.__prototype_chaining; 

     for (var property in instancePrototype) 
     { 
     if ( (instancePrototype[property] instanceof Function) 
      &&(parent.prototype[property] instanceof Function)) 
     { 
      derived.prototype[property] = augment(property, instancePrototype, parent.prototype); 
     } 
     else 
      derived.prototype[property] = instancePrototype[property]; 
     } 

     derived.extend = function(instancePrototype, classPrototype) 
         { 
          return extend(derived, instancePrototype, classPrototype); 
         } 

     for (var property in classPrototype) 
     { 
     if (property == 'initialize') 
      continue; 

     derived[property] = classPrototype[property]; 
     } 

     if (classPrototype && (classPrototype.initialize instanceof Function)) 
     classPrototype.initialize.apply(derived); 

     return derived; 
    } 

    base.extend = function(instancePrototype, classPrototype) 
        { 
        return extend(base, instancePrototype, classPrototype); 
        } 
    return base; 
    } 
} 

Y esta es la forma en que lo utilice:

var Base = Class.define(
{ 
    initialize: function(value) // Java constructor equivalent 
    { 
    this.property = value; 
    }, 

    property: undefined, // member variable 

    getProperty: function() // member variable accessor 
    { 
    return this.property; 
    }, 

    foo: function() 
    { 
    alert('inside Base.foo'); 
    // do something 
    }, 

    bar: function() 
    { 
    alert('inside Base.bar'); 
    // do something else 
    } 
}, 
{ 
    initialize: function() // Java static initializer equivalent 
    { 
    this.property = 'Base'; 
    }, 

    property: undefined, // static member variables can have the same 
           // name as non static member variables 

    getProperty: function() // static member functions can have the same 
    {         // name as non static member functions 
    return this.property; 
    } 
}); 

var Derived = Base.extend(
{ 
    initialize: function() 
    { 
    this.base('derived'); // chain with parent class's constructor 
    }, 

    property: undefined, 

    getProperty: function() 
    { 
    return this.property; 
    }, 

    foo: function() // override foo 
    { 
    alert('inside Derived.foo'); 
    this.base(); // call parent class implementation of foo 
    // do some more treatments 
    } 
}, 
{ 
    initialize: function() 
    { 
    this.property = 'Derived'; 
    }, 

    property: undefined, 

    getProperty: function() 
    { 
    return this.property; 
    } 
}); 

var b = new Base('base'); 
alert('b instanceof Base returned: ' + (b instanceof Base)); 
alert('b.getProperty() returned: ' + b.getProperty()); 
alert('Base.getProperty() returned: ' + Base.getProperty()); 

b.foo(); 
b.bar(); 

var d = new Derived('derived'); 
alert('d instanceof Base returned: ' + (d instanceof Base)); 
alert('d instanceof Derived returned: ' + (d instanceof Derived)); 
alert('d.getProperty() returned: ' + d.getProperty()); 
alert('Derived.getProperty() returned: ' + Derived.getProperty()); 

d.foo(); 
d.bar(); 
+0

Maldición, gracias por señalar esa falla importante. Daré otra puñalada, pero probablemente terminaré en la función original de John Resig. – Will

+0

La función de Sure John está perfectamente bien (de nuevo, debería haberle dado crédito a Dean Edwards). De todos modos, ve a la cabeza, dale otra puñalada como lo hice en aquel entonces: es parte de la diversión y la comprensión de que el funcionamiento interno del lenguaje te hará (sentirás) un mejor programador. Curiosamente, nunca utilicé mi implementación, solo por el bien :) Además, realmente no veo el punto de tratar de reducir la cantidad máxima de lógica en la cantidad mínima de código: seguro mi versión es prolija, pero cada vez que vuelvo a leerlo entiendo lo que está pasando. –

+0

Creo que todo funciona ahora. Realicé un pequeño cambio en las llamadas al método base para utilizar la sintaxis "base.method.call (this)" que corrige el problema que informaste. ¿Ve algún otro problema con la implementación? No estoy seguro de que este sea un ejercicio sin sentido. Creo que una de las razones por las que la mayoría de los desarrolladores se asustan de la herencia de JavaScript es por la "magia negra" que implica la comprensión de la implementación o la fea sintaxis de herencia a la que se ven forzados. Creo que esto ayuda a abordar ambas preocupaciones (siempre que sea correcto, por supuesto). – Will

1

Esto es tan simple como usted puede conseguir. Fue tomado del http://www.sitepoint.com/javascript-inheritance/.

// copyPrototype is used to do a form of inheritance. See http://www.sitepoint.com/blogs/2006/01/17/javascript-inheritance/# 
// Example: 
// function Bug() { this.legs = 6; } 
// Insect.prototype.getInfo = function() { return "a general insect"; } 
// Insect.prototype.report = function() { return "I have " + this.legs + " legs"; } 
// function Millipede() { this.legs = "a lot of"; } 
// copyPrototype(Millipede, Bug); /* Copy the prototype functions from Bug into Millipede */ 
// Millipede.prototype.getInfo = function() { return "please don't confuse me with a centipede"; } /* ''Override" getInfo() */ 
function copyPrototype(descendant, parent) { 
    var sConstructor = parent.toString(); 
    var aMatch = sConstructor.match(/\s*function (.*)\(/); 
    if (aMatch != null) { descendant.prototype[aMatch[1]] = parent; } 
    for (var m in parent.prototype) { 

    descendant.prototype[m] = parent.prototype[m]; 
    } 
}; 
+0

Es simple, está bien, pero no es tan útil (sin acceso base/super), ni es bonita IMO. – Will

+0

@Will: puede acceder a los métodos principales. Consulte el enlace para obtener más explicaciones. –

+0

@JohnFisher Creo que querías 'Bug.prototype' en lugar de 'Insert.prototype' en los comentarios del código. –

5

Hace algún tiempo, miré a varios sistemas de objetos de JS e incluso en práctica algunas de mi propia, por ejemplo class.js (ES5 version) y proto.js.

La razón por la que nunca los usé: terminará escribiendo la misma cantidad de código. Caso en cuestión: Ninja-ejemplo de Resig (sólo añade algo de espacio en blanco):

var Person = Class.extend({ 
    init: function(isDancing) { 
     this.dancing = isDancing; 
    }, 

    dance: function() { 
     return this.dancing; 
    } 
}); 

var Ninja = Person.extend({ 
    init: function() { 
     this._super(false); 
    }, 

    swingSword: function() { 
     return true; 
    } 
}); 

19 líneas, 264 bytes.

JS estándar con Object.create() (que es una función de 5 ECMAScript, pero para nuestros propósitos puede ser sustituida por una costumbre ES3 clone() aplicación):

function Person(isDancing) { 
    this.dancing = isDancing; 
} 

Person.prototype.dance = function() { 
    return this.dancing; 
}; 

function Ninja() { 
    Person.call(this, false); 
} 

Ninja.prototype = Object.create(Person.prototype); 

Ninja.prototype.swingSword = function() { 
    return true; 
}; 

17 líneas, 282 bytes. Imo, los bytes adicionales no son realmente la complejidad añadida de un sistema de objetos separado. Es bastante fácil acortar el ejemplo estándar agregando algunas funciones personalizadas, pero de nuevo: no vale la pena.

+0

+1, punto de vista muy interesante – Alsciende

+1

Solía ​​pensar lo mismo que tú, pero ahora tengo que estar en desacuerdo. Básicamente, todo lo que John Resig y yo hemos hecho es crear un único método ("extender") que conecte el comportamiento exacto del objeto/prototipo que tienes en el ejemplo anterior (es decir, no es un nuevo sistema de objetos). La única diferencia es que la sintaxis es más corta, más estricta y menos propensa a errores cuando se usa el método extend. – Will

+0

Después de reflexionar sobre esto, acepto que usar la sintaxis estándar de conexión es tan corta como usar la sintaxis de extensión. Todavía creo que este último es mucho más limpio, menos propenso a errores, y es más un enfoque de "convención sobre configuración", si tiene sentido. – Will