Définir des méthodes via un prototype vs utiliser cela dans le constructeur – vraiment une différence de performance?

Dans JavaScript, nous avons deux façons de faire une «classe» et de lui donner des fonctions publiques.

Méthode 1:

function MyClass() { var privateInstanceVariable = 'foo'; this.myFunc = function() { alert(privateInstanceVariable ); } } 

Méthode 2:

 function MyClass() { } MyClass.prototype.myFunc = function() { alert("I can't use private instance variables. :("); } 

J'ai lu de nombreuses fois que les gens disent que l'utilisation de la Méthode 2 est plus efficace car toutes les instances partagent la même copie de la fonction que les autres. La définition de fonctions via le prototype présente cependant un énorme inconvénient: il est impossible d'avoir des variables d'instance privées.

Bien que, en théorie, l'utilisation de la Méthode 1 donne à chaque instance d'un objet sa propre copie de la fonction (et utilise ainsi beaucoup plus de mémoire, sans parler du temps requis pour les allocations): c'est ce qui se passe réellement dans la pratique? Il semble qu'une optimisation des navigateurs Web pourrait facilement se faire est de reconnaître ce modèle extrêmement commun, et ont en fait toutes les instances de référence de l'objet la même copie de fonctions définie par ces "fonctions constructeur". Ensuite, il ne pouvait donner à une instance sa propre copie de la fonction si elle était modifiée explicitement plus tard.

Tout un aperçu – ou, mieux encore, une expérience du monde réel – sur les différences de performance entre les deux, serait extrêmement utile.

Voir http://jsperf.com/prototype-vs-this

La déclaration de vos méthodes via le prototype est plus rapide, mais si cela est pertinent est discutable.

Si vous avez un goulot d'étranglement de performance dans votre application, il est peu probable qu'il s'agisse de cela, à moins que vous n'arriviez plus 100 000 objets à chaque étape d'une animation arbitraire, par exemple.

Si la performance est une préoccupation sérieuse et que vous souhaitez micro-optimiser, je suggérerais de déclarer via un prototype. Sinon, utilisez le motif qui vous intéresse le plus.

Je ajouterai que, en JavaScript, il existe une convention de propriétés de préfixe qui doit être considérée comme privée avec un trait de soulignement (par exemple, _process() ). La plupart des développeurs comprendront et éviteront ces propriétés, à moins qu'ils ne soient prêts à renoncer au contrat social, mais dans ce cas, vous pourriez ne pas leur répondre. Ce que je veux dire, c'est que: vous n'avez probablement pas besoin de vraies variables privées …

Dans la nouvelle version de Chrome, this.method est environ 20% plus rapide que prototype.method, mais la création d'un nouvel objet est encore plus lente.

Si vous pouvez réutiliser l'objet au lieu de créer toujours un nouveau, cela peut être 50% – 90% plus rapide que la création de nouveaux objets. Plus le bénéfice de la collecte des ordures, ce qui est énorme:

http://jsperf.com/prototype-vs-this/59

Cela ne fait qu'une différence lorsque vous créez beaucoup d'exemples. Sinon, la performance de l'appel de la fonction membre est exactement la même dans les deux cas.

J'ai créé un cas de test sur jsperf pour démontrer ceci:

http://jsperf.com/prototype-vs-this/10

Vous n'avez peut-être pas considéré cela, mais mettre la méthode directement sur l'objet est en fait mieux d'une manière:

  1. Les invocations de méthode sont très légèrement plus rapides ( jsperf ) car la chaîne prototype ne doit pas être consultée pour résoudre la méthode.

Cependant, la différence de vitesse est presque négligeable. En plus, mettre une méthode sur un prototype est meilleur en deux manières:

  1. Plus rapide pour créer des instances ( jsperf )
  2. Utilise moins de mémoire

Comme James l'a dit, cette différence peut être importante si vous implémentez des milliers d'instances d'une classe.

Cela dit, je peux certainement imaginer un moteur de JavaScript qui reconnaisse que la fonction que vous attachez à chaque objet ne change pas entre les instances et ne conserve donc qu'une copie de la fonction en mémoire, toutes les méthodes d'instance indiquant la fonction partagée. En fait, il semble que Firefox effectue une optimisation spéciale comme celle-ci, mais Chrome n'est pas.


DE CÔTÉ:

