web-dev-qa-db-fra.com

Définir des méthodes via un prototype ou utiliser ceci dans le constructeur - vraiment une différence de performance?

En JavaScript, nous avons deux façons de créer 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 à plusieurs reprises que les gens disant que l'utilisation de la méthode 2 est plus efficace car toutes les instances partagent la même copie de la fonction plutôt que chacune en ayant la sienne. 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.

Même si, en théorie, l'utilisation de la méthode 1 donne à chaque instance d'un objet sa propre copie de la fonction (et utilise donc beaucoup plus de mémoire, sans parler du temps requis pour les allocations) - est-ce ce qui se passe réellement dans la pratique? Il semble qu'une optimisation que les navigateurs Web pourraient facilement faire consiste à reconnaître ce modèle extrêmement courant et à avoir en fait toutes les instances de la référence d'objet la même copie de fonctions définis via ces "fonctions constructeurs". Ensuite, il ne peut donner à une instance sa propre copie de la fonction que si elle est explicitement modifiée ultérieurement.

Toute information - ou, mieux encore, expérience du monde réel - sur les différences de performances entre les deux, serait extrêmement utile.

60
MgSam

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

Déclarer vos méthodes via le prototype est plus rapide, mais le fait de savoir si cela est pertinent est discutable.

Si vous avez un goulot d'étranglement dans les performances de votre application, il est peu probable que ce soit le cas, sauf si vous instanciez 10000+ objets à chaque étape d'une animation arbitraire, par exemple.

Si les performances sont une préoccupation sérieuse et que vous souhaitez micro-optimiser, je suggère de déclarer via prototype. Sinon, utilisez simplement le modèle qui vous convient le mieux.

J'ajouterai que, en JavaScript, il existe une convention de préfixation des propriétés qui sont censées être considérées comme privées 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 tout aussi bien ne pas y répondre. Ce que je veux dire, c'est que: vous n'avez probablement pas vraiment besoin de vraies variables privées ...

61
James

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 d'en créer toujours un nouveau, cela peut être 50% - 90% plus rapide que la création de nouveaux objets. De plus, l'avantage de l'absence de collecte des ordures, ce qui est énorme:

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

2
Yongtao Wang

Cela ne fait une différence que lorsque vous créez de nombreuses instances. Sinon, les performances de l'appel de la fonction membre sont exactement les mêmes dans les deux cas.

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

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

1
typeracer

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

  1. Les invocations de méthodes sont très légèrement plus rapides ( jsperf ) car la chaîne prototype n'a pas à consulter pour résoudre la méthode.

Cependant, la différence de vitesse est presque négligeable. En plus de cela, il est préférable de mettre une méthode sur un prototype de deux manières plus percutantes:

  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 instanciez des milliers d'instances d'une classe.

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


À CÔTÉ:

Vous avez raison, il est impossible d'accéder aux variables d'instance privées à partir des méthodes internes des prototypes. Donc, je suppose que la question que vous devez vous poser est la suivante: appréciez-vous la possibilité de rendre les variables d'instance vraiment privées plutôt que d'utiliser l'héritage et le prototypage? Personnellement, je pense que rendre les variables vraiment privées n'est pas si important et utiliserais simplement le préfixe de soulignement (par exemple, "this._myVar") pour signifier 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 mondes!

1
Niko Bellic

Vous pouvez utiliser cette approche et elle vous permettra d'utiliser prototype et d'accéder aux variables d'instance.

var Person = (function () {
    function Person(age, name) {
        this.age = age;
        this.name = name;
    }

    Person.prototype.showDetails = function () {
        alert('Age: ' + this.age + ' Name: ' + this.name);
    };

    return Person; // This is not referencing `var Person` but the Person function

}()); // See Note1 below

Note 1:

La parenthèse appellera la fonction (fonction auto-invoquante) et affectera le résultat au var Person.


tilisation

var p1 = new Person(40, 'George');
var p2 = new Person(55, 'Jerry');
p1.showDetails();
p2.showDetails();
0
CodingYoshi

Cette réponse doit ê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 intégrés.

