131 votes

Héritage prototypique - rédaction

J'ai donc ces 2 exemples, tirés de javascript.info :

Exemple 1 :

var animal = {
  eat: function() {
    alert( "I'm full" )
    this.full = true
  }
}

var rabbit = {
  jump: function() { /* something */ }
}

rabbit.__proto__ = animal 

rabbit.eat() 

Exemple 2 :

function Hamster() {  }
Hamster.prototype = {
  food: [],
  found: function(something) {
    this.food.push(something)
  }
}

// Create two speedy and lazy hamsters, then feed the first one
speedy = new Hamster()
lazy = new Hamster()

speedy.found("apple")
speedy.found("orange")

alert(speedy.food.length) // 2
alert(lazy.food.length) // 2 (!??)

Commencez par l'exemple 2 : lorsque le code atteint speedy.found il ne trouve pas found la propriété dans speedy Il grimpe donc jusqu'au prototype et le modifie à cet endroit. C'est pourquoi food.length est égale pour les deux hamsters, c'est-à-dire qu'ils ont le même estomac.

D'après ce que j'ai compris, lorsque l'on écrit et que l'on ajoute une nouvelle propriété qui n'existe pas, l'interprète remonte la chaîne des prototypes jusqu'à ce qu'il trouve la propriété, puis la modifie.

MAIS dans l'exemple 1, quelque chose d'autre se produit :
nous courons rabbit.eat qui change rabbit.full . full est introuvable, il faut donc remonter la chaîne des prototypes jusqu'à (jusqu'à l'objet ? ?), et là, je ne suis pas sûr de ce qui se passe. Dans cet exemple, la propriété full de rabbit est créé et modifié, alors que dans le premier exemple, il remonte la chaîne des prototypes car il ne trouve pas la propriété.

Je suis confus et je ne vois pas pourquoi cela se produit.

174voto

HMR Points 5459

Introduction de la fonction constructeur

Vous pouvez utiliser une fonction comme constructeur pour créer des objets. Si la fonction du constructeur est nommée Personne, les objets créés avec ce constructeur sont des instances de Personne.

var Person = function(name){
  this.name = name;
};
Person.prototype.walk=function(){
  this.step().step().step();
};
var bob = new Person("Bob");

Person est la fonction constructeur. Lorsque vous créez une instance en utilisant Person, vous devez utiliser le mot clé new :

var bob = new Person("Bob");console.log(bob.name);//=Bob
var ben = new Person("Ben");console.log(ben.name);//=Ben

Le bien/membre name est spécifique à l'instance, il est différent pour bob et ben

Le membre walk fait partie de Person.prototype et est partagé par toutes les instances bob et ben sont des instances de Person et partagent donc le membre walk (bob.walk===ben.walk).

bob.walk();ben.walk();

