Javascript quando usar protótipos

93

Eu gostaria de entender quando é apropriado usar métodos de protótipo em js. Eles sempre devem ser usados? Ou há casos em que usá-los não é preferido e / ou incorre em uma penalidade de desempenho?

Ao pesquisar neste site os métodos comuns para namespacing em js, parece que a maioria usa uma implementação não baseada em protótipo: simplesmente usando um objeto ou um objeto de função para encapsular um namespace.

Vindo de uma linguagem baseada em classes, é difícil não tentar traçar paralelos e pensar que os protótipos são como "classes" e as implementações de namespace que mencionei são como métodos estáticos.

opl
fonte

Respostas:

133

Protótipos são uma otimização .

Um ótimo exemplo de como usá-los bem é a biblioteca jQuery. Cada vez que você obtém um objeto jQuery usando $('.someClass'), esse objeto tem dezenas de "métodos". A biblioteca pode conseguir isso retornando um objeto:

return {
   show: function() { ... },
   hide: function() { ... },
   css: function() { ... },
   animate: function() { ... },
   // etc...
};

Mas isso significaria que cada objeto jQuery na memória teria dezenas de slots nomeados contendo os mesmos métodos, continuamente.

Em vez disso, esses métodos são definidos em um protótipo e todos os objetos jQuery "herdam" esse protótipo para obter todos esses métodos com um custo de tempo de execução muito pequeno.

Uma parte vitalmente importante de como o jQuery acerta é que isso é escondido do programador. É tratado puramente como uma otimização, não como algo com que você precisa se preocupar ao usar a biblioteca.

O problema com o JavaScript é que as funções do construtor simples exigem que o chamador se lembre de prefixá-las com newou de outra forma normalmente não funcionam. Não há um bom motivo para isso. O jQuery acerta ao esconder esse absurdo atrás de uma função comum $, então você não precisa se preocupar com a forma como os objetos são implementados.

Para que você possa criar convenientemente um objeto com um protótipo especificado, o ECMAScript 5 inclui uma função padrão Object.create. Uma versão bastante simplificada ficaria assim:

Object.create = function(prototype) {
    var Type = function () {};
    Type.prototype = prototype;
    return new Type();
};

Ele apenas cuida da dor de escrever uma função de construtor e, em seguida, chamá-la com new.

Quando você evitaria protótipos?

Uma comparação útil é com linguagens OO populares, como Java e C #. Eles suportam dois tipos de herança:

  • interface de herança, onde implementuma interfacetal forma que a classe fornece sua própria implementação exclusivo para cada membro da interface.
  • implementação de herança, onde extendum classque fornece implementações padrão de alguns métodos.

