Existem dois modelos para implementar classes e instâncias no JavaScript: a forma de prototipagem e a forma de fechamento. Ambos têm vantagens e desvantagens, e há muitas variações estendidas. Muitos programadores e bibliotecas têm abordagens diferentes e funções utilitárias de manipulação de classes para encobrir algumas das partes mais feias da linguagem.
O resultado é que, em empresas mistas, você terá uma mistura de metaclasses, todos se comportando de maneira um pouco diferente. O que é pior, a maioria dos materiais de tutoriais sobre JavaScript é terrível e serve como uma espécie de compromisso intermediário para cobrir todas as bases, deixando você muito confuso. (Provavelmente o autor também está confuso. O modelo de objeto do JavaScript é muito diferente da maioria das linguagens de programação e, em muitos lugares, mal projetado.)
Vamos começar com o protótipo . Este é o mais nativo do JavaScript que você pode obter: há um mínimo de código adicional e instanceof funcionará com instâncias desse tipo de objeto.
function Shape(x, y) {
this.x= x;
this.y= y;
}
Podemos adicionar métodos à instância criada new Shape
escrevendo-os na prototype
pesquisa dessa função construtora:
Shape.prototype.toString= function() {
return 'Shape at '+this.x+', '+this.y;
};
Agora, para subclassificá-lo, o máximo que você pode chamar de JavaScript faz subclassificação. Fazemos isso substituindo completamente essa prototype
propriedade mágica estranha :
function Circle(x, y, r) {
Shape.call(this, x, y); // invoke the base class's constructor function to take co-ords
this.r= r;
}
Circle.prototype= new Shape();
antes de adicionar métodos a ele:
Circle.prototype.toString= function() {
return 'Circular '+Shape.prototype.toString.call(this)+' with radius '+this.r;
}
Este exemplo funcionará e você verá o código semelhante em muitos tutoriais. Mas cara, isso new Shape()
é feio: estamos instanciando a classe base, mesmo que nenhuma Forma real seja criada. Isso acontece com o trabalho neste caso simples, porque o JavaScript é tão desleixada: permite zero de argumentos a serem passados em, caso em que x
e y
tornam-se undefined
e são atribuídos ao protótipo de this.x
e this.y
. Se a função construtora estivesse fazendo algo mais complicado, ela cairia de cara no chão.
Portanto, o que precisamos fazer é encontrar uma maneira de criar um objeto protótipo que contenha os métodos e outros membros que desejamos no nível da classe, sem chamar a função construtora da classe base. Para fazer isso, teremos que começar a escrever o código auxiliar. Esta é a abordagem mais simples que conheço:
function subclassOf(base) {
_subclassOf.prototype= base.prototype;
return new _subclassOf();
}
function _subclassOf() {};
Isso transfere os membros da classe base em seu protótipo para uma nova função construtora que não faz nada e depois usa esse construtor. Agora podemos escrever simplesmente:
function Circle(x, y, r) {
Shape.call(this, x, y);
this.r= r;
}
Circle.prototype= subclassOf(Shape);
em vez do new Shape()
erro. Agora temos um conjunto aceitável de primitivas para as classes construídas.
Existem alguns refinamentos e extensões que podemos considerar nesse modelo. Por exemplo, aqui está uma versão sintática do açúcar:
Function.prototype.subclass= function(base) {
var c= Function.prototype.subclass.nonconstructor;
c.prototype= base.prototype;
this.prototype= new c();
};
Function.prototype.subclass.nonconstructor= function() {};
...
function Circle(x, y, r) {
Shape.call(this, x, y);
this.r= r;
}
Circle.subclass(Shape);
Qualquer uma das versões tem a desvantagem de que a função construtora não pode ser herdada, como ocorre em muitos idiomas. Portanto, mesmo que sua subclasse não adicione nada ao processo de construção, lembre-se de chamar o construtor de base com quaisquer argumentos que a base desejasse. Isso pode ser um pouco automatizado apply
, mas você ainda precisa escrever:
function Point() {
Shape.apply(this, arguments);
}
Point.subclass(Shape);
Portanto, uma extensão comum é dividir o material de inicialização em sua própria função, e não no próprio construtor. Esta função pode herdar da base muito bem:
function Shape() { this._init.apply(this, arguments); }
Shape.prototype._init= function(x, y) {
this.x= x;
this.y= y;
};
function Point() { this._init.apply(this, arguments); }
Point.subclass(Shape);
// no need to write new initialiser for Point!
Agora, temos o mesmo padrão de função de construtor para cada classe. Talvez possamos colocar isso em sua própria função auxiliar, para que não tenhamos que continuar digitando, por exemplo, em vez de Function.prototype.subclass
girá-lo e deixar que a Função da classe base cuspa subclasses:
Function.prototype.makeSubclass= function() {
function Class() {
if ('_init' in this)
this._init.apply(this, arguments);
}
Function.prototype.makeSubclass.nonconstructor.prototype= this.prototype;
Class.prototype= new Function.prototype.makeSubclass.nonconstructor();
return Class;
};
Function.prototype.makeSubclass.nonconstructor= function() {};
...
Shape= Object.makeSubclass();
Shape.prototype._init= function(x, y) {
this.x= x;
this.y= y;
};
Point= Shape.makeSubclass();
Circle= Shape.makeSubclass();
Circle.prototype._init= function(x, y, r) {
Shape.prototype._init.call(this, x, y);
this.r= r;
};
... que está começando a parecer um pouco mais com outros idiomas, embora com sintaxe um pouco desajeitada. Você pode polvilhar alguns recursos extras, se quiser. Talvez você queira makeSubclass
pegar e lembrar um nome de classe e fornecer um padrão toString
usando-o. Talvez você queira que o construtor detecte quando foi chamado acidentalmente sem o new
operador (o que normalmente resultaria em depuração muito irritante):
Function.prototype.makeSubclass= function() {
function Class() {
if (!(this instanceof Class))
throw('Constructor called without "new"');
...
Talvez você queira passar todos os novos membros e makeSubclass
adicioná-los ao protótipo, para poupar a necessidade de escrever Class.prototype...
muito. Muitos sistemas de classe fazem isso, por exemplo:
Circle= Shape.makeSubclass({
_init: function(x, y, z) {
Shape.prototype._init.call(this, x, y);
this.r= r;
},
...
});
Existem muitos recursos em potencial que você pode considerar desejáveis em um sistema de objetos e ninguém realmente concorda com uma fórmula específica.
O caminho do fechamento , então. Isso evita os problemas da herança baseada em protótipo do JavaScript, ao não usar a herança. Em vez de:
function Shape(x, y) {
var that= this;
this.x= x;
this.y= y;
this.toString= function() {
return 'Shape at '+that.x+', '+that.y;
};
}
function Circle(x, y, r) {
var that= this;
Shape.call(this, x, y);
this.r= r;
var _baseToString= this.toString;
this.toString= function() {
return 'Circular '+_baseToString(that)+' with radius '+that.r;
};
};
var mycircle= new Circle();
Agora, cada instância única Shape
terá sua própria cópia do toString
método (e quaisquer outros métodos ou outros membros da classe que adicionarmos).
O lado ruim de toda instância ter sua própria cópia de cada aluno é que é menos eficiente. Se você estiver lidando com um grande número de instâncias subclassificadas, a herança prototípica poderá atendê-lo melhor. Também chamar um método da classe base é um pouco irritante, como você pode ver: precisamos lembrar qual era o método antes que o construtor da subclasse o substituísse ou se perdesse.
[Também porque não há herança aqui, o instanceof
operador não funcionará; você precisaria fornecer seu próprio mecanismo de detecção de classe, se necessário. Embora você possa mexer nos objetos do protótipo de maneira semelhante à herança do protótipo, é um pouco complicado e realmente não vale a pena apenas para começar a instanceof
trabalhar.]
O bom de toda instância ter seu próprio método é que o método pode ser vinculado à instância específica que o possui. Isso é útil devido à maneira estranha de ligação do JavaScript this
nas chamadas de método, que tem o resultado de que, se você desanexar um método do proprietário:
var ts= mycircle.toString;
alert(ts());
então, this
dentro do método, a instância Circle não será a esperada (na verdade, será o window
objeto global , causando um problema de depuração generalizado). Na realidade, isso normalmente acontece quando um método é usado e atribuído a um setTimeout
, onclick
ou EventListener
em geral.
Com o protótipo, você deve incluir um fechamento para cada tarefa:
setTimeout(function() {
mycircle.move(1, 1);
}, 1000);
ou, no futuro (ou agora, se você hackear Function.prototype), você também poderá fazê-lo com function.bind()
:
setTimeout(mycircle.move.bind(mycircle, 1, 1), 1000);
se suas instâncias são feitas da maneira de fechamento, a ligação é feita gratuitamente pelo fechamento da variável de instância (geralmente chamada that
ou self
, embora pessoalmente eu recomendaria contra essa última, self
já que ela tem outro significado diferente em JavaScript). No 1, 1
entanto, você não obtém os argumentos do snippet acima gratuitamente, portanto ainda precisaria de outro fechamento ou a, bind()
se precisar fazer isso.
Existem muitas variantes no método de fechamento também. Você pode preferir omitir this
completamente, criando um novo that
e retornando-o em vez de usar o new
operador:
function Shape(x, y) {
var that= {};
that.x= x;
that.y= y;
that.toString= function() {
return 'Shape at '+that.x+', '+that.y;
};
return that;
}
function Circle(x, y, r) {
var that= Shape(x, y);
that.r= r;
var _baseToString= that.toString;
that.toString= function() {
return 'Circular '+_baseToString(that)+' with radius '+r;
};
return that;
};
var mycircle= Circle(); // you can include `new` if you want but it won't do anything
Qual o caminho é "adequado"? Ambos. Qual é melhor"? Isso depende da sua situação. FWIW: Eu tendem a criar protótipos para herança de JavaScript real quando estou fazendo coisas fortemente com OO e fechamentos para simples efeitos de página descartáveis.
Mas ambas as formas são bastante contra-intuitivas para a maioria dos programadores. Ambos têm muitas variações confusas em potencial. Você encontrará ambos (assim como muitos esquemas intermediários e geralmente interrompidos) se usar o código / biblioteca de outras pessoas. Não existe uma resposta geralmente aceita. Bem-vindo ao maravilhoso mundo dos objetos JavaScript.
[Isso foi parte 94 de Por que o JavaScript não é minha linguagem de programação favorita.]