JavaScript divertissant: sans accolades

image


JavaScript m'a toujours surpris, tout d'abord, car il ne ressemble probablement à aucun autre langage répandu prenant en charge les deux paradigmes en même temps: une programmation normale et anormale. Et si presque tout a été lu sur les meilleures pratiques et les modèles adéquats, alors le monde merveilleux de la façon dont vous ne devriez pas écrire de code, mais vous pouvez, ne reste que légèrement entrouvert.


Dans cet article, nous analyserons une autre tâche farfelue qui nécessite un abus impardonnable d'une solution normale.


Tâche précédente:



Libellé


Implémentez une fonction décoratrice qui compte le nombre d'appels à la fonction passée et permet d'obtenir ce numéro à la demande. La solution n'utilise pas de crochets et de variables globales.

Le compteur d'appels n'est qu'une excuse, car il y a console.count () . L'essentiel est que notre fonction accumule certaines données lors de l'appel d'une fonction encapsulée et fournit une certaine interface pour y accéder. Cela peut être de sauvegarder tous les résultats d'un appel, de collecter des journaux et une sorte de mémorisation. Juste une contre-primitive et compréhensible pour tout le monde.


Toute complexité est dans une restriction anormale. Vous ne pouvez pas utiliser d'accolades, ce qui signifie que vous devez reconsidérer les pratiques quotidiennes et la syntaxe ordinaire.


Solution habituelle


Vous devez d'abord choisir un point de départ. Habituellement, si le langage ou son extension ne fournit pas la fonction de décoration nécessaire, nous implémenterons nous-mêmes un conteneur: une fonction encapsulée, des données accumulées et une interface pour y accéder. C'est souvent une classe:


class CountFunction { constructor(f) { this.calls = 0; this.f = f; } invoke() { this.calls += 1; return this.f(...arguments); } } const csum = new CountFunction((x, y) => x + y); csum.invoke(3, 7); // 10 csum.invoke(9, 6); // 15 csum.calls; // 2 