Vous avez raison qu'il est impossible d'accéder aux variables d'instance privée à partir de méthodes internes sur les prototypes. Donc, je suppose que la question que vous devez poser vous-même est-ce que vous appréciez la possibilité de rendre les variables d'instance vraiment privées en utilisant l'héritage et le prototypage? Personnellement, je pense que rendre les variables vraiment privées n'est pas si important et utiliserait simplement le préfixe de soulignement (par exemple, «this._myVar») pour signaler que bien que la variable soit publique, elle devrait être considérée comme privée. Cela dit, dans ES6, il y a apparemment un moyen d'avoir les deux des deux mondes!

En bref, utilisez la méthode 2 pour créer des propriétés / méthodes que toutes les instances partageront. Ceux-ci seront «globaux» et toute modification sera réfléchie dans toutes les instances. Utilisez la méthode 1 pour créer des propriétés / méthodes spécifiques à l'instance.

J'aimerais avoir une meilleure référence, mais maintenant, jetez un oeil à cela . Vous pouvez voir comment j'ai utilisé les deux méthodes dans le même projet à des fins différentes.

J'espère que cela t'aides. 🙂

Cette réponse devrait être considérée comme une extension du reste des réponses remplissant les points manquants. L'expérience personnelle et les repères sont incorporés.

