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?


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.


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.


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

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.


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.


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


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.


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

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.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.