Maison  >  Article  >  interface Web  >  Explication détaillée de l'héritage prototypique dans les astuces JavaScript_javascript

Explication détaillée de l'héritage prototypique dans les astuces JavaScript_javascript

WBOY
WBOYoriginal
2016-05-16 16:14:051081parcourir

JavaScript est un langage orienté objet. Il existe un dicton très classique en JavaScript : tout est objet. Puisqu’il est orienté objet, il présente trois caractéristiques majeures de l’orientation objet : l’encapsulation, l’héritage et le polymorphisme. Nous parlons ici de l’héritage JavaScript, et nous parlerons des deux autres plus tard.

L'héritage JavaScript est différent de l'héritage C. L'héritage C est basé sur des classes, tandis que l'héritage JavaScript est basé sur des prototypes.

Vient maintenant le problème.

Qu'est-ce qu'un prototype ? Pour le prototype, on peut se référer à la classe en C, qui sauvegarde également les propriétés et méthodes de l'objet. Par exemple, écrivons un objet simple

Copier le code Le code est le suivant :

function Animal(nom) {
This.name = nom;
>
Animal.prototype.setName = fonction (nom) {
This.name = nom;
>
var animal = new Animal("wangwang");

On voit qu'il s'agit d'un objet Animal, qui a un nom d'attribut et une méthode setName. Il est à noter qu'une fois le prototype modifié, comme l'ajout d'une méthode, toutes les instances de l'objet partageront cette méthode. Par exemple

Copier le code Le code est le suivant :

function Animal(nom) {
This.name = nom;
>
var animal = new Animal("wangwang");

À l'heure actuelle, l'animal n'a que l'attribut nom. Si on ajoute une phrase,

Copier le code Le code est le suivant :

Animal.prototype.setName = fonction (nom) {
This.name = nom;
>

À ce moment-là, animal aura également une méthode setName.

Hériter de cette copie - en commençant par l'objet vide.Nous savons que parmi les types de base de JS, il y en a un appelé objet, et son instance la plus basique est l'objet vide, c'est-à-dire l'instance générée en appelant directement new Object. (), ou Il est déclaré en utilisant le littéral { }. Un objet vide est un "objet propre" avec uniquement des propriétés et des méthodes prédéfinies, et tous les autres objets héritent de l'objet vide, donc tous les objets ont ces propriétés et méthodes prédéfinies. Un prototype est en réalité une instance d’objet. La signification de prototype est la suivante : si le constructeur a un objet prototype A, alors les instances créées par le constructeur doivent être copiées à partir de A. Puisque l'instance est copiée à partir de l'objet A, l'instance doit hériter de toutes les propriétés, méthodes et autres propriétés de A. Alors, comment la réplication est-elle réalisée ? Méthode 1 : Copie de construction Chaque fois qu'une instance est construite, une instance est copiée à partir du prototype. La nouvelle instance et le prototype occupent le même espace mémoire. Bien que cela rende obj1 et obj2 "complètement cohérents" avec leurs prototypes, cela est également très peu économique : la consommation d'espace mémoire augmentera rapidement. Comme le montre l'image :


Méthode 2 : Copie sur écriture Cette stratégie provient d'une astuce cohérente du système : la copie sur écriture. Un exemple typique de ce type de tromperie est la bibliothèque de liens dynamiques (DDL) du système d'exploitation, dont la zone mémoire est toujours copiée lors de l'écriture. Comme le montre l'image :


Il suffit d'indiquer dans le système que obj1 et obj2 sont égaux à leurs prototypes, de sorte que lors de la lecture, il suffit de suivre les instructions pour lire les prototypes. Lorsque nous devons écrire les propriétés d'un objet (comme obj2), nous copions une image prototype et pointons les opérations futures vers cette image. Comme le montre l'image :


L'avantage de cette méthode est que nous n'avons pas besoin de beaucoup de surcharge de mémoire lors de la création d'instances et de la lecture des attributs. Nous n'utilisons qu'une partie du code pour allouer de la mémoire lors de la première écriture, ce qui entraîne une surcharge de code et de mémoire. . Mais cette surcharge n’existe plus, car l’efficacité de l’accès à l’image est la même que celle de l’accès au prototype. Toutefois, pour les systèmes qui effectuent fréquemment des opérations d'écriture, cette méthode n'est pas plus économique que la méthode précédente. Méthode 3 : parcours de lecture Cette méthode modifie la granularité de la réplication du prototype au membre. La caractéristique de cette méthode est que ce n'est que lors de l'écriture d'un membre d'une instance que les informations sur le membre sont copiées dans l'image de l'instance. Lors de l'écriture d'un attribut d'objet, par exemple (obj2.value=10), une valeur d'attribut nommée value sera générée et placée dans la liste des membres de l'objet obj2. Regardez la photo :

On constate que obj2 est toujours une référence pointant vers le prototype, et aucune instance d'objet de même taille que le prototype n'est créée lors de l'opération. De cette manière, les opérations d'écriture n'entraînent pas d'allocations de mémoire importantes, ce qui rend l'utilisation de la mémoire économique. La différence est que obj2 (et toutes les instances d'objet) doivent conserver une liste de membres. Cette liste de membres suit deux règles : Il est garanti qu'elle sera accessible en premier lors de la lecture. Si aucun attribut n'est spécifié dans l'objet, une tentative est effectuée pour parcourir toute la chaîne de prototypes de l'objet jusqu'à ce que le prototype soit vide ou que l'attribut soit trouvé. La chaîne prototype sera discutée plus tard. Évidemment, parmi les trois méthodes, la traversée en lecture présente les meilleures performances. Par conséquent, l’héritage prototypique de JavaScript est une traversée en lecture. constructeur Les personnes qui connaissent C seront certainement confuses après avoir lu le code de l'objet supérieur. C'est plus facile à comprendre sans le mot-clé class. Après tout, il existe le mot-clé function, qui est juste un mot-clé différent. Mais qu’en est-il des constructeurs ? En fait, JavaScript possède également un constructeur similaire, mais il est appelé constructeur. Lors de l'utilisation de l'opérateur new, le constructeur a effectivement été appelé et celui-ci est lié à un objet. Par exemple, nous utilisons le code suivant

Copier le code Le code est le suivant :

var animal = Animal("wangwang");

l'animal ne sera pas défini. Certains diront qu’aucune valeur de retour n’est bien entendu indéfinie. Alors si vous changez la définition de l'objet Animal :

Copier le code Le code est le suivant :

function Animal(nom) {
This.name = nom;
Renvoyez ceci ;
>

Devinez quel animal est maintenant ?
A ce moment, l'animal devient une fenêtre. La différence est que la fenêtre est étendue de sorte qu'elle ait un attribut de nom. En effet, la valeur par défaut est window, qui est la variable de niveau supérieur, si elle n'est pas spécifiée. Ce n'est qu'en appelant le mot-clé new que le constructeur peut être appelé correctement. Alors, comment empêcher les utilisateurs de rater le nouveau mot-clé ? Nous pouvons apporter quelques petits changements :

Copier le code Le code est le suivant :

function Animal(nom) {
If(!(cette instance d'Animal)) {
          return new Animal(name); >
This.name = nom;
>

De cette façon, vous serez infaillible. Le constructeur est également utilisé pour indiquer à quel objet appartient l'instance. Nous pouvons utiliser instanceof pour juger, mais instanceof renvoie vrai à la fois pour les objets ancêtres et les objets réels lors de l'héritage, cela ne convient donc pas. Lorsque le constructeur est appelé avec new, il pointe par défaut vers l'objet actuel.

Copier le code Le code est le suivant :
console.log(Animal.prototype.constructor === Animal); // vrai

On peut penser différemment : le prototype n'a aucune valeur lorsque la fonction est initialisée. L'implémentation peut être la logique suivante

//Définissez __proto__ comme membre intégré de la fonction, et get_prototyoe() est sa méthode

Copier le code Le code est le suivant :
var __proto__ = nul;
fonction get_prototype() {
Si(!__proto__) {
​​​​ __proto__ = nouvel Objet();
         __proto__.constructor = this;
>
Retourner __proto__;
>


L'avantage de ceci est que cela évite de créer une instance d'objet à chaque fois qu'une fonction est déclarée, ce qui permet d'économiser des frais généraux. Le constructeur peut être modifié, ce qui sera discuté plus tard. Héritage basé sur des prototypes Je pense que tout le monde sait presque ce qu'est l'héritage, donc je ne montrerai pas la limite inférieure du QI.

Il existe plusieurs types d'héritage JS, en voici deux types

1. Méthode 1 Cette méthode est la plus couramment utilisée et offre une meilleure sécurité. Définissons d’abord deux objets

Copier le code Le code est le suivant :
function Animal(nom) {
This.name = nom;
>
fonction Chien(âge) {
Cet.age = âge;
>
var chien = nouveau Chien(2);

Pour construire l'héritage, c'est très simple, pointez le prototype de l'objet enfant vers l'instance de l'objet parent (notez qu'il s'agit d'une instance, pas d'un objet)

Copier le code Le code est le suivant :
Chien.prototype = new Animal("wangwang");

À ce moment-là, le chien aura deux attributs, le nom et l'âge. Et si vous utilisez l'opérateur instanceof

sur dog

Copier le code Le code est le suivant :
console.log (instance de chien d'Animal); // true
console.log (instance de chien de Dog); // false

De cette façon, l'héritage est obtenu, mais il y a un petit problème

Copier le code Le code est le suivant :
console.log(Dog.prototype.constructor === Animal); // vrai
console.log(Dog.prototype.constructor === Chien); // false

Vous pouvez voir que l'objet pointé par le constructeur a changé, ce qui ne répond pas à notre objectif. Nous ne pouvons pas juger à qui appartient notre nouvelle instance. On peut donc ajouter une phrase :

Copier le code Le code est le suivant :
Dog.prototype.constructor = Chien;

Regardons à nouveau :

Copier le code Le code est le suivant :

console.log (instance de chien d'Animal); // false
console.log (instance de chien de Dog); // true

fait. Cette méthode fait partie de la maintenance de la chaîne de prototypes et sera expliquée en détail ci-dessous. 2. Méthode 2 Cette méthode présente des avantages et des inconvénients, mais les inconvénients l'emportent sur les avantages. Regardons d'abord le code

Copier le code Le code est le suivant :

function Animal(nom) {
This.name = nom;
>
Animal.prototype.setName = fonction (nom) {
This.name = nom;
>
fonction Chien(âge) {
Cet.age = âge;
>
Chien.prototype = Animal.prototype;

Cela permet d'obtenir une copie de prototype.

L'avantage de cette méthode est qu'elle n'a pas besoin d'instancier des objets (par rapport à la méthode 1), ce qui permet d'économiser des ressources. Les inconvénients sont également évidents.En plus du même problème que ci-dessus, c'est-à-dire que le constructeur pointe vers l'objet parent, il ne peut copier que les propriétés et méthodes déclarées par le prototype de l'objet parent. En d'autres termes, dans le code ci-dessus, l'attribut name de l'objet Animal ne peut pas être copié, mais la méthode setName peut être copiée. Le plus fatal est que toute modification du prototype de l'objet enfant affectera le prototype de l'objet parent, c'est-à-dire que les instances déclarées par les deux objets seront affectées. Cette méthode n’est donc pas recommandée.

Chaîne prototype

Tous ceux qui ont écrit un héritage savent que l'héritage peut être hérité à plusieurs niveaux. En JS, cela constitue la chaîne de prototypes. La chaîne de prototypes a été mentionnée à plusieurs reprises ci-dessus, alors qu'est-ce que la chaîne de prototypes ? Une instance doit au moins avoir un attribut proto pointant vers le prototype, qui est la base du système objet en JavaScript. Cependant, cette propriété est invisible. Nous l'appelons la « chaîne de prototypes interne » pour la distinguer de la « chaîne de prototypes du constructeur » composée du prototype du constructeur (qui est ce que nous appelons habituellement la « chaîne de prototypes »). Construisons d’abord une relation d’héritage simple selon le code ci-dessus :

Copier le code Le code est le suivant :

function Animal(nom) {
This.name = nom;
>
fonction Chien(âge) {
Cet.age = âge;
>
var animal = new Animal("wangwang");
Chien.prototype = animal;
var chien = nouveau Chien(2);

Pour rappel, comme mentionné précédemment, tous les objets héritent des objets vides. Nous avons donc construit une chaîne prototype :


Nous pouvons voir que le prototype de l'objet enfant pointe vers l'instance de l'objet parent, formant la chaîne de prototypes du constructeur. Le proto-objet interne de l'instance enfant pointe également vers l'instance de l'objet parent, formant une chaîne de prototypes interne. Lorsque nous devons trouver un certain attribut, le code est similaire à

Copier le code Le code est le suivant :

fonction getAttrFromObj(attr, obj) {
Si(typeof(obj) === "objet") {
        var proto = obj;
​​​​pendant que(proto) {
Si(proto.hasOwnProperty(attr)) {
                     return proto[attr];
            }
              proto = proto.__proto__;
>
>
Retour indéfini ;
>

Dans cet exemple, si nous recherchons l'attribut name dans dog, il sera recherché dans la liste des membres dans dog. Bien sûr, il ne sera pas trouvé car la liste des membres de dog n'a désormais que l'âge. Ensuite, il continuera à chercher le long de la chaîne de prototypes, c'est-à-dire l'instance pointée par .proto, c'est-à-dire qu'en animal, il trouvera l'attribut name et le renverra. S'il recherche une propriété qui n'existe pas et ne peut pas la trouver dans animal, il continuera à chercher avec .proto et trouvera un objet vide. S'il ne le trouve pas, il continuera à chercher avec .proto et avec. objet vide. proto pointe vers null, cherchant la sortie.

Maintenance de la chaîne de prototypes Nous avons soulevé une question lorsque nous venons de parler de l'héritage prototypique. Lors de l'utilisation de la méthode 1 pour construire l'héritage, le constructeur de l'instance de l'objet enfant pointe vers l'objet parent. L'avantage est que nous pouvons accéder à la chaîne de prototypes via l'attribut constructeur, mais les inconvénients sont également évidents. Un objet, l'instance qu'il génère doit pointer vers lui-même, c'est-à-dire

Copier le code Le code est le suivant :

(nouveau obj()).prototype.constructor === obj

Ensuite, lorsqu'on surcharge la propriété prototype, le constructeur de l'instance générée par le sous-objet ne pointe pas sur lui-même ! Cela va à l’encontre de l’intention initiale du constructeur. Nous avons évoqué une solution ci-dessus :

Copier le code Le code est le suivant :

Chien.prototype = new Animal("wangwang");
Dog.prototype.constructor = Chien;

On dirait qu’il n’y a pas de problème. Mais en fait, cela pose un nouveau problème, car nous constaterons que nous ne pouvons pas revenir en arrière sur la chaîne de prototypes, car nous ne pouvons pas trouver l'objet parent et que la propriété .proto de la chaîne de prototypes interne est inaccessible. Par conséquent, SpiderMonkey propose une solution améliorée : ajouter un attribut nommé __proto__ à tout objet créé, qui pointe toujours vers le prototype utilisé par le constructeur. De cette façon, toute modification du constructeur n’affectera pas la valeur de __proto__, ce qui facilite la maintenance du constructeur.

Cependant, il y a deux autres problèmes :

__proto__ est remplaçable, ce qui signifie qu'il y a toujours des risques lors de son utilisation

__proto__ est un traitement spécial de spiderMonkey et ne peut pas être utilisé dans d'autres moteurs (tels que JScript).

Nous avons également un autre moyen, qui consiste à conserver les propriétés du constructeur du prototype et à initialiser les propriétés du constructeur de l'instance dans la fonction constructeur de la sous-classe.

Le code est le suivant : Réécrire le sous-objet

Copier le code Le code est le suivant :

fonction Chien(âge) {
This.constructor = arguments.callee;
Cet.age = âge;
>
Chien.prototype = new Animal("wangwang");

De cette façon, les constructeurs de toutes les instances d'objets enfants pointent correctement vers l'objet et le constructeur du prototype pointe vers l'objet parent. Bien que cette méthode soit relativement inefficace car l’attribut constructeur doit être réécrit à chaque fois qu’une instance est construite, il ne fait aucun doute que cette méthode peut résoudre efficacement la contradiction précédente. ES5 prend cette situation en considération et résout complètement ce problème : vous pouvez utiliser Object.getPrototypeOf() à tout moment pour obtenir le vrai prototype d'un objet sans avoir à accéder au constructeur ou à maintenir une chaîne de prototypes externe. Par conséquent, pour trouver les propriétés de l'objet comme mentionné dans la section précédente, nous pouvons le réécrire comme suit :

Copier le code Le code est le suivant :

fonction getAttrFromObj(attr, obj) {
Si(typeof(obj) === "objet") {
faire {
          var proto = Object.getPrototypeOf(dog);
Si(proto[attr]) {
                     return proto[attr];
            }
>
​​​​pendant que(proto);
>
Retour indéfini ;
>

Bien entendu, cette méthode ne peut être utilisée que dans les navigateurs prenant en charge ES5. Pour des raisons de compatibilité ascendante, nous devons toujours considérer la méthode précédente. Une méthode plus appropriée consiste à intégrer et à encapsuler ces deux méthodes. Je pense que les lecteurs sont très bons dans ce domaine, je ne vais donc pas me montrer ici.

Déclaration:
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn