Qu'est-ce que «cela» et que mange-t-il


Photo de Sebastian Herrmann .

Bonjour mes amis!

Je vous présente la traduction de l'article de Daniel James «Qu'est-ce que« ceci »? Pourquoi cela? ” .

Qu'est-ce que «cela» et que mange-t-il


Quand j'ai commencé à apprendre JavaScript, le concept de cela m'a semblé extrêmement déroutant.

Présentation


La croissance rapide de la popularité de JS s'explique en partie par un seuil d'entrée bas. Des fonctionnalités telles que les fonctions et cela fonctionnent généralement comme prévu. Pour devenir un professionnel de JS, vous n'avez pas besoin de connaître beaucoup de petits détails et de détails (je dirais avec cela - environ Per.). Mais une fois, chaque développeur rencontre une erreur causée par la valeur de ceci.

Après cela, vous avez envie de comprendre comment cela fonctionne dans JS. S'agit-il d'un concept de programmation orientée objet (POO)? JS est-il un langage de programmation orienté objet (OOJP)? Si vous "google" cela, vous recevrez en réponse la mention de certains prototypes. Quel genre de prototypes? À quoi servait le «nouveau» mot clé avant que les classes n'apparaissent dans JS?

Toutes ces choses sont étroitement liées. Mais avant d'expliquer comment cela fonctionne, je vais me permettre une petite digression. Je veux parler un peu de la raison pour laquelle JS est ce qu'il est.

POO dans JS


Le paradigme de programmation (héritage) de prototype dans JS est l'une des caractéristiques de la POO. Même avant l'avènement des classes JS, il y avait OOJP. JS est un langage simple qui utilise seulement quelques éléments de la POO. Les plus importants d'entre eux sont les fonctions, les fermetures, ceci, les prototypes, les littéraux d'objet et le nouveau mot-clé.

Encapsulation et réutilisabilité avec fermetures


Créons la classe Counter. Cette classe doit avoir des méthodes pour réinitialiser et incrémenter le compteur. Nous pouvons écrire quelque chose comme ceci:

function Counter(initialValue = 0){ let _count = initialValue return { reset: function(){ _count = 0 }, next: function(){ return ++_count } } } const myCounter = Counter() console.log(myCounter.next()) // 1 

Dans ce cas, nous nous sommes limités à utiliser des fonctions et des littéraux d'objet sans cela ou nouveau. Oui, nous avons déjà reçu quelque chose de la POO. Nous avons la possibilité de créer de nouvelles instances de Counter. Chaque instance de Counter a son propre nombre de variables internes. Nous avons implémenté l'encapsulation et la réutilisation de manière purement fonctionnelle.

Problème de performance


Supposons que nous écrivons un programme qui utilise un grand nombre de compteurs. Chaque compteur aura ses propres méthodes de réinitialisation et ensuite (Counter (). Reset! = Counter (). Reset). La création de telles fermetures pour chaque méthode de chaque instance nécessitera une énorme quantité de mémoire! Une telle architecture est «insoutenable». Par conséquent, nous devons trouver un moyen de stocker dans chaque instance de Counter uniquement des références aux méthodes qu'il utilise (en fait, c'est ce que font tous les OOJP, comme Java).

Nous pourrions résoudre ce problème comme suit (sans impliquer des fonctionnalités de langue supplémentaires):

 let Counter = { reset: function(counter){ counter._count = 0 }, next: function(counter){ return ++counter._count }, new: function(initialValue = 0){ return { _count: initialValue } } } const myCounter = Counter.new() console.log(Counter.next(myCounter)) // 1 