Parce que walk() n'a pas pu être trouvé sur bob directement, JavaScript va le chercher dans Person.prototype car c'est le constructeur de bob. Si elle n'est pas trouvée à cet endroit, elle sera recherchée dans Object.prototype. C'est ce qu'on appelle la chaîne des prototypes. La partie prototype de l'héritage se fait en allongeant cette chaîne ; par exemple bob => Employee.prototype => Person.prototype => Object.prototype (plus sur l'héritage plus tard).

Même si bob, ben et toutes les autres instances de Personne créées partagent la marche, la fonction se comportera différemment pour chaque instance, car dans la fonction de marche, elle utilise la méthode suivante this . La valeur de this sera l'objet invoquant ; pour l'instant, disons qu'il s'agit de l'instance courante, donc pour l'option bob.walk() "this" sera bob. (nous reviendrons plus tard sur "this" et l'objet invoquant).

Si ben attendait un feu rouge et que bob était au feu vert, alors vous invoquerez walk() sur ben et bob, il est évident que quelque chose de différent arrivera à ben et bob.

L'observation des membres se produit lorsque nous faisons quelque chose comme ben.walk=22 même si Bob et Ben partagent walk le site affectation de 22 à ben.walk n'affectera pas bob.walk. Cela est dû au fait que cette instruction créera un membre appelé walk sur ben directement et lui attribuer une valeur de 22. Il y aura 2 membres de marche différents : ben.walk et Person.prototype.walk.

En demandant bob.walk, vous obtiendrez la fonction Person.prototype.walk car walk n'a pas pu être trouvé sur bob. En revanche, si vous demandez ben.walk, vous obtiendrez la valeur 22, car le membre walk a été créé sur ben et comme JavaScript a trouvé walk sur ben, il ne cherchera pas dans le prototype Person.

Lorsque vous utilisez Object.create avec 2 arguments, Object.defineProperty ou Object.defineProperties, l'ombrage fonctionne un peu différemment. Plus d'informations à ce sujet aquí .

En savoir plus sur le prototype

Un objet peut hériter d'un autre objet grâce à l'utilisation du prototype. Vous pouvez définir le prototype de n'importe quel objet avec n'importe quel autre objet à l'aide de la fonction Object.create . Dans l'introduction de la fonction constructeur, nous avons vu que si un membre ne peut être trouvé sur l'objet, JavaScript le cherchera dans la chaîne de prototypes.

Dans la partie précédente, nous avons vu que la réaffectation des membres qui proviennent du prototype d'une instance (ben.walk) va ombrager ce membre (créer walk sur ben plutôt que de changer Person.prototype.walk).

Que se passe-t-il si nous ne réassignons pas le membre mais le mutons ? La mutation consiste (par exemple) à modifier les propriétés secondaires d'un objet ou à invoquer des fonctions qui modifieront la valeur de l'objet. Par exemple :

var o = [];
var a = o;
a.push(11);//mutate a, this will change o
a[1]=22;//mutate a, this will change o

Le code suivant démontre la différence entre les membres prototypes et les membres instances en mutant les membres.

var person = {
  name:"default",//immutable so can be used as default
  sayName:function(){
    console.log("Hello, I am "+this.name);
  },
  food:[]//not immutable, should be instance specific
         //  not suitable as prototype member
};
var ben = Object.create(person);
ben.name = "Ben";
var bob = Object.create(person);
console.log(bob.name);//=default, setting ben.name shadowed the member
                      //  so bob.name is actually person.name
ben.food.push("Hamburger");
console.log(bob.food);//=["Hamburger"], mutating a shared member on the
// prototype affects all instances as it changes person.food
console.log(person.food);//=["Hamburger"]

Le code ci-dessus montre que ben et bob partagent les membres de person. Il n'y a qu'une seule personne, elle est définie comme le prototype de bob et ben (person est utilisé comme premier objet dans la chaîne des prototypes pour rechercher les membres demandés qui n'existent pas sur l'instance). Le problème avec le code ci-dessus est que bob et ben devraient avoir leur propre food membre. C'est là que la fonction constructeur entre en jeu. Elle est utilisée pour créer des membres spécifiques à une instance. Vous pouvez également lui passer des arguments pour définir les valeurs de ces membres spécifiques à une instance.

Le code suivant montre une autre façon d'implémenter la fonction constructeur, la syntaxe est différente mais l'idée est la même :

  1. Définir un objet dont les membres seront les mêmes pour plusieurs instances (person est un modèle pour bob et ben et peut l'être pour jilly, marie, clair ...)
  2. Définir des membres spécifiques aux instances qui doivent être uniques pour les instances (bob et ben).
  3. Créez une instance exécutant le code de l'étape 2.

Avec les fonctions constructeurs, vous définissez le prototype à l'étape 2 ; dans le code suivant, nous définissons le prototype à l'étape 3.

Dans ce code, j'ai supprimé le nom du prototype ainsi que la nourriture parce que vous allez probablement l'ombrager presque immédiatement lors de la création d'une instance de toute façon. Name est maintenant un membre spécifique à l'instance avec une valeur par défaut définie dans la fonction constructeur. Parce que le membre food est également déplacé du prototype au membre spécifique à l'instance, il n'affectera pas bob.food lors de l'ajout de la nourriture à ben.

var person = {
  sayName:function(){
    console.log("Hello, I am "+this.name);
  },
  //need to run the constructor function when creating
  //  an instance to make sure the instance has
  //  instance specific members
  constructor:function(name){
    this.name = name || "default";
    this.food = [];
    return this;
  }
};
var ben = Object.create(person).constructor("Ben");
var bob = Object.create(person).constructor("Bob");
console.log(bob.name);//="Bob"
ben.food.push("Hamburger");
console.log(bob.food);//=[]

Vous rencontrerez peut-être des modèles similaires, plus robustes, qui vous aideront à créer et à définir des objets.

Héritage

Le code suivant montre comment hériter. Les tâches sont fondamentalement les mêmes que dans le code précédent avec un petit plus

  1. Définir des membres spécifiques à l'instance d'un objet (fonctions Hamster et RussionMini).
  2. Définir la partie prototype de l'héritage (RussionMini.prototype = Object.create(Hamster.prototype))
  3. Définir les membres qui peuvent être partagés entre les instances.(Hamster.prototype et RussionMini.prototype)
  4. Créez une instance qui exécute le code de l'étape 1 et faites en sorte que les objets qui héritent exécutent également le code Parent (Hamster.apply(this,arguments) ;).

En utilisant un modèle que certains appelleraient "héritage classique". Si vous êtes confus par la syntaxe, je serai heureux de vous donner plus d'explications ou de vous proposer d'autres modèles.

function Hamster(){
 this.food=[];
}
function RussionMini(){
  //Hamster.apply(this,arguments) executes every line of code
  //in the Hamster body where the value of "this" is
  //the to be created RussionMini (once for mini and once for betty)
  Hamster.apply(this,arguments);
}
//setting RussionMini's prototype
RussionMini.prototype=Object.create(Hamster.prototype);
//setting the built in member called constructor to point
// to the right function (previous line has it point to Hamster)
RussionMini.prototype.constructor=RussionMini;
mini=new RussionMini();
//this.food (instance specic to mini)
//  comes from running the Hamster code
//  with Hamster.apply(this,arguments);
mini.food.push("mini's food");
//adding behavior specific to Hamster that will still be
//  inherited by RussionMini because RussionMini.prototype's prototype
//  is Hamster.prototype
Hamster.prototype.runWheel=function(){console.log("I'm running")};
mini.runWheel();//=I'm running

Object.create pour définir la partie prototype de l'héritage

Voici la documentation sur Objet.create il renvoie essentiellement le second argument (non supporté dans le polyfil) avec le premier argument comme prototype de l'objet renvoyé.

Si aucun second argument n'a été donné, il retournera un objet vide avec le premier argument à utiliser comme prototype de l'objet retourné (le premier objet à utiliser dans la chaîne de prototypes de l'objet retourné).

Certains définissent le prototype de RussionMini comme une instance de Hamster (RussionMini.prototype = new Hamster()). Ce n'est pas souhaitable car même si cela revient au même (le prototype de RussionMini.prototype est Hamster.prototype), cela définit également les membres de l'instance de Hamster comme membres de RussionMini.prototype. Ainsi, RussionMini.prototype.food existera mais sera un membre partagé (vous vous souvenez de bob et ben dans "More about prototype" ?). Le membre food sera caché lors de la création d'un RussionMini parce que le code Hamster est exécuté avec la commande Hamster.apply(this,arguments); qui à son tour exécute this.food = [] mais tous les membres de Hamster seront toujours membres de RussionMini.prototype.

Une autre raison pourrait être que pour créer un hamster, de nombreux calculs compliqués doivent être effectués sur les arguments passés qui peuvent ne pas être encore disponibles. Là encore, vous pourriez passer des arguments factices mais cela pourrait compliquer inutilement votre code.

Extension et remplacement des fonctions parentales

Parfois children besoin de prolonger parent fonctions.

Vous voulez que l'"enfant" (=RussionMini) fasse quelque chose de plus. Lorsque RussionMini peut appeler le code Hamster pour faire quelque chose et ensuite faire quelque chose de plus, vous n'avez pas besoin de copier et coller le code Hamster dans RussionMini.

Dans l'exemple suivant, nous supposons qu'un hamster peut courir 3 km par heure, mais qu'un Russion mini ne peut courir que la moitié de cette vitesse. Nous pouvons coder en dur 3/2 dans RussionMini mais si cette valeur devait changer, nous aurions plusieurs endroits dans le code où il faudrait la modifier. Voici comment nous utilisons Hamster.prototype pour obtenir la vitesse du parent (Hamster).

var Hamster = function(name){
 if(name===undefined){
   throw new Error("Name cannot be undefined");
 }
 this.name=name;
}
Hamster.prototype.getSpeed=function(){
  return 3;
}
Hamster.prototype.run=function(){
  //Russionmini does not need to implement this function as
  //it will do exactly the same as it does for Hamster
  //But Russionmini does need to implement getSpeed as it
  //won't return the same as Hamster (see later in the code) 
  return "I am running at " + 
    this.getSpeed() + "km an hour.";
}

var RussionMini=function(name){
  Hamster.apply(this,arguments);
}
//call this before setting RussionMini prototypes
RussionMini.prototype = Object.create(Hamster.prototype);
RussionMini.prototype.constructor=RussionMini;

RussionMini.prototype.getSpeed=function(){
  return Hamster.prototype
    .getSpeed.call(this)/2;
}    

var betty=new RussionMini("Betty");
console.log(betty.run());//=I am running at 1.5km an hour.

L'inconvénient est que vous devez coder en dur Hamster.prototype. Il existe peut-être des modèles qui vous donneront l'avantage de super comme en Java.

La plupart des modèles que j'ai vus se cassent lorsque le niveau d'héritage est supérieur à 2 niveaux (Enfant => Parent => GrandParent) ou utilisent plus de ressources en implémentant super par le biais de fermetures .

Pour remplacer une méthode Parent (=Hamster), vous faites la même chose, mais ne faites pas Hamster.prototype.parentMethod.call(this,....

this.constructor

La propriété constructeur est incluse dans le prototype par JavaScript, vous pouvez la modifier mais elle doit pointer vers la fonction constructeur. Donc Hamster.prototype.constructor devrait pointer vers Hamster.

Si après avoir défini le prototype de la partie de l'héritage, vous devez le faire pointer à nouveau vers la bonne fonction.

var Hamster = function(){};
var RussionMinni=function(){
   // re use Parent constructor (I know there is none there)
   Hamster.apply(this,arguments);
};
RussionMinni.prototype=Object.create(Hamster.prototype);
console.log(RussionMinni.prototype.constructor===Hamster);//=true
RussionMinni.prototype.haveBaby=function(){
  return new this.constructor();
};
var betty=new RussionMinni();
var littleBetty=betty.haveBaby();
console.log(littleBetty instanceof RussionMinni);//false
console.log(littleBetty instanceof Hamster);//true
//fix the constructor
RussionMinni.prototype.constructor=RussionMinni;
//now make a baby again
var littleBetty=betty.haveBaby();
console.log(littleBetty instanceof RussionMinni);//true
console.log(littleBetty instanceof Hamster);//true

Héritage multiple avec mix ins

Certaines choses sont mieux de ne pas être héritées, si un chat peut se déplacer et qu'un chat ne devrait pas hériter de Movable. Un chat n'est pas un Movable, mais un chat peut se déplacer. Dans un langage basé sur des classes, le chat devrait implémenter Movable. En JavaScript, nous pouvons définir Movable et définir son implémentation ici, le chat peut soit le surcharger, l'étendre ou utiliser son implémentation par défaut.

Pour Movable, nous avons des membres spécifiques à l'instance (tels que location ). Et nous avons des membres qui ne sont pas spécifiques à une instance (comme la fonction move()). Les membres spécifiques à l'instance seront définis en appelant mxIns (ajouté par la fonction d'aide mixin) lors de la création d'une instance. Les membres du prototype seront copiés un par un sur Cat.prototype à partir de Movable.prototype en utilisant la fonction d'aide mixin.

var Mixin = function Mixin(args){
  if(this.mixIns){
    i=-1;len=this.mixIns.length;
    while(++i<len){
        this.mixIns[i].call(this,args);
      }
  }  
};
Mixin.mix = function(constructor, mix){
  var thing
  ,cProto=constructor.prototype
  ,mProto=mix.prototype;
  //no extending, if multiple prototypes
  // have members with the same name then use
  // the last
  for(thing in mProto){
    if(Object.hasOwnProperty.call(mProto, thing)){
      cProto[thing]=mProto[thing];
    }
  }
  //instance intialisers
  cProto.mixIns = cProto.mixIns || [];
  cProto.mixIns.push(mix);
};
var Movable = function(args){
  args=args || {};
  //demo how to set defaults with truthy
  // not checking validaty
  this.location=args.location;
  this.isStuck = (args.isStuck===true);//defaults to false
  this.canMove = (args.canMove!==false);//defaults to true
  //speed defaults to 4
  this.speed = (args.speed===0)?0:(args.speed || 4);
};
Movable.prototype.move=function(){
  console.log('I am moving, default implementation.');
};
var Animal = function(args){
  args = args || {};
  this.name = args.name || "thing";
};
var Cat = function(args){
  var i,len;
  Animal.call(args);
  //if an object can have others mixed in
  //  then this is needed to initialise 
  //  instance members
  Mixin.call(this,args);
};
Cat.prototype = Object.create(Animal.prototype);
Cat.prototype.constructor = Cat;
Mixin.mix(Cat,Movable);
var poochie = new Cat({
  name:"poochie",
  location: {x:0,y:22}
});
poochie.move();

Ce qui précède est une implémentation simple qui remplace les fonctions du même nom par n'importe quel mélange en dernier.

Cette variable

Dans tous les exemples de code, vous verrez this se référant à l'instance actuelle.

La variable this fait en fait référence à l'objet invoquant, elle fait référence à l'objet qui précède la fonction.

Pour clarifier, voir le code suivant :

theInvokingObject.thefunction();

Les cas où il s'agirait d'un mauvais objet sont généralement liés à l'attachement d'écouteurs d'événements, de rappels ou de délais et d'intervalles. Dans les 2 lignes de code suivantes, nous pass la fonction, nous ne l'invoquons pas. Le passage de la fonction est : someObject.aFunction et l'invoquer l'est : someObject.aFunction() . El this ne se réfère pas à l'objet sur lequel la fonction a été déclarée, mais à l'objet sur lequel la fonction a été déclarée. invokes il.

setTimeout(someObject.aFuncton,100);//this in aFunction is window
somebutton.onclick = someObject.aFunction;//this in aFunction is somebutton

Pour faire this dans les cas ci-dessus font référence à un certainObjet, vous pouvez passer un fermeture au lieu de la fonction directement :

setTimeout(function(){someObject.aFuncton();},100);
somebutton.onclick = function(){someObject.aFunction();};

J'aime définir des fonctions qui renvoient une fonction de fermetures sur le prototype afin d'avoir un contrôle fin sur les variables qui sont incluses dans l'échantillon. fermeture l'étendue.

var Hamster = function(name){
  var largeVariable = new Array(100000).join("Hello World");
  // if I do 
  // setInterval(function(){this.checkSleep();},100);
  // then largeVariable will be in the closure scope as well
  this.name=name
  setInterval(this.closures.checkSleep(this),1000);
};
Hamster.prototype.closures={
  checkSleep:function(hamsterInstance){
    return function(){
      console.log(typeof largeVariable);//undefined
      console.log(hamsterInstance);//instance of Hamster named Betty
      hamsterInstance.checkSleep();
    };
  }
};
Hamster.prototype.checkSleep=function(){
  //do stuff assuming this is the Hamster instance
};

var betty = new Hamster("Betty");

Passage d'arguments (constructeurs)

Quand un enfant appelle un parent ( Hamster.apply(this,arguments); ) nous supposons que Hamster utilise les mêmes arguments que RussionMini dans le même ordre. Pour les fonctions qui appellent d'autres fonctions, j'utilise généralement une autre façon de passer les arguments.

En général, je passe un objet à une fonction et je demande à cette fonction de muter ce dont elle a besoin (définir les valeurs par défaut), puis cette fonction le passera à une autre fonction qui fera de même et ainsi de suite. Voici un exemple :

//helper funciton to throw error
function thowError(message){
  throw new Error(message)
};
var Hamster = function(args){
  //make sure args is something so you get the errors
  //  that make sense to you instead of "args is undefined"
  args = args || {};
  //default value for type:
  this.type = args.type || "default type";
  //name is not optional, very simple truthy check f
  this.name = args.name || thowError("args.name is not optional");
};
var RussionMini = function(args){
  //make sure args is something so you get the errors
  //  that make sense to you instead of "args is undefined"
  args = args || {};
  args.type = "Russion Mini";
  Hamster.call(this,args);
};
var ben = new RussionMini({name:"Ben"});
console.log(ben);// Object { type="Russion Mini", name="Ben"}
var betty = new RussionMini();//Error: args.name is not optional

Cette façon de passer des arguments dans une chaîne de fonctions est utile dans de nombreux cas. Lorsque vous travaillez sur un code qui calcule un total de quelque chose et que plus tard vous souhaitez refacturer ce total dans une certaine devise, vous pouvez être amené à modifier de nombreuses fonctions pour passer la valeur de la devise. Vous pouvez augmenter la valeur d'une devise (même au niveau global comme window.currency='USD' ) mais c'est une mauvaise façon de résoudre le problème.

En passant un objet, vous pouvez ajouter de la monnaie à args chaque fois qu'elle est disponible dans la chaîne de fonctions et la muter/utiliser chaque fois que vous en avez besoin sans modifier les autres fonctions (il faut explicitement la passer dans les appels de fonction).

Variables privées

JavaScript ne dispose pas d'un modificateur privé.

Je suis d'accord avec ce qui suit : http://blog.millermedeiros.com/a-case-against-private-variables-and-functions-in-javascript/ et je ne les ai personnellement pas utilisés.

Vous pouvez indiquer aux autres programmeurs qu'un membre est destiné à être privé en le nommant _aPrivate ou mettre toutes les variables privées dans une variable objet appelée _ .

Vous pouvez mettre en œuvre des membres privés par le biais de fermetures mais les membres privés spécifiques à une instance ne peuvent être accédés que par des fonctions qui ne sont pas sur le prototype.

Le fait de ne pas implémenter les objets privés en tant que fermetures entraînerait des fuites au niveau de l'implémentation et permettrait à vous ou aux utilisateurs qui étendent votre code d'utiliser des membres qui ne font pas partie de votre API publique. Cela peut être à la fois bon et mauvais.

C'est une bonne chose car cela vous permet, ainsi qu'à d'autres, de simuler certains membres pour les tester facilement. Cela donne aux autres une chance d'améliorer facilement (patch) votre code mais c'est aussi mauvais car il n'y a aucune garantie que la prochaine version de votre code aura la même implémentation et/ou les mêmes membres privés.

En utilisant les fermetures, vous ne donnez pas le choix aux autres et en utilisant la convention de dénomination avec la documentation, vous le faites. Ceci n'est pas spécifique à JavaScript, dans d'autres langages vous pouvez décider de ne pas utiliser les membres privés car vous faites confiance aux autres pour savoir ce qu'ils font et vous leur laissez le choix de faire ce qu'ils veulent (avec les risques que cela comporte).

Si vous insistez toujours sur le privé, alors le suivant peut vous aider. Il n'implémente pas private mais implémente protected.

15voto

BLSully Points 3045

Les prototypes sont PAS instancié pour chaque instance d'un objet.

Hamster.prototype.food = []

Chaque instance de Hamster partagera ce tableau

Si vous avez besoin (et c'est le cas ici) d'instances distinctes de collections de nourriture pour chaque hamster, vous devez créer la propriété sur l'instance. Par exemple :

function Hamster() {
  this.food = [];
}

Pour répondre à votre question sur l'exemple 1, s'il ne trouve pas la propriété dans la chaîne des prototypes, il crée la propriété sur l'objet cible.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X