En ce qui concerne mon expérience, j'utilise les constructeurs pour construire littéralement mes objets religieusement, que les méthodes soient privées ou non. La raison principale étant que lorsque j'ai commencé, c'était l'approche immédiate la plus simple pour moi, ce n'est pas une préférence particulière. Il a peut-être été aussi simple que j'aime l'encapsulation visible et les prototypes sont un peu désincorporés. Mes méthodes privées seront également attribuées comme variables dans la portée. Bien que ce soit mon habitude et garde les choses bien adaptées, ce n'est pas toujours la meilleure habitude et je frappe parfois les murs. Outre les scénarios rudes avec un auto-assemblage très dynamique en fonction des objets de configuration et de la mise en page du code, il est généralement l'approche la plus faible, en particulier si la performance est une préoccupation. Savoir que les internes sont privés, c'est utile, mais vous pouvez le faire par d'autres moyens avec la bonne discipline. Sauf si la performance est une considération sérieuse, utilisez tout ce qui fonctionne mieux sinon pour la tâche à accomplir.

  1. L'utilisation d'un héritage de prototype et d'une convention pour marquer les objets en tant que privé rend le débogage plus facile car vous pouvez ensuite traverser facilement le graphe d'objet de la console ou du débogueur. D'autre part, une telle convention rend l'obfuscation un peu plus difficile et facilite l'accès des autres à leurs propres scripts sur votre site. C'est l'une des raisons pour lesquelles l'approche de la portée privée a gagné en popularité. Ce n'est pas une vraie sécurité mais ajoute plutôt une résistance. Malheureusement, beaucoup de gens pensent toujours que c'est vraiment un moyen de programmer un JavaScript sécurisé. Étant donné que les débogueurs sont vraiment bons, l'obfuscation du code prend sa place. Si vous cherchez des défauts de sécurité où le client est trop, c'est un motif de conception que vous pourriez vouloir faire attention.
  2. Une convention vous permet d'avoir des propriétés protégées avec peu d'agitation. Cela peut être une bénédiction et une malédiction. Cela soulage certains problèmes d'héritage car il est moins restrictif. Vous avez toujours le risque de collision ou de charge cognitive accrue en considérant où d'autre une propriété pourrait être consultée. Les objets d'assemblage automatique vous permettent de faire des choses étranges où vous pouvez contourner un certain nombre de problèmes d'héritage, mais ils peuvent être non conventionnels. Mes modules ont tendance à avoir une structure intérieure riche où les choses ne sont pas retirées jusqu'à ce que la fonctionnalité soit nécessaire ailleurs (partagée) ou exposée, sauf si nécessaire à l'extérieur. Le modèle de constructeur tend à conduire à créer des modules sophistiqués autonomes plus que simplement des objets fragmentés. Si vous voulez cela, c'est bien. Sinon, si vous voulez une structure et une structure OOP plus traditionnelles, je proposerais probablement de réglementer l'accès par convention. Dans mes scénarios d'utilisation, l'OOP complexe n'est pas souvent justifié et les modules font l'affaire.
  3. Tous les tests ici sont minimes. Dans l'utilisation du monde réel, il est probable que les modules seront plus complexes rendant le coup beaucoup plus grand que les tests indiqués ici. Il est assez commun d'avoir une variable privée avec plusieurs méthodes qui fonctionnent sur elle et chacune de ces méthodes ajoutera plus de frais généraux lors de l'initialisation que vous ne recevrez pas avec l'héritage de prototype. Dans la plupart des cas, ce n'est pas grave parce que seules quelques instances de ces objets flottent, bien que cumulativement, cela pourrait s'accumuler.
  4. On suppose que les méthodes de prototypes sont plus lentes à appeler en raison de la recherche de prototypes. Ce n'est pas une hypothèse injuste, j'ai même fait moi même jusqu'à ce que je l'ai testé. En réalité, il est complexe et certains tests suggèrent que cet aspect est trivial. Entre, prototype.m = f , this.m = f et this.m = function... ce dernier se comporte nettement mieux que les deux premiers qui se this.m = function... autour de la même. Si la recherche de prototypes seule était une question importante, alors les deux dernières fonctions au lieu de cela n'auraient plus la première. Au lieu de cela, quelque chose d'autre est étrange, au moins, en ce qui concerne Canary. Les fonctions possibles sont optimisées selon leur appartenance. Une multitude de considérations de performance entrent en jeu. Vous avez également des différences pour l'accès aux paramètres et l'accès variable.
  5. Capacité mémoire. Il n'est pas bien discuté ici. Une hypothèse que vous pouvez faire face à l'avance, ce qui est vraisemblable, c'est que l'héritage du prototype sera généralement beaucoup plus efficace et d'après mes tests, il est en général. Lorsque vous créez votre objet dans votre constructeur, vous pouvez supposer que chaque objet aura probablement sa propre instance de chaque fonction plutôt que partagée, une carte de propriété plus grande pour ses propres propriétés personnelles et probablement des frais généraux pour garder la portée du constructeur ouverte aussi. Les fonctions qui fonctionnent sur le plan privé sont extrêmement exigeantes et exigeantes pour la mémoire. Je trouve que dans beaucoup de scénarios, la différence proportionnelle dans la mémoire sera beaucoup plus importante que la différence proportionnelle dans les cycles CPU.
  6. Mémoire graphique. Vous pouvez également bloquer le moteur pour rendre GC plus cher. Les profils ont tendance à montrer le temps consacré au GC ces jours-ci. Ce n'est pas seulement un problème lorsqu'il s'agit d'attribuer et de libérer davantage. Vous créerez également un graphique d'objet plus large pour traverser et des choses comme ça afin que le GC consomme plus de cycles. Si vous créez un million d'objets et ne les touchez guère, selon le moteur, il pourrait s'avérer avoir plus d'impact sur la performance ambiante que prévu. J'ai prouvé que cela permet au moins de faire fonctionner la gc plus longtemps lorsque les objets sont éliminés. Il y a tendance à être une corrélation avec la mémoire utilisée et le temps nécessaire à la GC. Cependant, il y a des cas où l'heure est la même quelle que soit la mémoire. Cela indique que le maquillage du graphique (couches d'impartition, nombre d'éléments, etc.) a plus d'impact. Ce n'est pas quelque chose qui est toujours facile à prévoir.
  7. Beaucoup de personnes n'utilisent pas beaucoup de prototypes enchaînés, même si je dois l'admettre. Les chaînes de prototypes peuvent être coûteuses en théorie. Quelqu'un le fera, mais je n'ai pas mesuré le coût. Si vous construisez plutôt vos objets entièrement dans le constructeur et que vous avez une chaîne d'héritage car chaque constructeur appelle un constructeur parent sur lui-même, en théorie l'accès aux méthodes devrait être beaucoup plus rapide. D'autre part, vous pouvez accomplir l'équivalent si cela importe (comme aplatir les prototypes en bas de la chaîne des ancêtres) et vous n'affez pas de rompre des choses comme hasOwnProperty, peut-être instanceof, etc. si vous en avez vraiment besoin. Dans les deux cas, les choses commencent à se complexifier une fois que vous descendez cette route en ce qui concerne les hacks de performance. Vous allez probablement finir par faire des choses que vous ne devriez pas faire.
  8. Beaucoup de gens n'utilisent pas directement l'une ou l'autre approche que vous avez présentée. Au lieu de cela, ils font leurs propres choses en utilisant des objets anonymes permettant la méthode de partage de n'importe quelle manière (mixins par exemple). Il existe également un certain nombre de cadres qui mettent en œuvre leurs propres stratégies d'organisation de modules et d'objets. Ce sont des approches personnalisées basées sur des conventions. Pour la plupart des gens et pour vous, votre premier défi devrait être l'organisation plutôt que la performance. Ceci est souvent compliqué dans la mesure où Javascript offre de nombreuses façons de réaliser des choses par rapport à des langues ou des plates-formes avec un support OOP / espace de noms / module plus explicite. En ce qui concerne la performance, je dirais plutôt éviter les pièges majeurs avant tout.
  9. Il existe un nouveau type de symboles qui devrait fonctionner pour des variables et des méthodes privées. Il existe plusieurs façons d'utiliser ceci et soulève une foule de questions liées à la performance et à l'accès. Dans mes tests, la performance de Symbols n'était pas géniale par rapport à tout autre chose, mais je ne les ai jamais testés à fond.