Cette approche résout le problème de performance, mais nous avons dû faire un compromis sérieux, qui consiste en la nécessité d'une participation élevée du programmeur à l'exécution du programme (en s'assurant que le code fonctionne). Sans outils supplémentaires, il faudrait se contenter de cette approche.

Cela se précipite à la rescousse


Nous réécrivons notre exemple en utilisant ceci:

 let Counter = { reset: function(){ this._count = 0 }, next: function(){ return ++this._count }, new: function(initialValue = 0){ return { _count: initialValue, //          reset: Counter.reset, next: Counter.next } } } const myCounter = Counter.new() myCounter.next() // ,  reset     myCounter (Counter.new()).reset() console.log(myCounter.next()) // 2 

Notez que nous continuons à créer des fonctions simples de réinitialisation et ensuite (Counter.new (). Reset == Counter.new (). Reset). Dans l'exemple précédent, pour que le programme fonctionne, nous avons été obligés de fournir un descripteur d'instance pour les méthodes implémentées conjointement. Maintenant, nous appelons simplement myCounter.next () et faisons référence à l'instance utilisant ceci. Mais comment ça marche? Reset et next sont déclarés dans l'objet Counter. Comment JS sait-il à quoi cela se réfère lors de l'appel d'une fonction?

Appel de fonction dans JS


Vous savez très bien que les fonctions de JS ont une méthode d'appel (il y a aussi une méthode apply; la différence entre ces méthodes n'est pas significative. La différence est dans la façon dont nous passons les paramètres: in apply en tant que tableau, en appel séparé par des virgules - environ Per.) . En utilisant call, vous décidez de ce que cela signifie lorsque la fonction est appelée:

 const myCounter = Counter.new() Counter.next.call(myCounter) 

C'est en fait ce que fait la notation par points derrière la scène lorsque nous appelons la fonction. lhs.fn () est identique à fn.call (lhs).

Il s'agit donc d'un identifiant spécial qui est défini lors de l'appel de la fonction.

Les problèmes commencent


Supposons que vous souhaitiez créer un compteur et incrémenter sa valeur à chaque seconde. Voici comment procéder:

 const myCounter = Counter.new() setInterval(myCounter.next, 1000) //    console.log(`Why is ${myCounter.next()} still 0?`) //  myCounter.next()    0? 

Voyez-vous une erreur ici? Lorsque setInterval démarre, la valeur de ceci n'est pas définie, donc rien ne se passe. Ce problème peut être résolu comme suit:

 const myCounter = Counter.new() setInterval(function(){ myCounter.next() }, 1000) 

Un peu de lier


Il existe une autre façon de résoudre ce problème:

 function bindThis(fn, _this){ return function(...args){ return fn.call(_this, ...args) } } const myCounter = Counter.new() setInterval(bindThis(myCounter.next, myCounter), 1000) 

En utilisant la fonction d'usine bindThis, nous pouvons être sûrs que Counter.next appelle toujours myCounter comme ceci, quelle que soit la façon dont la nouvelle fonction est appelée. En fait, nous ne changeons pas la fonction de Counter.next. JS a une méthode de liaison intégrée. Par conséquent, nous pouvons réécrire l'exemple ci-dessus comme ceci: setInterval (myCounter.next.bind (myCounter), 1000).

Nous travaillons avec des prototypes


En ce moment, nous avons une belle classe Counter, mais elle est toujours un peu "tordue". Ce sont les lignes suivantes:

 // ... reset: Counter.reset, next: Counter.next, // ... 

Nous avons besoin d'un meilleur moyen de partager les méthodes de classe avec ses instances. Les prototypes en font un excellent travail. Si vous vous référez à la propriété d'une fonction ou d'un objet qui n'existe pas, JS recherchera cette propriété dans le prototype de cette fonction ou de cet objet (puis dans le prototype du prototype et ainsi de suite au Object.prototype situé en haut de la chaîne du prototype - environ Trans.). Vous pouvez définir le prototype d'un objet à l'aide d'Object.setPrototypeOf. Réécrivons notre classe Counter en utilisant des prototypes:

 let Counter = { reset: function(){ this._count = 0 }, next: function(){ return ++this._count }, new: function(initialValue = 0){ this._count = initialValue } } function newInstanceOf(klass, ...args){ //       "klass",  "class"   const instance = {} Object.setPrototypeOf(instance, klass) instance.new(...args) return instance } const myCounter = newInstanceOf(Counter) console.log(myCounter.next()) // 1 

Mot-clé "nouveau"


L'utilisation de setPrototypeOf est très similaire à la façon dont fonctionne le "nouvel" opérateur. La différence est que new utilisera le constructeur prototype de la fonction passée. Par conséquent, au lieu de créer un objet pour nos méthodes, nous les passons au prototype du constructeur de la fonction:

 function Counter(initialValue = 0){ this._count = initialValue } Counter.prototype.reset = function(){ this._count = 0 } Counter.prototype.next = function(){ return ++this._count } const myCounter = new Counter() console.log(`${myCounter.next()}`) // 1 

Enfin, nous avons le code sous la forme dans laquelle il peut être trouvé dans la pratique. Avant l'apparition des classes dans JS, c'était l'approche standard pour créer et initialiser des classes.

Mot-clé "classe"


J'espère que vous comprenez maintenant pourquoi nous utilisons le prototype du constructeur de fonction et comment cela fonctionne dans les méthodes de fonction. Cependant, notre code peut être amélioré. Heureusement, aujourd'hui, dans JS, il existe une meilleure façon de déclarer des classes:

 class Counter { reset(){ this._count = 0 } next(){ return ++this._count } constructor(initialValue = 0){ this._count = initialValue } } const myCounter = new Counter() console.log(`${myCounter.next()}`) // 1 

Le mot-clé «classe» ne fait rien spécialement sous «chat». Vous pouvez le considérer comme du sucre syntaxique, un wrapper pour l'approche «prototype». Si vous exécutez le transporteur orienté vers ES3, vous obtiendrez quelque chose comme ceci:

 var Counter = /** @class **/ (function(){ function Counter(initialValue){ if(initialValue === void 0) { initialValue = 0 } this._count = initialValue } Counter.prototype.reset = function(){ this._count = 0 } Counter.prototype.next = function(){ ++this._count } return Counter }()); var myCounter = new Counter() console.log(myCounter.next()) 

Notez que le transpilateur a généré du code qui est presque identique à l'exemple précédent.

Fonctions fléchées


Si vous écrivez du code en JS depuis 5 ans, vous serez peut-être surpris que je mentionne les fonctions fléchées. Mon conseil: utilisez toujours les fonctions flèches jusqu'à ce que vous ayez vraiment besoin d'une fonction régulière. Il se trouve que la définition du constructeur et des méthodes de la classe est juste le cas lorsque nous devons utiliser des fonctions ordinaires. L'une des caractéristiques des fonctions fléchées est l'obscurcissement.

Cette fonction fléchée


Certains peuvent supposer que les fonctions fléchées en prennent la valeur actuelle lors de leur création. C'est incorrect d'un point de vue technique (le sens de cela n'est pas défini, il est tiré de l'environnement lexical), mais c'est un bon modèle mental. Une fonction flèche comme celle-ci:

 const myArrowFunction = () => { this.doSomething() } 

Vous pouvez le réécrire comme ceci:
 const _this = this const myRegularFunction = function(){ _this.doSomething() } 

Merci de votre attention. Mes meilleurs vœux.

Source: https://habr.com/ru/post/fr486048/


All Articles