Buon esempio di ereditarietà basata su prototipi di JavaScript


89

Sto programmando con linguaggi OOP da oltre 10 anni, ma ora sto imparando JavaScript ed è la prima volta che incontro l'ereditarietà basata su prototipi. Tendo ad imparare più velocemente studiando un buon codice. Qual è un esempio ben scritto di un'applicazione (o libreria) JavaScript che utilizza correttamente l'ereditarietà prototipale? E puoi descrivere (brevemente) come / dove viene utilizzata l'ereditarietà prototipale, quindi so da dove iniziare a leggere?


1
Hai avuto la possibilità di controllare quella libreria di Base? È davvero carino e piuttosto piccolo. Se ti piace, considera la possibilità di contrassegnare la mia risposta come risposta. TIA, roland.
Roland Bouman

Immagino di essere sulla stessa barca di te. Voglio anche imparare un po 'su questo linguaggio prototipale, non essendo limitato solo a framework oop o simili, anche se sono fantastici e tutto ciò che dobbiamo imparare, giusto? Non solo alcuni framework lo fanno per me, anche se lo userò. Ma impara a creare cose nuove in nuove lingue con nuovi modi, pensa fuori dagli schemi. Mi piace il tuo stile. Cercherò di aiutarmi e forse di aiutarti. Non appena trovo qualcosa, te lo farò sapere.
marcelo-ferraz

Risposte:


48

Douglas Crockford ha una bella pagina su JavaScript Prototypal Inheritance :

Cinque anni fa ho scritto Classical Inheritance in JavaScript. Ha dimostrato che JavaScript è un linguaggio prototipale privo di classi e che ha una potenza espressiva sufficiente per simulare un sistema classico. Da allora il mio stile di programmazione si è evoluto, come dovrebbe fare qualsiasi buon programmatore. Ho imparato ad abbracciare pienamente il prototipismo e mi sono liberato dai confini del modello classico.

I lavori Base.js di Dean Edward , Class di Mootools o Simple Inheritance di John Resig sono modi per eseguire l'ereditarietà classica in JavaScript.


Perché non semplicemente newObj = Object.create(oldObj);se lo vuoi senza classi? Altrimenti, sostituire con oldObjcon l'oggetto prototipo della funzione costruttore dovrebbe funzionare?
Cyker

76

Come accennato, i film di Douglas Crockford danno una buona spiegazione sul perché e copre il come. Ma per metterlo in un paio di righe di JavaScript:

// Declaring our Animal object
var Animal = function () {

    this.name = 'unknown';

    this.getName = function () {
        return this.name;
    }

    return this;
};

// Declaring our Dog object
var Dog = function () {

    // A private variable here        
    var private = 42;

    // overriding the name
    this.name = "Bello";

    // Implementing ".bark()"
    this.bark = function () {
        return 'MEOW';
    }  

    return this;
};


// Dog extends animal
Dog.prototype = new Animal();

// -- Done declaring --

// Creating an instance of Dog.
var dog = new Dog();

// Proving our case
console.log(
    "Is dog an instance of Dog? ", dog instanceof Dog, "\n",
    "Is dog an instance of Animal? ", dog instanceof Animal, "\n",
    dog.bark() +"\n", // Should be: "MEOW"
    dog.getName() +"\n", // Should be: "Bello"
    dog.private +"\n" // Should be: 'undefined'
);

Il problema con questo approccio, tuttavia, è che ricrea l'oggetto ogni volta che ne crei uno. Un altro approccio consiste nel dichiarare i tuoi oggetti sullo stack del prototipo, in questo modo:

// Defining test one, prototypal
var testOne = function () {};
testOne.prototype = (function () {
    var me = {}, privateVariable = 42;
    me.someMethod = function () {
        return privateVariable;
    };

    me.publicVariable = "foo bar";
    me.anotherMethod = function () {
        return this.publicVariable;
    };

    return me;

}());


// Defining test two, function
var testTwo = function() {
    var me = {}, privateVariable = 42;
    me.someMethod = function () {
        return privateVariable;
    };

    me.publicVariable = "foo bar";
    me.anotherMethod = function () {
        return this.publicVariable;
    };

    return me;
};


// Proving that both techniques are functionally identical
var resultTestOne = new testOne(),
    resultTestTwo = new testTwo();

console.log(
    resultTestOne.someMethod(), // Should print 42
    resultTestOne.publicVariable // Should print "foo bar"
);

console.log(
    resultTestTwo.someMethod(), // Should print 42
    resultTestTwo.publicVariable // Should print "foo bar"
);



// Performance benchmark start
var stop, start, loopCount = 1000000;

// Running testOne
start = (new Date()).getTime(); 
for (var i = loopCount; i>0; i--) {
    new testOne();
}
stop = (new Date()).getTime();