En ce qui concerne mon expérience, j'utilise des constructeurs pour construire littéralement mes objets religieusement, que les méthodes soient privées ou non. La principale raison étant que lorsque j'ai commencé, c'était l'approche immédiate la plus simple pour moi, donc ce n'est pas une préférence particulière. Cela aurait pu être aussi simple que j'aime l'encapsulation visible et les prototypes sont un peu désincarnés. Mes méthodes privées seront également affectées en tant que variables dans la portée. Bien que ce soit mon habitude et que les choses restent bien autonomes, ce n'est pas toujours la meilleure habitude et je frappe parfois des murs. En dehors des scénarios farfelus avec un auto-assemblage hautement dynamique en fonction des objets de configuration et de la disposition du code, cela a tendance à être l'approche la plus faible à mon avis, en particulier si les performances sont un problème. Savoir que les internes sont privés est utile mais vous pouvez y parvenir par d'autres moyens avec la bonne discipline. À moins que les performances ne soient sérieusement prises en compte, utilisez tout ce qui fonctionne le mieux pour la tâche à accomplir.

  1. L'utilisation de l'héritage de prototype et d'une convention pour marquer les éléments comme privés facilite le débogage, car vous pouvez ensuite parcourir facilement le graphique de l'objet à partir de la console ou du débogueur. D'un autre côté, une telle convention rend l'obscurcissement un peu plus difficile et permet aux autres de verrouiller leurs propres scripts sur votre site. C'est l'une des raisons pour lesquelles l'approche de portée privée a gagné en popularité. Ce n'est pas de la vraie sécurité, mais cela ajoute de la résistance. Malheureusement, beaucoup de gens pensent toujours que c'est vraiment un moyen de programmer du JavaScript sécurisé. Puisque les débogueurs sont devenus vraiment bons, l'obscurcissement de code prend sa place. Si vous recherchez des failles de sécurité où le client en a trop, c'est un modèle de conception que vous voudrez peut-être rechercher.
  2. Une convention vous permet d'avoir des propriétés protégées en toute simplicité. Cela peut être une bénédiction et une malédiction. Il atténue certains problèmes d'héritage car il est moins restrictif. Vous avez toujours le risque de collision ou d'augmentation de la charge cognitive lorsque vous envisagez où une propriété pourrait être accessible. Les objets auto-assemblés 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 interne riche où les choses ne sont pas retirées jusqu'à ce que la fonctionnalité soit nécessaire ailleurs (partagée) ou exposée à moins qu'elle ne soit nécessaire à l'extérieur. Le modèle constructeur a tendance à conduire à la création de modules sophistiqués autonomes plus que de simples objets fragmentaires. Si vous le voulez, c'est bien. Sinon, si vous voulez une structure et une disposition OOP plus traditionnelles, je suggérerais probablement de réglementer l'accès par convention. Dans mes scénarios d'utilisation, le complexe OOP n'est pas souvent justifié) et les modules font l'affaire.
  3. Tous les tests ici sont minimes. Dans le monde réel, il est probable que les modules seront plus complexes, ce qui rendra le coup beaucoup plus important que les tests ici ne l'indiquent. Il est assez courant d'avoir une variable privée avec plusieurs méthodes qui travaillent dessus et chacune de ces méthodes ajoutera plus de surcharge à l'initialisation que vous n'obtiendrez pas avec l'héritage de prototype. Dans la plupart des cas, cela n'a pas d'importance car seules quelques instances de ces objets flottent, bien que cumulativement, cela puisse s'additionner.
  4. Il est supposé que les méthodes de prototype sont plus lentes à appeler en raison de la recherche de prototype. Ce n'est pas une hypothèse injuste, j'ai fait la même chose jusqu'à ce que je la teste. En réalité, c'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 fonctionne nettement mieux que les deux premiers qui fonctionnent de la même manière. Si la recherche de prototype seule était un problème important, les deux dernières fonctions auraient à la place des performances significatives. Au lieu de cela, quelque chose d'autre d'étrange se passe au moins en ce qui concerne les Canaries. Il est possible que les fonctions soient optimisées en fonction de leurs membres. Une multitude de considérations de performances entrent en jeu. Vous avez également des différences pour l'accès aux paramètres et l'accès aux variables.
  5. Capacité mémoire. Ce n'est pas bien discuté ici. Une hypothèse que vous pouvez faire à l'avance qui est probablement vraie est que l'héritage du prototype sera généralement beaucoup plus efficace en mémoire et selon mes tests, il l'est en général. Lorsque vous construisez 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és plus grande pour ses propres propriétés personnelles et probablement une surcharge pour garder la portée du constructeur ouverte également. Les fonctions qui opèrent sur la portée privée sont extrêmement et disproportionnellement exigeantes en mémoire. Je trouve que dans de nombreux scénarios, la différence proportionnelle de mémoire sera beaucoup plus importante que la différence proportionnelle de cycles CPU.
  6. Graphique de mémoire. Vous pouvez également bloquer le moteur, ce qui rend le GC plus cher. Les profileurs ont tendance à montrer le temps passé au GC ces jours-ci. Ce n'est pas seulement un problème lorsqu'il s'agit d'allouer et de libérer plus. Vous allez également créer un graphique d'objet plus grand à parcourir et des choses comme ça pour que le GC consomme plus de cycles. Si vous créez un million d'objets et que vous les touchez à peine, selon le moteur, il se peut que l'impact sur les performances ambiantes soit plus important que prévu. J'ai prouvé que cela fait au moins faire fonctionner le gc plus longtemps lorsque les objets sont éliminés. Autrement dit, il a tendance à être une corrélation avec la mémoire utilisée et le temps qu'il faut pour GC. Cependant, il existe des cas où l'heure est la même quelle que soit la mémoire. Cela indique que la composition du graphique (couches d'indirection, nombre d'éléments, etc.) a plus d'impact. Ce n'est pas quelque chose qui est toujours facile à prévoir.
  7. Peu de gens utilisent largement les prototypes enchaînés, y compris moi-même, je dois l'admettre. Les chaînes prototypes peuvent être chères en théorie. Quelqu'un le fera, mais je n'ai pas mesuré le coût. Si vous construisez à la place vos objets entièrement dans le constructeur et que vous avez ensuite 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'un autre côté, vous pouvez obtenir l'équivalent si cela est important (comme aplatir les prototypes le long de la chaîne des ancêtres) et cela ne vous dérange pas de casser des choses comme hasOwnProperty, peut-être instanceof, etc. si vous en avez vraiment besoin. Dans les deux cas, les choses commencent à devenir complexes une fois que vous avez suivi cette voie en ce qui concerne les hacks de performance. Vous finirez probablement 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 créent leurs propres objets en utilisant des objets anonymes permettant le partage de méthode dans tous les sens (mixins par exemple). Il existe également un certain nombre de cadres qui mettent en œuvre leurs propres stratégies d'organisation des modules et des objets. Il s'agit d'approches personnalisées fortement 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é en ce que Javascript offre de nombreuses façons de réaliser des choses par rapport aux langues ou aux plates-formes avec un support OOP/namespace/module plus explicite. En ce qui concerne les performances, je dirais plutôt d'éviter d'abord les pièges majeurs.
  9. Il existe un nouveau type de symbole censé fonctionner pour les variables et méthodes privées. Il existe plusieurs façons de l'utiliser et cela soulève une multitude de questions liées aux performances et à l'accès. Dans mes tests, les performances des symboles n'étaient pas excellentes par rapport à tout le reste, 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 de réponse permanente à cela à mesure que les scénarios d'utilisation et les moteurs changent. Toujours profiler mais aussi toujours mesurer 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 performances pour les zones sensibles dans les tests automatisés et de les exécuter lorsque les navigateurs sont mis à jour.
  3. N'oubliez pas que la durée de vie de la batterie est importante, ainsi que les performances perceptibles. La solution la plus lente peut s'avérer plus rapide après l'exécution d'un compilateur d'optimisation (IE, un compilateur peut avoir une meilleure idée de l'accès aux variables de portée restreinte que les propriétés marquées comme privées par convention). Considérez le backend tel que node.js. Cela peut nécessiter une latence et un débit meilleurs que ceux que vous trouveriez souvent sur le navigateur. La plupart des gens n'auront pas besoin de s'inquiéter de ces choses avec quelque chose comme la validation d'un formulaire d'inscription, mais le nombre de scénarios divers où de telles choses pourraient avoir une importance augmente.
  4. Vous devez être prudent avec les outils de suivi d'allocation de mémoire pour conserver le résultat. Dans certains cas, lorsque je ne suis pas retourné et que j'ai persisté, les données ont été entièrement optimisées ou le taux d'échantillonnage n'était pas suffisant entre instancié/non référencé, me laissant me gratter la tête sur la façon dont un tableau initialisé et rempli pour un million enregistré sous 3,4 Ko. dans le profil d'allocation.
  5. Dans le monde réel, dans la plupart des cas, la seule façon d'optimiser vraiment une application est de l'écrire en premier lieu afin de pouvoir la 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, il peut s'agir de 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 classes ne se comportent pas de la même manière en termes de performances pour les constructeurs prototypés qui devraient être équivalentes. Vous devez également être très prudent avec les repères. Les classes prototypées peuvent avoir différé l'initialisation de diverses manières, surtout si vous avez également prototypé vos propriétés (conseils, ne le faites pas). Cela signifie que vous pouvez sous-estimer le coût d'initialisation et surévaluer le coût d'accès/de mutation de propriété. J'ai également vu des indications d'optimisation progressive. Dans ces cas, j'ai rempli un grand tableau d'instances d'objets identiques et à mesure que 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 aient également un impact significatif sur les performances du processeur. Ces choses dépendent fortement non seulement du code que vous écrivez, mais de ce qui se passe lors de l'exécution, comme le nombre d'objets, la variance entre les objets, etc.
0
jgmjgm

En bref, utilisez la méthode 2 pour créer des propriétés/méthodes que toutes les instances partageront. Celles-ci seront "globales" et tout changement apporté sera reflété 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 pour l'instant jetez un oeil à this . 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. :)

0
Johnny