web-dev-qa-db-fra.com

Héritage prototypique - rédaction

J'ai donc ces 2 exemples, 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 de propriété found dans speedy, et donc il monte jusqu'au prototype et le change là. Voilà pourquoi food.length est égal pour les deux hamsters, c'est-à-dire qu'ils ont le même ventre.

D'après ce que je comprends, lors de l'écriture et de l'ajout d'une nouvelle propriété qui n'existe pas, l'interprète remontera la chaîne du prototype jusqu'à ce qu'il trouve la propriété, puis la changera.

MAIS dans l'exemple 1, il se passe autre chose:
nous courrons rabbit.eat, ce qui change rabbit.full. La propriété full est introuvable, elle devrait donc remonter la chaîne du prototype vers (pour objecter ??), et bien, je ne suis pas sûr de ce qui se passe ici. Dans cet exemple, la propriété full de rabbit est créée et modifiée, tandis que dans le premier exemple, elle remonte la chaîne du prototype car elle ne trouve pas la propriété.

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

129
frrlod

Introduction à la fonction constructeur

Vous pouvez utiliser une fonction en tant que constructeur pour créer des objets. Si la fonction 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");

La personne est la fonction constructeur. Lorsque vous créez une instance à l'aide de Person, vous devez utiliser le nouveau mot-clé:

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

La propriété/membre name est spécifique à l'instance, elle est différente pour bob et ben

Le membre walk fait partie du Person.prototype et est partagé pour toutes les instances bob et ben sont des instances de Person afin qu'ils partagent le membre walk (bob.walk === ben.walk).

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

Parce que walk () n'a pas pu être trouvé directement sur bob, JavaScript le recherchera dans le Person.prototype car il s'agit du constructeur de bob. S'il ne peut pas être trouvé là, il regardera Object.prototype. C'est ce qu'on appelle la chaîne prototype. La partie prototype de l'héritage se fait en allongeant cette chaîne; par exemple bob => Employee.prototype => Person.prototype => Object.prototype (plus d'informations sur l'héritage plus tard).