console.log('Test one took: '+ Math.round(((stop/1000) - (start/1000))*1000) +' milliseconds');



// Running testTwo
start = (new Date()).getTime(); 
for (var i = loopCount; i>0; i--) {
    new testTwo();
}
stop = (new Date()).getTime();

console.log('Test two took: '+ Math.round(((stop/1000) - (start/1000))*1000) +' milliseconds');

C'è un leggero svantaggio quando si parla di introspezione. Dumping testOne, risulterà in informazioni meno utili. Anche la proprietà privata "privateVariable" in "testOne" è condivisa in tutti i casi, anche utilmente menzionata nelle risposte di shesek.


3
Nota che in testOne privateVariableè semplicemente una variabile nell'ambito dell'IIFE ed è condivisa tra tutte le istanze, quindi non dovresti memorizzare dati specifici dell'istanza su di essa. (su testTwo è specifico dell'istanza, poiché ogni chiamata a testTwo () crea un nuovo ambito per istanza)
shesek

Ho votato positivamente perché hai mostrato l'altro approccio e perché non usarlo perché fa copie
Murphy316

Il problema di ricreare l'oggetto ogni volta è dovuto principalmente ai metodi che vengono ricreati per ogni nuovo oggetto. Tuttavia, possiamo mitigare il problema definendo il metodo su Dog.prototype. Quindi, invece di usare this.bark = function () {...}, possiamo fare Dot.prototype.bark = function () {...}al di fuori della Dogfunzione. (Vedi maggiori dettagli in questa risposta )
Huang Chao

26
function Shape(x, y) {
    this.x = x;
    this.y = y;
}

// 1. Explicitly call base (Shape) constructor from subclass (Circle) constructor passing this as the explicit receiver
function Circle(x, y, r) {
    Shape.call(this, x, y);
    this.r = r;
}

// 2. Use Object.create to construct the subclass prototype object to avoid calling the base constructor
Circle.prototype = Object.create(Shape.prototype);

3
Forse l'aggiunta di questo link con la tua risposta potrebbe completare ancora di più il quadro: developer.mozilla.org/en/docs/Web/JavaScript/Reference/…
Dynom

14

Vorrei dare un'occhiata a YUI e alla Basebiblioteca di Dean Edward : http://dean.edwards.name/weblog/2006/03/base/

Per YUI puoi dare una rapida occhiata al modulo lang , esp. il metodo YAHOO.lang.extend . Quindi, puoi sfogliare l'origine di alcuni widget o utilità e vedere come usano quel metodo.


YUI 2 è stato deprecato a partire dal 2011, quindi il collegamento a langè semi-interrotto. Qualcuno vuole aggiustarlo per YUI 3?
ack

lang in yui 3 non sembra avere un metodo di estensione. ma poiché la risposta intende utilizzare l'implementazione come esempio, la versione non ha importanza.
eMBee


4

Questo è l'esempio più chiaro che ho trovato, dal libro Node di Mixu ( http://book.mixu.net/node/ch6.html ):

Preferisco la composizione all'eredità:

Composizione: la funzionalità di un oggetto è costituita da un aggregato di classi diverse contenente istanze di altri oggetti. Ereditarietà - La funzionalità di un oggetto è costituita dalla sua funzionalità più funzionalità dalle sue classi genitore. Se devi avere l'eredità, usa il vecchio JS semplice

Se devi implementare l'ereditarietà, evita almeno di usare un'altra implementazione / funzione magica non standard. Ecco come implementare un ragionevole facsimile di eredità in ES3 puro (a patto di seguire la regola di non definire mai le proprietà sui prototipi):

function Animal(name) {
  this.name = name;
};
Animal.prototype.move = function(meters) {
  console.log(this.name+" moved "+meters+"m.");
};

function Snake() {
  Animal.apply(this, Array.prototype.slice.call(arguments));
};
Snake.prototype = new Animal();
Snake.prototype.move = function() {
  console.log("Slithering...");
  Animal.prototype.move.call(this, 5);
};

var sam = new Snake("Sammy the Python");
sam.move();

Questa non è la stessa cosa dell'ereditarietà classica, ma è Javascript standard e comprensibile e ha le funzionalità che le persone cercano principalmente: costruttori concatenabili e la capacità di chiamare metodi della superclasse.


4

ES6 classeextends

ES6 classe extendssono solo zucchero sintattico per la manipolazione della catena di prototipi precedentemente possibile, e quindi probabilmente la configurazione più canonica.

Per prima cosa, scopri di più sulla catena di prototipi e sulla .ricerca di proprietà su: https://stackoverflow.com/a/23877420/895245

Ora decostruiamo cosa succede:

class C {
    constructor(i) {
        this.i = i
    }
    inc() {
        return this.i + 1
    }
}