Em JavaScript, a herança prototípica é um tipo de herança de implementação . Portanto, nas situações em que (em C # ou Java) você teria derivado de uma classe base para obter o comportamento padrão, no qual você faria pequenas modificações por meio de substituições, em JavaScript, a herança prototípica faz sentido.

No entanto, se você estiver em uma situação em que usaria interfaces em C # ou Java, não precisará de nenhum recurso de linguagem específico em JavaScript. Não há necessidade de declarar explicitamente algo que representa a interface e não há necessidade de marcar objetos como "implementando" essa interface:

var duck = {
    quack: function() { ... }
};

duck.quack(); // we're satisfied it's a duck!

Em outras palavras, se cada "tipo" de objeto tem suas próprias definições de "métodos", então não há valor em herdar de um protótipo. Depois disso, depende de quantas instâncias você aloca de cada tipo. Mas em muitos projetos modulares, existe apenas uma instância de um determinado tipo.

E, de fato, tem sido sugerido por muitas pessoas que a herança de implementação é má . Ou seja, se houver algumas operações comuns para um tipo, então talvez seja mais claro se elas não forem colocadas em uma classe base / super, mas apenas expostas como funções comuns em algum módulo, para o qual você passa o (s) objeto (s) você quer que eles operem.

Daniel Earwicker
fonte
1
Boa explicação. Então, você concorda que, por considerar protótipos uma otimização, eles sempre podem ser usados ​​para melhorar seu código? Estou me perguntando se há casos em que o uso de protótipos não faz sentido ou realmente incorre em uma penalidade de desempenho.
opl
Em seu acompanhamento, você menciona que "depende de quantas instâncias você aloca de cada tipo". Mas o exemplo ao qual você se refere não está usando protótipos. Onde está a noção de alocar uma instância (você ainda estaria usando "novo" aqui)? Além disso: digamos que o método quack tivesse um parâmetro - cada invocação de duck.quack (param) faria com que um novo objeto fosse criado na memória (talvez seja irrelevante se ele tem um parâmetro ou não)?
opl
3
1. Eu quis dizer que, se houvesse um grande número de instâncias de um tipo de pato, faria sentido modificar o exemplo para que a quackfunção estivesse em um protótipo, ao qual as muitas instâncias de pato estão vinculadas. 2. A sintaxe literal do objeto { ... }cria uma instância (não há necessidade de usar newcom ela). 3. Chamar qualquer função JS faz com que pelo menos um objeto seja criado na memória - é chamado de argumentsobjeto e armazena os argumentos passados ​​na chamada: developer.mozilla.org/en/JavaScript/Reference/…
Daniel Earwicker
Obrigado, aceitei sua resposta. Mas ainda tenho uma pequena confusão com seu ponto (1): não estou entendendo o que você quer dizer com "grande número de ocorrências de um tipo de pato". Como você disse em (3), cada vez que você chama uma função JS, um objeto é criado na memória - então, mesmo se você tivesse apenas um tipo de pato, você não estaria alocando memória cada vez que chamar uma função de pato (em caso em que sempre faria sentido usar um protótipo)?
opl
11
1 A comparação com jQuery foi a primeira explicação clara e concisa de quando e por que usar protótipos que li. Muito obrigado.
GFoley83
46

Você deve usar protótipos se desejar declarar um método "não estático" do objeto.

var myObject = function () {

};

myObject.prototype.getA = function (){
  alert("A");
};

myObject.getB = function (){
  alert("B");
};

myObject.getB();  // This works fine

myObject.getA();  // Error!

var myPrototypeCopy = new myObject();
myPrototypeCopy.getA();  // This works, too.
KeatsKelleher
fonte
@keatsKelleher mas podemos criar um método não estático para o objeto apenas definindo o método dentro da função construtora usando o thisexemplo this.getA = function(){alert("A")}certo?
Amr Labib
17

Um motivo para usar o prototypeobjeto embutido é se você for duplicar um objeto várias vezes que compartilhará uma funcionalidade comum. Ao anexar métodos ao protótipo, você pode economizar na duplicação de métodos criados para cada newinstância. Mas quando você anexa um método ao prototype, todas as instâncias terão acesso a esses métodos.

Digamos que você tenha uma Car()classe / objeto base .

function Car() {
    // do some car stuff
}

em seguida, você cria várias Car()instâncias.

var volvo = new Car(),
    saab = new Car();

Agora, você sabe que cada carro precisará dirigir, ligar, etc. Em vez de anexar um método diretamente à Car()classe (que ocupa memória para cada instância criada), você pode anexar os métodos ao protótipo (criando apenas os métodos uma vez), dando acesso a esses métodos tanto para o novo volvoquanto para saab.

// just mapping for less typing
Car.fn = Car.prototype;

Car.fn.drive = function () {
    console.log("they see me rollin'");
};
Car.fn.honk = function () {
    console.log("HONK!!!");
}

volvo.honk();
// => HONK!!!
saab.drive();
// => they see me rollin'
Hellatan
fonte
2
na verdade, isso está incorreto. volvo.honk () não funcionará porque você substituiu completamente o objeto de protótipo, não o estendeu. Se você fizesse algo assim, funcionaria como você espera: Car.prototype.honk = function () {console.log ('HONK');} volvo.honk (); // 'HONK'
29 de
1
@ 29er - da maneira como escrevi este exemplo, você está correto. A ordem importa. Se eu mantivesse este exemplo como está, o Car.prototype = { ... }teria que vir antes de chamar um, new Car()conforme ilustrado neste jsfiddle: jsfiddle.net/mxacA . Quanto ao seu argumento, esta seria a maneira correta de fazê-lo: jsfiddle.net/Embnp . O engraçado é que não me lembro de responder a esta pergunta =)
hellatan
@hellatan você pode consertar isso configurando constructor: Car para, uma vez que você substituiu a propriedade prototype com um literal de objeto.
Josh Bedo
@josh obrigado por apontar isso. Atualizei minha resposta para não sobrescrever o protótipo com um objeto literal, como deveria ter sido desde o início.
hellatan
12

Coloque funções em um objeto de protótipo quando for criar muitas cópias de um tipo específico de objeto e todos eles precisam compartilhar comportamentos comuns. Ao fazer isso, você economizará memória tendo apenas uma cópia de cada função, mas esse é apenas o benefício mais simples.

Alterar métodos em objetos de protótipo, ou adicionar métodos, muda instantaneamente a natureza de todas as instâncias do (s) tipo (s) correspondente (s).

Agora, exatamente por que você faria todas essas coisas é principalmente uma função do design de seu próprio aplicativo e dos tipos de coisas que você precisa fazer no código do lado do cliente. (Uma história totalmente diferente seria o código dentro de um servidor; muito mais fácil imaginar fazer código "OO" em grande escala lá.)

Pontudo
fonte
então, quando eu instanciar um novo objeto com métodos de protótipo (por meio de nova palavra-chave), esse objeto não obtém uma nova cópia de cada função (apenas uma espécie de ponteiro)? Se for esse o caso, por que você não gostaria de usar um protótipo?
opl
como @marcel, d'oh ... =)
hellatan
@opi sim, você está certo - nenhuma cópia foi feita. Em vez disso, os símbolos (nomes de propriedades) no objeto de protótipo estão naturalmente "lá" como partes virtuais de cada objeto de instância. A única razão pela qual as pessoas não gostariam de se preocupar com isso seriam os casos em que os objetos têm vida curta e são distintos, ou onde não há muito "comportamento" para compartilhar.
Pointy
3

Se eu explicar em termos de classe, então Person é classe, walk () é o método Prototype. Portanto, walk () terá sua existência somente após você instanciar um novo objeto com this.

Portanto, se você deseja criar as cópias de um objeto como Pessoa, você pode criar muitos usuários, o Protótipo é uma boa solução, pois economiza memória compartilhando / herdando a mesma cópia de função para cada um dos objetos na memória.

Considerando que estática não é uma grande ajuda em tal cenário.

function Person(){
this.name = "anonymous";
}

// its instance method and can access objects data data 
Person.prototype.walk = function(){
alert("person has started walking.");
}
// its like static method
Person.ProcessPerson = function(Person p){
alert("Persons name is = " + p.name);
}

var userOne = new Person();
var userTwo = new Person();

//Call instance methods
userOne.walk();

//Call static methods
Person.ProcessPerson(userTwo);

Então, com isso é mais como um método de instância. A abordagem do objeto é semelhante a métodos estáticos.

https://developer.mozilla.org/en/Introduction_to_Object-Oriented_JavaScript

Anil Namde
fonte