Même si bob, ben et toutes les autres instances Person créées partagent walk, la fonction se comportera différemment par instance car dans la fonction walk, elle utilise this. La valeur de this sera l'objet appelant; pour l'instant, disons que c'est l'instance actuelle, donc pour bob.walk() "this" sera bob. (plus sur "ceci" et l'objet invoquant plus tard).

Si ben attendait un feu rouge et que bob était au feu vert; vous invoquerez alors walk () à la fois sur ben et sur bob, évidemment quelque chose de différent arriverait à 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 la assignation de 22 à ben.walk n'affectera pas bob.walk. En effet, cette instruction créera directement un membre appelé walk sur ben et lui attribuera une valeur de 22. Il y aura 2 membres de promenade différents: ben.walk et Person.prototype.walk.

En demandant bob.walk, vous obtiendrez la fonction Person.prototype.walk car walk est introuvable sur bob. Cependant, demander ben.walk vous donnera la valeur 22 car le membre walk a été créé sur ben et puisque JavaScript a trouvé walk on ben, il ne cherchera pas dans le Person.prototype.

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

En savoir plus sur le prototype

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

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

Et si nous ne réaffectons pas mais mutons le membre? La mutation consiste (par exemple) à modifier les sous-propriétés 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 montre la différence entre les membres de prototype et les membres d'instance 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 d'une personne. Il n'y a qu'une seule personne, il est défini comme le prototype de Bob et Ben (la personne est utilisée comme premier objet dans la chaîne de prototype 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 membre food. C'est là qu'intervient la fonction constructeur. Elle est utilisée pour créer des membres spécifiques à l'instance. Vous pouvez également lui passer des arguments pour définir les valeurs de ces membres spécifiques à l'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éfinissez un objet dont les membres seront identiques dans de nombreux cas (la personne est un modèle pour bob et ben et peut être pour jilly, marie, clair ...)
  2. Définissez des membres spécifiques à l'instance qui doivent être uniques pour les instances (bob et ben).
  3. Créez une instance exécutant le code à l'étape 2.

Avec les fonctions constructeur, 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 celui de la nourriture, car vous allez très probablement le masquer presque immédiatement lors de la création d'une instance. Le nom est désormais un membre spécifique à l'instance avec une valeur par défaut définie dans la fonction constructeur. Parce que le membre alimentaire est également déplacé du prototype vers le membre spécifique de l'instance, cela n'affectera pas bob.food lors de l'ajout de 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 pouvez rencontrer des modèles similaires qui sont plus robustes pour aider à la création et à la définition d'objet.

Héritage

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

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

En utilisant un modèle, certains appellent "l'héritage classique". Si vous êtes confus par la syntaxe, je serai heureux d'expliquer plus ou de fournir différents 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 Object.create , elle retourne essentiellement le deuxième argument (non pris en charge dans le polyfil) avec le premier argument comme prototype de l'objet retourné.

Si aucun deuxième 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éfiniraient le prototype de RussionMini sur une instance de Hamster (RussionMini.prototype = new Hamster ()). Ce n'est pas souhaitable car même s'il accomplit la même chose (le prototype de RussionMini.prototype est Hamster.prototype), il définit également les membres d'instance de Hamster comme membres de RussionMini.prototype. Donc RussionMini.prototype.food existera mais est un membre partagé (rappelez-vous bob et ben dans "Plus sur le prototype"?). Le membre de la nourriture sera ombré lors de la création d'un RussionMini car le code Hamster est exécuté avec Hamster.apply(this,arguments); qui à son tour exécute this.food = [] Mais tous les membres Hamster seront toujours membres de RussionMini.prototype.

Une autre raison pourrait être que pour créer un hamster, beaucoup de calculs compliqués doivent être effectués sur des arguments passés qui ne sont peut-être pas encore disponibles, encore une fois, vous pouvez passer des arguments factices mais cela pourrait compliquer inutilement votre code.

Extension et remplacement des fonctions parent

Parfois, children doit étendre les fonctions parent.

Vous voulez que "l'enfant" (= RussionMini) fasse quelque chose de plus. Lorsque RussionMini peut appeler le code Hamster pour faire quelque chose, puis 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 à l'heure mais qu'un Russion mini ne peut courir qu'à moitié moins vite. Nous pouvons coder en dur 3/2 dans RussionMini mais si cette valeur devait changer, nous aurions plusieurs endroits dans le code où elle devrait être modifiée. 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 codez en dur Hamster.prototype. Il peut y avoir des modèles qui vous donneront l'avantage de super comme en Java.

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

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

ce.constructeur

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

Si, après avoir défini la partie prototype de l'héritage, vous devriez la 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 valent 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 mobile mais un chat peut se déplacer. Dans un langage basé sur les classes, Cat devrait implémenter Movable. En JavaScript, nous pouvons définir Movable et définir l'implémentation ici, Cat peut soit remplacer, étendre ou utiliser l'implémentation par défaut.

Pour Movable, nous avons des membres spécifiques à l'instance (comme location). Et nous avons des membres qui ne sont pas spécifiques à l'instance (comme la fonction move ()). Les membres spécifiques à l'instance seront définis en appelant mxIns (ajoutés par la fonction d'assistance mixin) lors de la création d'une instance. Les membres du prototype seront copiés un par un sur Cat.prototype de Movable.prototype en utilisant la fonction d'assistance 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 mêmes fonctions nommées par tout mélange qui est mélangé en dernier.

La variable this

Dans tout l'exemple de code, vous verrez this faisant référence à l'instance actuelle.

La variable this fait en fait référence à l'objet appelant, elle fait référence à l'objet qui a précédé la fonction.

Pour clarifier voir le code suivant:

theInvokingObject.thefunction();

Les cas où cela ferait référence au mauvais objet sont généralement lors de la connexion d'écouteurs d'événements, de rappels ou de délais d'expiration et d'intervalles. Dans les 2 lignes de code suivantes, nous pass la fonction, nous ne l'invoquons pas. Passer la fonction est: someObject.aFunction Et l'invoquer est: someObject.aFunction(). La valeur this ne fait pas référence à l'objet sur lequel la fonction a été déclarée mais à l'objet qui invokes la.

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

Pour que this dans les cas ci-dessus se réfère à someObject, 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 retournent une fonction pour fermetures sur le prototype pour avoir un contrôle fin sur les variables qui sont incluses dans la portée fermeture .

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 des arguments (constructeur)

Lorsque Child 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 des arguments.

Je passe généralement un objet à une fonction et je mets cette fonction en fonction de ce dont elle a besoin (définir les valeurs par défaut), puis cette fonction le transmettra à une autre fonction qui fera de même et ainsi de suite 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 calculerait un total de quelque chose et plus tard, vous voudriez re-factoriser le total de ce quelque chose pour qu'il soit dans une certaine devise, vous devrez peut-être modifier de nombreuses fonctions pour transmettre la valeur de la devise. Vous pouvez augmenter la portée d'une valeur monétaire (même à l'échelle mondiale comme window.currency='USD'), Mais c'est une mauvaise façon de la résoudre.

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

Variables privées

JavaScript n'a pas de modificateur privé.

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

Vous pouvez indiquer à d'autres programmeurs qu'un membre est censé être privé en le nommant _aPrivate Ou en plaçant toutes les variables privées dans une variable objet appelée _.

Vous pouvez implémenter des membres privés via fermetures mais les membres privés spécifiques à l'instance ne sont accessibles que par des fonctions qui ne sont pas sur le prototype.

Ne pas implémenter des éléments privés en tant que fermetures entraînerait une fuite d'implémentation et permettrait à vous ou aux utilisateurs d'étendre votre code pour utiliser des membres qui ne font pas partie de votre API publique. Cela peut être bon et mauvais à la fois.

C'est bien car cela vous permet, ainsi qu'à d'autres, de se moquer de certains membres pour les tester facilement. Cela donne aux autres une chance d'améliorer (patcher) facilement votre code mais cela est également mauvais car il n'y a aucune garantie qu'une prochaine version de votre code aura la même implémentation et ou des membres privés.

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

Si vous insistez toujours sur les privés, le modèle suivant peut vous aider. Cependant, il n'implémente pas private mais implémente protected.

172
HMR

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

Hamster.prototype.food = []

Chaque instance de Hamster partagera ce tableau

Si vous avez besoin (et vous le faites dans ce cas) d'instances distinctes de collections d'aliments 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 la propriété nulle part dans la chaîne de prototype, il crée la propriété sur l'objet cible.

15
BLSully