class D extends C {
    constructor(i) {
        super(i)
    }
    inc2() {
        return this.i + 2
    }
}
// Inheritance syntax works as expected.
(new C(1)).inc() === 2
(new D(1)).inc() === 2
(new D(1)).inc2() === 3
// "Classes" are just function objects.
C.constructor === Function
C.__proto__ === Function.prototype
D.constructor === Function
// D is a function "indirectly" through the chain.
D.__proto__ === C
D.__proto__.__proto__ === Function.prototype
// "extends" sets up the prototype chain so that base class
// lookups will work as expected
var d = new D(1)
d.__proto__ === D.prototype
D.prototype.__proto__ === C.prototype
// This is what `d.inc` actually does.
d.__proto__.__proto__.inc === C.prototype.inc
// Class variables
// No ES6 syntax sugar apparently:
// /programming/22528967/es6-class-variable-alternatives
C.c = 1
C.c === 1
// Because `D.__proto__ === C`.
D.c === 1
// Nothing makes this work.
d.c === undefined

Diagramma semplificato senza tutti gli oggetti predefiniti:

      __proto__
(C)<---------------(D)         (d)
| |                |           |
| |                |           |
| |prototype       |prototype  |__proto__
| |                |           |
| |                |           |
| |                | +---------+
| |                | |
| |                | |
| |                v v
|__proto__        (D.prototype)
| |                |
| |                |
| |                |__proto__
| |                |
| |                |
| | +--------------+
| | |
| | |
| v v
| (C.prototype)--->(inc)
|
v
Function.prototype


1

I migliori esempi che ho visto sono in JavaScript: The Good Parts di Douglas Crockford . Vale sicuramente la pena acquistarlo per ottenere una visione equilibrata della lingua.

Douglas Crockford è responsabile del formato JSON e lavora in Yahoo come guru di JavaScript.


7
responsabile? suona quasi come "colpevole di" :)
Roland Bouman

@Roland Penso che JSON sia un bel formato non dettagliato per la memorizzazione dei dati. Sicuramente non l'ha inventato, il formato era lì per le impostazioni di configurazione in Steam nel 2002
Chris S

Chris S, lo penso anch'io - Sempre più spesso vorrei che avessimo potuto saltare tutti XML come formato di scambio e passare subito a JSON.
Roland Bouman

3
Non c'è molto da inventare: JSON è un sottoinsieme della sintassi letterale dell'oggetto di JavaScript, presente nella lingua dal 1997 circa.
Tim Down

@ Time good point - Non mi ero reso conto che fosse lì dall'inizio
Chris S


0

Aggiunta di un esempio di ereditarietà basata su prototipi in Javascript.

// Animal Class
function Animal (name, energy) {
  this.name = name;
  this.energy = energy;
}

Animal.prototype.eat = function (amount) {
  console.log(this.name, "eating. Energy level: ", this.energy);
  this.energy += amount;
  console.log(this.name, "completed eating. Energy level: ", this.energy);
}

Animal.prototype.sleep = function (length) {
  console.log(this.name, "sleeping. Energy level: ", this.energy);
  this.energy -= 1;
  console.log(this.name, "completed sleeping. Energy level: ", this.energy);
}

Animal.prototype.play = function (length) {
  console.log(this.name, " playing. Energy level: ", this.energy);
  this.energy -= length;
  console.log(this.name, "completed playing. Energy level: ", this.energy);
}

// Dog Class
function Dog (name, energy, breed) {
  Animal.call(this, name, energy);
  this.breed = breed;
}

Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;

Dog.prototype.bark = function () {
  console.log(this.name, "barking. Energy level: ", this.energy);
  this.energy -= 1;
  console.log(this.name, "done barking. Energy level: ", this.energy);
}

Dog.prototype.showBreed = function () {
  console.log(this.name,"'s breed is ", this.breed);
}

// Cat Class
function Cat (name, energy, male) {
  Animal.call(this, name, energy);
  this.male = male;
}

Cat.prototype = Object.create(Animal.prototype);
Cat.prototype.constructor = Cat;

Cat.prototype.meow = function () {
  console.log(this.name, "meowing. Energy level: ", this.energy);
  this.energy -= 1;
  console.log(this.name, "done meowing. Energy level: ", this.energy);
}

Cat.prototype.showGender = function () {
  if (this.male) {
    console.log(this.name, "is male.");
  } else {
    console.log(this.name, "is female.");
  }
}

// Instances
const charlie = new Dog("Charlie", 10, "Labrador");
charlie.bark();
charlie.showBreed();

const penny = new Cat("Penny", 8, false);
penny.meow();
penny.showGender();

ES6 utilizza un'implementazione molto più semplice dell'ereditarietà con l'uso di parole chiave costruttore e super.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.