Bom exemplo de herança baseada em protótipo do JavaScript

89

Eu tenho programado com linguagens OOP por mais de 10 anos, mas estou aprendendo JavaScript agora e é a primeira vez que encontro herança baseada em protótipo. Eu tendo a aprender mais rápido estudando um bom código. Qual é um exemplo bem escrito de um aplicativo (ou biblioteca) JavaScript que usa apropriadamente a herança prototípica? E você pode descrever (brevemente) como / onde a herança prototípica é usada, para que eu saiba por onde começar a ler?

Alex Reisner
fonte
1
Você teve a chance de verificar aquela biblioteca Base? É realmente bom e bem pequeno. Se você gostar, considere marcar minha resposta como a resposta. TIA, roland.
Roland Bouman
Acho que estou no mesmo barco que você. Quero, também, aprender um pouco sobre essa linguagem prototípica, não me restringindo apenas a frameworks oop ou similares, mesmo sendo ótimos e tal, precisamos aprender, certo? Não é apenas uma estrutura que faz isso por mim, mesmo que eu vá usá-la. Mas aprenda como criar coisas novas em novas linguagens com novas maneiras, pense fora da caixa. Eu gosto do seu estilo. Vou tentar me ajudar e talvez ajudar você. Assim que eu encontrar algo, avisarei você.
marcelo-ferraz

Respostas:

48

Douglas Crockford tem uma boa página sobre herança de protótipo de JavaScript :

Cinco anos atrás, escrevi Herança clássica em JavaScript. Ele mostrou que o JavaScript é uma linguagem prototípica sem classes e que tem poder expressivo suficiente para simular um sistema clássico. Meu estilo de programação evoluiu desde então, como qualquer bom programador deveria. Aprendi a abraçar totalmente o prototipismo e me libertei dos limites do modelo clássico.

Dean Edward's Base.js , Mootools's Class ou John Resig's Simple Inheritance são maneiras de fazer herança clássica em JavaScript.

Gregory Pakosz
fonte
Por que não simplesmente newObj = Object.create(oldObj);se você quiser sem aulas? Caso contrário, substituir com oldObjpelo objeto de protótipo da função do construtor deve funcionar?
Cyker
76

Conforme mencionado, os filmes de Douglas Crockford dão uma boa explicação sobre o porquê e cobre o como. Mas, para colocá-lo em algumas linhas de 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'
);

O problema com essa abordagem, entretanto, é que ela recriará o objeto sempre que você criar um. Outra abordagem é declarar seus objetos na pilha de protótipo, assim:

// 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');

Existe uma pequena desvantagem quando se trata de introspecção. O despejo do testOne resultará em informações menos úteis. Além disso, a propriedade privada "privateVariable" em "testOne" é compartilhada em todas as instâncias, também mencionada de maneira útil nas respostas de shesek.

Dynom
fonte
3
Observe que testOne privateVariableé simplesmente uma variável no escopo do IIFE e é compartilhada por todas as instâncias, portanto, você não deve armazenar dados específicos da instância nela. (no testTwo é específico da instância, já que cada chamada para testTwo () cria um novo escopo por instância)
shesek
Eu votei porque você mostrou a outra abordagem e por que não usá-la porque faz cópias
Murphy316
O problema de recriar o objeto todas as vezes é principalmente devido aos métodos sendo recriados para cada novo objeto. No entanto, podemos mitigar o problema definindo o método em Dog.prototype. Portanto, em vez de usar this.bark = function () {...}, podemos fazer Dot.prototype.bark = function () {...}fora da Dogfunção. (Veja mais detalhes nesta resposta )
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);
Vlad Bezden
fonte
3
Talvez adicionar este link com sua resposta complete o quadro ainda mais: developer.mozilla.org/en/docs/Web/JavaScript/Reference/…
Dynom
14

Eu daria uma olhada na YUI e na Basebiblioteca de Dean Edward : http://dean.edwards.name/weblog/2006/03/base/