Cela ne nous convient pas tout de suite, car:


  1. En JavaScript, vous ne pouvez pas implémenter une propriété privée de cette manière: nous pouvons à la fois lire les appels de l' instance (dont nous avons besoin) et écrire la valeur de l'extérieur (dont nous n'avons PAS besoin). Bien sûr, nous pouvons utiliser la fermeture dans le constructeur , mais alors quel est le sens de la classe? Et j'aurais encore peur d'utiliser de nouveaux champs privés sans babel 7 .
  2. Le langage prend en charge un paradigme fonctionnel et la création d'une instance via new ne semble pas la meilleure solution ici. Il est plus agréable d'écrire une fonction qui renvoie une autre fonction. Oui!
  3. Enfin, la syntaxe de ClassDeclaration et MethodDefinition ne nous permettra pas de nous débarrasser de tous les accolades.

Mais nous avons un merveilleux modèle de module qui implémente la confidentialité en utilisant la fermeture:


 function count(f) { let calls = 0; return { invoke: function() { calls += 1; return f(...arguments); }, getCalls: function() { return calls; } }; } const csum = count((x, y) => x + y); csum.invoke(3, 7); // 10 csum.invoke(9, 6); // 15 csum.getCalls(); // 2 

Vous pouvez déjà travailler avec cela.


Décision divertissante


Pourquoi des accolades sont-elles utilisées ici? Ce sont 4 cas différents:


  1. Définition du corps d'une fonction de comptage ( FunctionDeclaration )
  2. Initialisation de l'objet retourné
  3. La définition du corps de la fonction d'appel ( FunctionExpression ) avec deux expressions
  4. Définition du corps d'une fonction getCalls ( FunctionExpression ) avec une seule expression

Commençons par le deuxième paragraphe. En fait, nous n'avons pas besoin de renvoyer un nouvel objet, tout en compliquant l'invocation de la fonction finale via invoke . Nous pouvons profiter du fait qu'une fonction en JavaScript est un objet, ce qui signifie qu'elle peut contenir ses propres champs et méthodes. Créons notre fonction return df et y ajoutons la méthode getCalls qui, à travers la fermeture, aura accès aux appels comme auparavant:


 function count(f) { let calls = 0; function df() { calls += 1; return f(...arguments); } df.getCalls = function() { return calls; } return df; } 

C'est plus agréable de travailler avec ça:


 const csum = count((x, y) => x + y); csum(3, 7); // 10 csum(9, 6); // 15 csum.getCalls(); // 2 

Le quatrième point est clair: nous remplaçons simplement FunctionExpression par ArrowFunction . L'absence d'accolades nous fournira un bref enregistrement de la fonction flèche dans le cas d'une seule expression dans son corps:


 function count(f) { let calls = 0; function df() { calls += 1; return f(...arguments); } df.getCalls = () => calls; return df; } 

Avec le troisième - tout est plus compliqué. N'oubliez pas que la première chose que nous avons faite a été de remplacer FunctionExpression par des fonctions d' appel avec FunctionDeclaration df . Pour réécrire ceci dans ArrowFunction, deux problèmes devront être résolus: ne pas perdre l'accès aux arguments (maintenant c'est un pseudo-tableau d' arguments ) et éviter le corps de fonction de deux expressions.


Le premier problème nous aidera à faire face à l'argument explicitement spécifié pour le paramètre de fonction avec l' opérateur d'étalement . Et pour combiner deux expressions en une, vous pouvez utiliser un ET logique . Contrairement à l'opérateur de conjonction logique classique qui renvoie un booléen, il calcule les opérandes de gauche à droite jusqu'au premier "faux" et le renvoie, et si tous sont "vrais" - alors la dernière valeur. Le tout premier incrément du compteur nous donnera 1, ce qui signifie que cette sous-expression sera toujours convertie en true. La réductibilité à la «vérité» du résultat de l'appel de fonction dans la deuxième sous-expression ne nous intéresse pas: en tout cas, la calculatrice s'arrête là. Nous pouvons maintenant utiliser la fonction ArrowFunction :


 function count(f) { let calls = 0; let df = (...args) => (calls += 1) && f(...args); df.getCalls = () => calls; return df; } 

Vous pouvez décorer un enregistrement un peu en utilisant l'incrément de préfixe:


 function count(f) { let calls = 0; let df = (...args) => ++calls && f(...args); df.getCalls = () => calls; return df; } 

La solution au premier point le plus difficile commencera par remplacer FunctionDeclaration par ArrowFunction . Mais nous avons toujours le corps entre accolades:


 const count = f => { let calls = 0; let df = (...args) => ++calls && f(...args); df.getCalls = () => calls; return df; }; 

Si nous voulons nous débarrasser des accolades encadrant le corps de la fonction, nous devrons éviter de déclarer et d'initialiser des variables via let . Et nous avons deux variables entières: les appels et df .


Commençons par le compteur. Nous pouvons créer une variable locale en la définissant dans la liste des paramètres de fonction, et transférer la valeur initiale en l'appelant à l'aide de IIFE (Immediately Invoked Function Expression):


 const count = f => (calls => { let df = (...args) => ++calls && f(...args); df.getCalls = () => calls; return df; })(0); 

Reste à concaténer les trois expressions en une seule. Puisque nous avons les trois expressions représentant des fonctions qui sont toujours réductibles à true, nous pouvons également utiliser un ET logique :


 const count = f => (calls => (df = (...args) => ++calls && f(...args)) && (df.getCalls = () => calls) && df)(0); 

Mais il existe une autre option pour concaténer des expressions: utiliser l' opérateur virgule . Il est préférable, car il ne traite pas des transformations logiques inutiles et nécessite moins de crochets. Les opérandes sont évalués de gauche à droite, et le résultat est la valeur de ce dernier:


 const count = f => (calls => (df = (...args) => ++calls && f(...args), df.getCalls = () => calls, df))(0); 

Je suppose que j'ai réussi à te tromper? Nous nous sommes audacieusement débarrassés de la déclaration de la variable df et n'avons laissé que l'affectation de notre fonction flèche. Dans ce cas, cette variable sera déclarée globalement, ce qui est inacceptable! Pour df , nous répétons l'initialisation de la variable locale dans les paramètres de notre fonction IIFE, mais nous ne passerons aucune valeur initiale:


 const count = f => ((calls, df) => (df = (...args) => ++calls && f(...args), df.getCalls = () => calls, df))(0); 

Ainsi, l'objectif est atteint.


Variations sur un thème


Fait intéressant, nous avons pu éviter de créer et d'initialiser des variables locales, plusieurs expressions dans des blocs fonctionnels et la création d'un littéral d'objet. Dans le même temps, la solution d'origine a été maintenue propre: absence de variables globales, confidentialité du compteur, accès aux arguments de la fonction en cours de wrapping.


En général, vous pouvez prendre n'importe quelle implémentation et essayer de faire quelque chose de similaire. Par exemple, le polyfill pour la fonction de liaison à cet égard est assez simple:


 const bind = (f, ctx, ...a) => (...args) => f.apply(ctx, a.concat(args)); 

Cependant, si l'argument f n'est pas une fonction, nous devrions lever une exception dans le bon sens. Et l'exception throw ne peut pas être levée dans le contexte de l'expression. Vous pouvez attendre les expressions throw (étape 2) et réessayer. Ou est-ce que quelqu'un a déjà des pensées maintenant?


Ou considérez une classe qui décrit les coordonnées d'un point:


 class Point { constructor(x, y) { this.x = x; this.y = y; } toString() { return `(${this.x}, ${this.y})`; } } 

Qui peut être représenté par une fonction:


 const point = (x, y) => (p => (px = x, py = y, p.toString = () => ['(', x, ', ', y, ')'].join(''), p))(new Object); 

Seulement ici, nous avons perdu l'héritage du prototype: toString est une propriété de l'objet prototype Point , pas un objet créé séparément. Cela peut-il être évité si vous essayez dur?


Dans les résultats des transformations, nous obtenons un mélange malsain de programmation fonctionnelle avec des hacks impératifs et certaines fonctionnalités du langage lui-même. Si vous y réfléchissez, cela peut s'avérer être un obscurcisseur intéressant (mais pas pratique) du code source. Vous pouvez créer votre propre version de la tâche de "masquage d'obturateur" et divertir vos collègues et amis de JavaScript'ers pendant leur temps libre d'un travail utile.


Conclusion


La question est de savoir à qui est-elle utile et pourquoi est-elle nécessaire? Ceci est complètement nocif pour les débutants, car il forme une fausse idée de la complexité et de la déviance excessives de la langue. Mais cela peut être utile pour les praticiens, car cela vous permet de regarder les caractéristiques de la langue de l'autre côté: l'appel n'est pas à éviter, et l'appel est à essayer d'éviter à l'avenir.

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


All Articles