Implementação do Mecanismo de Herança ECMAScript

Implementação do mecanismo de herança

Para implementar o mecanismo de herança no ECMAScript, você pode começar com a classe base que deseja herdar. Todas as classes definidas pelos desenvolvedores podem ser classes base. Por razões de segurança, classes locais e classes anfitriãs não podem ser classes base, para evitar o acesso público ao código compilado de nível do navegador, que pode ser usado para ataques maliciosos.

Após selecionar a classe base, você pode criar suas subclasses. A decisão de usar a classe base é completamente sua. Às vezes, você pode querer criar uma classe base que não pode ser usada diretamente, que apenas fornece funções genéricas para subclasses. Neste caso, a classe base é considerada uma classe abstrata.

Embora o ECMAScript não defina de maneira tão rigorosa a classe abstrata como outros idiomas, às vezes ele realmente cria algumas classes que não podem ser usadas. Geralmente, chamamos essas classes de classes abstratas.

A classe derivada herdará todas as propriedades e métodos da classe superior, incluindo a implementação do construtor e métodos. Lembre-se, todas as propriedades e métodos são públicos, portanto, a classe derivada pode acessar diretamente esses métodos. A classe derivada também pode adicionar novas propriedades e métodos que não existem na classe superior, e pode sobrescrever as propriedades e métodos da classe superior.

Formas de herança

Como outras funcionalidades, há mais de uma maneira de implementar a herança no ECMAScript. Isso porque o mecanismo de herança no JavaScript não é明确规定,mas sim implementado por imitação. Isso significa que todos os detalhes da herança não são completamente processados pelo interpretador. Como desenvolvedor, você tem o direito de decidir o modo mais apropriado de herança.

A seguir, apresentaremos várias formas específicas de herança.

Objecto simulado

Ao conceber o ECMAScript original, não havia a intenção de projetar o objecto simulado (object masquerading). Isso foi desenvolvido apenas quando os desenvolvedores começaram a entender como as funções funcionam, especialmente como usar a palavra-chave this no ambiente da função.

O princípio é o seguinte: o construtor usa a palavra-chave this para atribuir todos os atributos e métodos (isto é, usando o construtor declarado pela classe). Como o construtor é apenas uma função, é possível fazer com que o construtor da ClassA seja um método da ClassB, e chamá-lo. A ClassB receberá os atributos e métodos definidos no construtor da ClassA. Por exemplo, defina ClassA e ClassB da seguinte maneira:

function ClassA(sColor) {
    this.color = sColor;
    this.sayColor = function () {
        alert(this.color);
    };
}
function ClassB(sColor) {
}

Lembras? A palavra-chave this se refere ao objeto que está sendo criado pelo construtor atual. No entanto, neste método, this se refere ao objeto do qual pertence. Este princípio é construir a ClassA como uma função comum para estabelecer o mecanismo de herança, em vez de como um construtor. A seguir, é possível implementar o mecanismo de herança usando o construtor ClassB:

function ClassB(sColor) {
    this.newMethod = ClassA;
    this.newMethod(sColor);
    delete this.newMethod;
}

Neste código, o método newMethod foi atribuído à ClassA (lembrando que o nome da função é apenas um ponteiro para ela). Em seguida, foi chamado esse método, passando-lhe os parâmetros do construtor da ClassB sColor. O último código removeu a referência à ClassA, então não será possível chamá-la mais.

Todas as novas propriedades e métodos devem ser definidos após a remoção da linha de código do novo método. Caso contrário, pode haver sobreescrita de propriedades e métodos da superclasse relevantes:

function ClassB(sColor, sName) {
    this.newMethod = ClassA;
    this.newMethod(sColor);
    delete this.newMethod;
    this.name = sName;
    this.sayName = function () {
        alert(this.name);
    };
}

Para provar a validade do código anterior, pode rodar o seguinte exemplo:

var objA = new ClassA("blue");
var objB = new ClassB("red", "John");
objA.sayColor(); // Saída "blue"
objB.sayColor(); // Saída "red"
objB.sayName();		// Saída "John"

O objecto simulado pode implementar a herança múltipla

Curiosamente, o objecto simulado pode suportar a herança múltipla. Isso significa que uma classe pode herdar várias superclasses. O mecanismo de herança múltipla representado no UML é como na figura a seguir:

Exemplo de Diagrama UML de Mecanismo de Herança

