2010-03-24 10 views
14

John Resig (di fama jQuery) fornisce un'implementazione concisa di Simple JavaScript Inheritance. Il suo approccio ha ispirato il mio tentativo di migliorare ulteriormente le cose. Ho riscritto funzione originaria Class.extend di Resig per includere i seguenti vantaggi:Miglioramento dell'ereditarietà JavaScript semplice

  • prestazioni - meno overhead durante la definizione della classe, la costruzione oggetto e metodo della classe base chiama

  • Flessibilità - ottimizzato per nuovi browser compatibili con ECMAScript 5 (ad es. Chrome), ma fornisce uno "shim" equivalente per i browser più vecchi (es. IE6)

  • Compatibilità - convalida in modalità rigorosa e offre una migliore compatibilità degli strumenti (ad es. commenti VSDoc/JSDoc, Visual Studio IntelliSense, ecc)

  • Semplicità - non c'è bisogno di essere un "ninja" per capire il codice sorgente (ed è ancora più semplice se si perde le caratteristiche ECMAScript 5)

  • Robustezza - passa più test di unità "case d'angolo" (es imperativi toString in IE)

Perché sembra quasi troppo bello per essere vero, voglio assicurare la mia logica non lo fa ha qualche difetto fondamentale o bu gs, e vedi se qualcuno può suggerire miglioramenti o smentire il codice. Con questo, presento la funzione 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; 
} 

E l'utilizzo è quasi identico a Resig di eccezione per il nome del costruttore (constructor vs. init) e la sintassi per le chiamate metodo della classe 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); 

E qui sono le mie prove QUnit cui tutto passa:

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!"); 

Chiunque vede qualcosa di sbagliato con il mio approccio vs John Resig di original approach? Suggerimenti e feedback sono ben accetti!

NOTA: il codice precedente è stato modificato in modo significativo da quando ho inizialmente postato questa domanda. Quanto sopra rappresenta l'ultima versione. Per vedere come si è evoluto, si prega di controllare la cronologia delle revisioni.

+0

Consiglierei 'Object.create' e [traitsjs] (http://traitsjs.org/). L'ereditarietà non va bene in javascript, usa la composizione dell'oggetto – Raynos

+0

Forse non mi sono ancora abituato, ma la sintassi dei tratti mi fa girare la testa. Penso che passerò fino a quando non otterrà un seguito ... – Will

risposta

4

Non così veloce. Semplicemente non funziona.

considerare:

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 è solo un altro oggetto inizializzato con i membri di default che ti ha fatto pensare che funziona.

EDIT: per la cronaca, qui è la mia (anche se più verboso) implementazione di Java come eredità in Javascript, realizzato nel 2006, al momento mi sono ispirato da Dean Edward's Base.js (e sono d'accordo con lui quando 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; 
    } 
} 

e questo è come lo si utilizza:

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

Darn, grazie per aver indicato che il difetto principale. Prenderò un'altra pugnalata, ma probabilmente finirò con la funzione originale di John Resig. – Will

+0

Certo, la funzione di John è perfetta (anche in questo caso avrebbe dovuto accreditare Dean Edwards). Ad ogni modo, vai a testa, prendi un'altra pugnalata come ho fatto allora: fa parte del divertimento e capire questi meccanismi interni del linguaggio ti farà sentire un programmatore migliore. È interessante notare che non ho mai veramente usato la mia implementazione, era solo per il gusto di farlo :) Inoltre non vedo davvero il punto di cercare di ridurre la quantità massima di logica nella quantità minima di codice: certo la mia versione è prolissa, ma ogni volta che torno a leggerlo capisco cosa sta succedendo. –

+0

Credo che tutto funzioni ora. Ho apportato una piccola modifica alle chiamate al metodo base per utilizzare la sintassi "base.method.call (this)" che corregge il problema segnalato. Vedi altri problemi con l'implementazione? Non sono sicuro che sia un esercizio inutile. Credo che uno dei motivi per cui la maggior parte degli sviluppatori evita l'ereditarietà di JavaScript è a causa della "magia nera" che è implicata nella comprensione dell'implementazione o della brutta sintassi di ereditarietà in cui sono forzati. Credo che questo aiuti ad affrontare entrambe le preoccupazioni (purché sia ​​corretto, naturalmente). – Will

1

Questo è quanto di semplice come si può ottenere. È stato preso da 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

È semplice, ma non altrettanto utile (senza base/accesso super), né abbastanza IMO. – Will

+0

@Will: è possibile accedere ai metodi padre. Controlla il link per ulteriori spiegazioni. –

+0

@JohnFisher Penso che tu intendessi "Bug".prototipo' piuttosto che 'Insert.prototype' nei commenti del codice. –

5

Qualche tempo fa, ho guardato diversi sistemi oggetto per JS e anche implementato un paio di mio, per esempio class.js (ES5 version) e proto.js.

Il motivo per cui non li ho mai usati: finirai per scrivere la stessa quantità di codice. Caso in questione: Ninja-esempio di Resig (aggiunto solo qualche spazio bianco):

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 linee, 264 byte.

standard JS con Object.create() (che è una funzione ECMAScript 5, ma per i nostri scopi può essere sostituita da una consuetudine ES3 clone() attuazione):

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 linee, 282 byte. Imo, i byte extra non sono realmente la complessità aggiuntiva di un sistema di oggetti separato. È abbastanza facile rendere l'esempio standard più breve aggiungendo alcune funzioni personalizzate, ma ancora una volta: non ne vale davvero la pena.

+0

+1, punto di vista molto interessante – Alsciende

+1

Ero abituato a pensare come te, ma ora non sono d'accordo. Fondamentalmente, tutto ciò che John Resig ed io abbiamo fatto è creare un singolo metodo ("estendere") che colleghi il comportamento esatto dell'oggetto/prototipo che hai nell'esempio precedente (cioè non è un nuovo sistema di oggetti). L'unica differenza è che la sintassi è più breve, più stretta e meno soggetta a errori quando si utilizza il metodo di estensione. – Will

+0

Dopo aver riflettuto su di esso, sono d'accordo sul fatto che l'utilizzo della sintassi di wire-up standard è altrettanto breve quanto l'uso della sintassi di estensione. Continuo a pensare che quest'ultimo sia molto più pulito, meno incline agli errori ed è più un approccio di "convenzione sulla configurazione", se questo ha senso. – Will

Problemi correlati