Avertissements:

  1. Il y a beaucoup de discussions sur les performances et il n'y a pas toujours une réponse permanente pour cela car les scénarios d'utilisation et les moteurs changent. Toujours le profil mais aussi mesurer toujours de plusieurs façons car les profils ne sont pas toujours précis ou fiables. Évitez les efforts importants d'optimisation à moins qu'il n'y ait définitivement un problème démontrable.
  2. Il est probablement préférable d'inclure des contrôles de performance pour les zones sensibles dans les tests automatisés et de fonctionner lors de la mise à jour des navigateurs.
  3. Rappelez-vous que la durée de vie de la batterie est importante ainsi que des performances perceptibles. La solution la plus lente pourrait s'avérer plus rapide après avoir exécuté un compilateur d'optimisation (IE, un compilateur pourrait avoir une meilleure idée de l'accès aux variables de portée restreintes que des propriétés marquées comme privées par convention). Considérons le backend tel que node.js. Cela peut nécessiter une meilleure latence et un meilleur débit que celui que vous trouverez souvent sur le navigateur. La plupart des gens ne devront pas s'inquiéter de ces choses avec quelque chose comme la validation d'un formulaire d'inscription, mais le nombre de scénarios variés où de telles choses peuvent être importantes augmente.
  4. Vous devez faire attention aux outils de suivi de l'allocation de mémoire afin de persévérer dans le résultat. Dans certains cas où je ne suis pas retourné et persiste, les données ont été totalement optimisées ou le taux d'échantillonnage n'était pas suffisant entre instancié / non référencé, ce qui m'a permis de me gratter la manière dont un tableau initialisé et rempli à un million enregistré comme 3.4KiB Dans le profil d'allocation.
  5. Dans le monde réel, dans la plupart des cas, la seule façon d'optimiser une application est de l'écrire en premier lieu afin que vous puissiez le mesurer. Il y a des dizaines à des centaines de facteurs qui peuvent entrer en jeu sinon des milliers dans un scénario donné. Les moteurs font également des choses qui peuvent conduire à des caractéristiques de performance asymétriques ou non linéaires. Si vous définissez des fonctions dans un constructeur, elles peuvent être des fonctions fléchées ou traditionnelles, chacune se comporte différemment dans certaines situations et je n'ai aucune idée des autres types de fonctions. Les cours ne se comportent pas non plus comme les performances pour les constructeurs prototypes qui devraient être équivalents. Vous devez également être très prudent avec les benchmarks. Les classes prototypes peuvent avoir différé l'initialisation de différentes façons, surtout si votre prototype a également été créé (conseils, pas). Cela signifie que vous pouvez sous-estimer le coût d'initialisation et exagérer le coût de la mutation de l'accès / propriété. J'ai également vu des indications d'optimisation progressive. Dans ces cas, j'ai rempli un grand tableau avec des instances d'objets identiques et lorsque le nombre d'instances augmente, les objets semblent être optimisés de manière incrémentielle pour la mémoire jusqu'à un point où le reste est le même. Il est également possible que ces optimisations puissent également avoir un impact significatif sur la performance du processeur. Ces choses dépendent fortement non seulement du code que vous écrivez, mais de ce qui se passe en temps d'exécution tel que le nombre d'objets, la variance entre les objets, etc.