Por exemplo, se existirem duas classes ClassX e ClassY, e a ClassZ quiser herdar essas duas classes, pode usar o seguinte código:

function ClassZ() {
    this.newMethod = ClassX;
    this.newMethod();
    delete this.newMethod;
    this.newMethod = ClassY;
    this.newMethod();
    delete this.newMethod;
}

TIY

这里存在一个弊端,如果存在两个类 ClassX 和 ClassY 具有同名的属性或方法,ClassY 具有高优先级。因为它从后面的类继承。除这点小问题之外,用对象冒充实现多重继承机制轻而易举。

由于这种继承方法的流行,ECMAScript 的第三版为 Function 对象加入了两个方法,即 call() 和 apply()。

call() 方法

call() 方法是与经典的对象冒充方法最相似的方法。它的第一个参数用作 this 的对象。其他参数都直接传递给函数自身。例如:

function sayColor(sPrefix,sSuffix) {
    alert(sPrefix + this.color + sSuffix);
};
var obj = new Object();
obj.color = "blue";
sayColor.call(obj, "The color is ", "a very nice color indeed.");

在这个例子中,函数 sayColor() 在对象外定义,即使它不属于任何对象,也可以引用关键字 this。对象 obj 的 color 属性等于 blue。调用 call() 方法时,第一个参数是 obj,说明应该赋予 sayColor() 函数中的 this 关键字值是 obj。第二个和第三个参数是字符串。它们与 sayColor() 函数中的参数 sPrefix 和 sSuffix 匹配,最后生成的消息 "The color is blue, a very nice color indeed." 将被显示出来。

要与继承机制的对象冒充方法一起使用该方法,只需将前三行的赋值、调用和删除代码替换即可:

function ClassB(sColor, sName) {
    //this.newMethod = ClassA;
    //this.newMethod(color);
    //delete this.newMethod;
    ClassA.call(this, sColor);
    this.name = sName;
    this.sayName = function () {
        alert(this.name);
    };
}

TIY

在这里,我们需要让 ClassA 中的关键字 this 等于新创建的 ClassB 对象,因此 this 是第一个参数。第二个参数 sColor 对两个类来说都是唯一的参数。

apply() 方法

apply() 方法有两个参数,用作 this 的对象和要传递给函数的参数的数组。例如:

function sayColor(sPrefix,sSuffix) {
    alert(sPrefix + this.color + sSuffix);
};
var obj = new Object();
obj.color = "blue";
sayColor.apply(obj, new Array("The color is ", "a very nice color indeed."));

Este exemplo é o mesmo que o anterior, apenas agora é chamado o método apply(). Ao chamar o método apply(), o primeiro parâmetro ainda é obj, indicando que deve-se atribuir o valor do objeto this à função sayColor(). O segundo parâmetro é um array composto por duas strings, que correspondem aos parâmetros sPrefix e sSuffix da função sayColor(), e a mensagem gerada ainda é "The color is blue, a very nice color indeed.", que será exibida.

Este método também é usado para substituir o código de atribuição, chamada e exclusão de métodos novos nas três primeiras linhas:

function ClassB(sColor, sName) {
    //this.newMethod = ClassA;
    //this.newMethod(color);
    //delete this.newMethod;
    ClassA.apply(this, new Array(sColor));
    this.name = sName;
    this.sayName = function () {
        alert(this.name);
    };
}

Da mesma forma, o primeiro parâmetro ainda é this, e o segundo parâmetro é um array com apenas um valor, color. Pode-se passar todo o objeto arguments da ClassB como o segundo parâmetro para o método apply():

function ClassB(sColor, sName) {
    //this.newMethod = ClassA;
    //this.newMethod(color);
    //delete this.newMethod;
    ClassA.apply(this, arguments);
    this.name = sName;
    this.sayName = function () {
        alert(this.name);
    };
}

TIY

Claro, apenas quando a ordem dos parâmetros na superclasse coincide com a ordem dos parâmetros na subclasse, é possível passar o objeto de parâmetros. Se não for o caso, é necessário criar um array separado, colocando os parâmetros na ordem correta. Além disso, pode-se usar o método call().

Cadeia de prototypes (prototype chaining)

Esta forma de herança é originalmente usada para a cadeia de prototypes no ECMAScript. No capítulo anterior, foi introduzida a forma de definir a classe usando o prototype. A cadeia de prototypes expande essa maneira, implementando o mecanismo de herança de uma forma interessante.