Para YUI, você pode dar uma olhada rápida no módulo lang , esp. o método YAHOO.lang.extend . E então, você pode navegar pela fonte de alguns widgets ou utilitários e ver como eles usam esse método.

Roland Bouman
fonte
YUI 2 foi preterido em 2011, então o link para langestá parcialmente quebrado. Alguém se preocupa em consertá-lo para YUI 3?
ack
lang no yui 3 não parece ter um método extend. mas como a resposta pretende usar a implementação como exemplo, a versão não importa.
eMBee de
4

Este é o exemplo mais claro que encontrei, do livro Mixu's Node ( http://book.mixu.net/node/ch6.html ):

Eu sou a favor da composição em vez da herança:

Composição - A funcionalidade de um objeto é composta de um agregado de diferentes classes, contendo instâncias de outros objetos. Herança - a funcionalidade de um objeto é composta de sua própria funcionalidade mais a funcionalidade de suas classes pai. Se você deve ter herança, use JS simples

Se você deve implementar herança, pelo menos evite usar outra implementação não padrão / função mágica. Aqui está como você pode implementar um fac-símile de herança razoável no ES3 puro (contanto que você siga a regra de nunca definir propriedades em protótipos):

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();

Isso não é a mesma coisa que herança clássica - mas é um Javascript padrão, compreensível e tem a funcionalidade que as pessoas mais procuram: construtores encadeados e a capacidade de chamar métodos da superclasse.

supershnee
fonte
4

ES6 classeextends

ES6 classe extendssão apenas açúcar de sintaxe para manipulação de cadeia de protótipo anteriormente possível e, portanto, sem dúvida a configuração mais canônica.

Primeiro, aprenda mais sobre a cadeia de protótipo e .pesquisa de propriedade em: https://stackoverflow.com/a/23877420/895245

Agora vamos desconstruir o que acontece:

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

Diagrama simplificado sem todos os objetos predefinidos:

      __proto__
(C)<---------------(D)         (d)
| |                |           |
| |                |           |
| |prototype       |prototype  |__proto__
| |                |           |
| |                |           |
| |                | +---------+
| |                | |
| |                | |
| |                v v
|__proto__        (D.prototype)
| |                |
| |                |
| |                |__proto__
| |                |
| |                |
| | +--------------+
| | |
| | |
| v v
| (C.prototype)--->(inc)
|
v
Function.prototype
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
fonte
1

Os melhores exemplos que vi estão em JavaScript: The Good Parts de Douglas Crockford . Definitivamente, vale a pena comprar para ajudá-lo a ter uma visão equilibrada do idioma.

Douglas Crockford é responsável pelo formato JSON e trabalha no Yahoo como um guru do JavaScript.

Chris S
fonte
7
responsável? isso soa quase como "culpado de" :)
Roland Bouman
@Roland Eu acho que JSON é um formato não verboso bastante bom para armazenar dados. Ele definitivamente não o inventou, o formato estava lá para as configurações do Steam em 2002
Chris S
Chris S, também acho - Cada vez com mais frequência, gostaria que todos pudéssemos ter pulado o XML como formato de troca e mudado para o JSON imediatamente.
Roland Bouman
3
Não há muito a inventar: JSON é um subconjunto da sintaxe literal de objeto do próprio JavaScript, que está na linguagem desde cerca de 1997.
Tim Down
@Time bom ponto - eu não sabia que estava lá desde o início
Chris S
0

Há um fragmento de herança baseada em protótipo de JavaScript com implementações específicas de versão ECMAScript. Ele escolherá automaticamente qual usar entre as implementações ES6, ES5 e ES3 de acordo com o tempo de execução atual.

Fuweichin
fonte
0

Adicionando um exemplo de herança baseada em protótipo em 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 usa uma implementação muito mais fácil de herança com o uso de construtor e super palavras-chave.


fonte