Como aprendido no capítulo anterior, o objeto prototype é um modelo, e todos os objetos instanciados são baseados neste modelo. Em resumo, qualquer atributo ou método do objeto prototype é passado para todas as instâncias dessa classe. A cadeia de prototypes utiliza essa funcionalidade para implementar o mecanismo de herança.

Se a classe for redefinida de forma protótipo no exemplo anterior, ela se tornará da seguinte forma:

function ClassA() {
}
ClassA.prototype.color = "blue";
ClassA.prototype.sayColor = function () {
    alert(this.color);
};
function ClassB() {
}
ClassB.prototype = new ClassA();

A maravilha do método de protótipo está destacada na linha de código azul. Aqui, a propriedade prototype do ClassB é definida como uma instância de ClassA. Isso é interessante, porque queremos todas as propriedades e métodos da ClassA, mas não queremos adicionar它们 um por um ao prototype da ClassB. Há algum método melhor do que atribuir uma instância da ClassA à propriedade prototype?

Atenção:Chama o construtor da ClassA, sem passar parâmetros. Isso é o做法 padrão na cadeia de protótipos. Certifique-se de que o construtor não tenha nenhum parâmetro.

Da mesma forma que a máscara de objeto, todas as propriedades e métodos do subclasse devem aparecer após a atribuição da propriedade prototype, porque todos os métodos atribuídos antes disso serão deletados. Por que? Porque a propriedade prototype é substituída por um novo objeto, e o objeto original que adicionou novos métodos será destruído. Portanto, o código para adicionar a propriedade name e o método sayName() para a classe ClassB é o seguinte:

function ClassB() {
}
ClassB.prototype = new ClassA();
ClassB.prototype.name = "";
ClassB.prototype.sayName = function () {
    alert(this.name);
};

Você pode testar este código executando o exemplo a seguir:

var objA = new ClassA();
var objB = new ClassB();
objA.color = "blue";
objB.color = "red";
objB.name = "John";
objA.sayColor();
objB.sayColor();
objB.sayName();

TIY

Além disso, na cadeia de protótipos, o modo de operação do operador instanceof também é muito peculiar. Para todos os instâncias de ClassB, instanceof retorna true para ClassA e ClassB. Por exemplo:

var objB = new ClassB();
alert(objB instanceof ClassA); // Saída "true"
alert(objB instanceof ClassB); // Saída "true"

No mundo de tipos fracos do ECMAScript, isso é uma ferramenta extremamente útil, mas não pode ser usado ao usar máscara de objeto.

A desvantagem da cadeia de protótipos é que não suporta herança múltipla. Lembre-se, a cadeia de protótipos usará outro tipo de objeto para sobrescrever a propriedade prototype da classe.

Mistura

Este método de herança usa o construtor da classe, não qualquer protótipo. O principal problema do objeto de máscara é que é necessário usar o método do construtor, que não é a melhor escolha. No entanto, se usar a cadeia de protótipos, não pode usar o construtor com parâmetros. Como o desenvolvedor escolhe? A resposta é simples, ambos.

No capítulo anterior, explicamos a melhor maneira de criar classes, usando construtores para definir atributos e prototypes para definir métodos. Este método também se aplica ao mecanismo de herança, onde o objeto herda atributos do construtor usando pseudo-herança e os métodos do objeto prototype usando o chain de prototypes. Rewrite o exemplo anterior com esses dois métodos, o código é o seguinte:

function ClassA(sColor) {
    this.color = sColor;
}
ClassA.prototype.sayColor = function () {
    alert(this.color);
};
function ClassB(sColor, sName) {
    ClassA.call(this, sColor);
    this.name = sName;
}
ClassB.prototype = new ClassA();
ClassB.prototype.sayName = function () {
    alert(this.name);
};

Neste exemplo, o mecanismo de herança é implementado por duas linhas destacadas em azul. Na primeira linha destacada, na função construtora ClassB, o objeto herda a propriedade sColor da classe ClassA. Na segunda linha destacada, os métodos são herdados da classe ClassA via chain de prototypes. Devido a essa mistura que usa o chain de prototypes, o operador instanceof ainda funciona corretamente.

Abaixo está um exemplo que testa este código:

var objA = new ClassA("blue");
var objB = new ClassB("red", "John");
objA.sayColor(); // Saída "blue"
objB.sayColor(); // Saída "red"
objB.sayName(); // Saída "John"

TIY