paint-brush
Comment étudier le code source : prototypes d'objets et mixins dans Express.jspar@luminix
187 lectures

Comment étudier le code source : prototypes d'objets et mixins dans Express.js

par Lumin-ix11m2024/08/19
Read on Terminal Reader

Trop long; Pour lire

L'étude du code source peut sans aucun doute changer la trajectoire de votre carrière de développeur. Même en regardant sous la surface, ne serait-ce qu'un niveau, vous pouvez vous démarquer de la plupart des développeurs moyens. C'est le but de cette série : ne pas se contenter de l'API, mais aller au-delà, apprendre à recréer ces outils. Sortir de la moyenne dans ce monde de battage médiatique autour de l'IA, c'est ce qui fait qu'un développeur est précieux au-delà de la moyenne !
featured image - Comment étudier le code source : prototypes d'objets et mixins dans Express.js
Lumin-ix HackerNoon profile picture
0-item
1-item

L'étude du code source peut sans aucun doute changer la trajectoire de votre carrière de développeur. Même en regardant sous la surface, ne serait-ce qu'un niveau, vous pouvez vous démarquer de la plupart des développeurs moyens.


C'est la première étape vers la maîtrise !


Voici une histoire personnelle : dans mon travail actuel dans une startup d'IA/ML, l'équipe n'arrivait pas à comprendre comment transférer les données Neo4j du serveur vers le frontend pour une visualisation, et ils avaient une présentation en 12 heures. J'ai été embauché en tant que freelance, et on pouvait clairement voir la panique. Le problème était que les données renvoyées par Neo4j n'étaient pas au format correct attendu par l'outil de visualisation, neo4jd3 .


Imaginez ceci : Neo4jd3 attend un triangle et Neo4j renvoie un carré. C'est une incompatibilité !


néo4jd3


Nous pourrions bientôt faire de la science des données graphiques avec JavaScript et Neo4j dans Mastered ! Cette image est nostalgique.


Il n'y avait que deux choix : refaire tout le backend Neo4j ou étudier le code source de Neo4jd3, déterminer le format attendu, puis créer un adaptateur pour transformer le carré en triangle.


 neo4jd3 <- adapter <- Neo4j 

adaptateur à savoir



Mon cerveau a choisi de lire le code source par défaut et j'ai créé un adaptateur : neo4jd3-ts .


 import createNeoChart, { NeoDatatoChartData } from "neo4jd3-ts";


L'adaptateur est NeoDatatoChartData , et tout le reste appartient à l'histoire. J'ai pris cette leçon à cœur et, à chaque fois que j'en ai l'occasion, je descends d'un niveau dans chaque outil que j'utilise. C'est devenu si courant que parfois je ne lis même pas la documentation.


Cette approche a changé ma carrière de manière spectaculaire. Tout ce que je fais ressemble à de la magie. En quelques mois, j'ai dirigé des migrations et des projets de serveurs critiques, tout cela parce que j'ai fait un pas vers la source.


C'est de cela dont il s'agit dans cette série : ne pas se contenter de l'API, mais aller au-delà, apprendre à recréer ces outils. Sortir de la moyenne dans ce monde de battage médiatique autour de l'IA, c'est ce qui fait qu'un développeur est d'une valeur inestimable !


Mon objectif avec cette série est d’étudier les bibliothèques et outils JavaScript populaires, de découvrir ensemble comment ils fonctionnent et quels modèles nous pouvons en tirer, un outil à la fois.


Étant donné que je suis principalement un ingénieur backend (full stack, oui, mais gérant le backend 90 % du temps), il n'y a pas de meilleur outil pour commencer qu'Express.js.


Je suppose que vous avez de l'expérience en programmation et une bonne maîtrise des fondamentaux de la programmation ! Vous pouvez être classé comme débutant avancé.


Il sera très difficile et fastidieux d'essayer d'apprendre/d'enseigner le code source tout en enseignant les fondamentaux. Vous pouvez rejoindre la série, mais attendez-vous à ce que ce soit difficile. Je ne peux pas tout couvrir, mais je ferai de mon mieux.


Cet article est antérieur à Express pour une raison : j'ai décidé de couvrir une très petite bibliothèque dont dépend Express, merge-descriptors , qui, au moment où j'écris ces lignes, compte 27 181 495 téléchargements et seulement 26 lignes de code.


Cela nous donnera l'occasion d'établir une structure et me permettra d'introduire les fondamentaux des objets qui sont essentiels dans la construction de modules JavaScript.

Installation

Avant de continuer, assurez-vous que vous disposez du code source d'Express et des descripteurs de fusion dans votre système. De cette façon, vous pouvez l'ouvrir dans un IDE et je peux vous guider avec les numéros de ligne sur lesquels nous recherchons.


Express est une bibliothèque complète. Nous allons couvrir tout ce que nous pouvons dans quelques articles avant de passer à un autre outil.


Ouvrez votre source Express dans votre IDE, de préférence avec des numéros de ligne, accédez au dossier lib et ouvrez le fichier express.js , le fichier d'entrée.


Sur la ligne 17, voici notre première bibliothèque :


 var mixin = require('merge-descriptors');


L'utilisation se trouve aux lignes 42 et 43 :


 mixin(app, EventEmitter.prototype, false); mixin(app, proto, false);


Avant même d'explorer ce qui se passe ici, nous devons prendre du recul et parler des objets en JavaScript, au-delà de la structure des données. Nous parlerons de la composition, de l'héritage, des prototypes et des mixins, le titre de cet article.

L'objet JavaScript

Fermez le code source d'Express et créez un nouveau dossier quelque part pour suivre notre apprentissage de ces principes fondamentaux d'objet cruciaux.


Un objet est une encapsulation de données et de comportements, au cœur de la programmation orientée objet (POO) . Fait amusant : presque tout dans JavaScript est un objet.


 const person = { // data name: "Jane", age: 0, // behavior grow(){ this.age += 1; } };

Tout ce qui se trouve entre les accolades ouvrantes et fermantes dans l'objet person est appelé propriétés propres de l'objet . C'est important.


Les propriétés propres sont celles directement sur l'objet. name , age et grow sont des propriétés propres à person .


Ceci est important car chaque objet JavaScript possède une propriété prototype . Encodons l'objet ci-dessus dans un plan de fonction pour nous permettre de créer dynamiquement des objets person .


 function createNewPerson(name, age){ this.name = name; this.age = age; } createNewPerson.prototype.print = function(){ console.log(`${this.name} is ${this.age}`); }; const john = new createNewPerson("John", 32);

Le prototype est la manière dont les objets JavaScript héritent des propriétés et des méthodes d'autres objets. La différence entre Own Properties et Prototype se situe lors de l'accès à une propriété d'un objet :


 john.name; // access


JavaScript recherchera d'abord Own Properties , car elles ont une priorité élevée. S'il ne trouve pas la propriété, il recherche de manière récursive dans l'objet prototype de l'objet jusqu'à ce qu'il trouve null et génère une erreur.


Un objet prototype peut hériter d'un autre objet via son propre prototype. C'est ce qu'on appelle une chaîne de prototypes.


 console.log(john.hasOwnProperty('name')); // true console.log(john.hasOwnProperty('print')); // false, it's in the prototype


Cependant, print fonctionne sur john :


 john.print(); // "John is 32"


C'est pourquoi JavaScript est défini comme un langage basé sur des prototypes. Nous pouvons faire bien plus avec les prototypes que simplement ajouter des propriétés et des méthodes, comme l'héritage.


Le « bonjour le monde » de l'héritage est l'objet mammifère. Recréons-le avec JavaScript.


 // our Mammal blueprint function Mammal(name) { this.name = name; } Mammal.prototype.breathe = function() { console.log(`${this.name} is breathing.`); };


En JavaScript, il existe une fonction statique à l'intérieur de l'objet Object :


 Object.create();


Il crée un objet de manière similaire à {} et new functionBlueprint , mais la différence est que create peut prendre un prototype comme paramètre pour hériter.


 // we use a cat blueprint function here (implemented below) Cat.prototype = Object.create(Mammal.prototype); // correction after we inherited all the properties Cat.prototype.constructor = Cat;


Maintenant, Cat aura la méthode breathe trouvée dans Mammal , mais la chose importante à savoir est que Cat pointe vers Mammal comme son prototype.

Explication:

  1. Mammal Blueprint : Nous définissons d’abord la fonction Mammal et ajoutons une méthode breathe à son prototype.


  2. Héritage de Cat : nous créons la fonction Cat et définissons Cat.prototype sur Object.create(Mammal.prototype) . Cela fait hériter le prototype Cat de Mammal , mais cela change le pointeur constructor en Mammal .


  3. Correction du constructeur : nous corrigeons le Cat.prototype.constructor pour qu'il pointe vers Cat , garantissant ainsi que l'objet Cat conserve son identité tout en héritant des méthodes de Mammal . Enfin, nous ajoutons une méthode meow à Cat .


Cette approche permet à l'objet Cat d'accéder aux méthodes de Mammal (comme breathe ) et de son propre prototype (comme meow ).


Nous devons corriger cela. Créons l'exemple complet :


 function Cat(name, breed) { this.name = name; this.breed = breed; } Cat.prototype = Object.create(Mammal.prototype); // cat prototype pointing to mammal // correction after we inherited all the properties Cat.prototype.constructor = Cat; // we are re-pointing a pointer, the inherited properties are still there Cat.prototype.meow = function() { console.log(`${this.name} is meowing.`); };


Pour comprendre le Cat.prototype.constructor = Cat , vous devez connaître les pointeurs. Lorsque nous héritons de Mammal avec Object.create , il change le pointeur de notre prototype Cat en Mammal , ce qui est faux. Nous voulons toujours que notre Cat soit son propre individu, malgré le fait qu'il ait le parent Mammal .


C'est pourquoi nous devons corriger cela.


Dans cet exemple, Cat hérite de Mammal en utilisant la chaîne de prototypes. L'objet Cat peut accéder aux méthodes breathe et meow .


 const myCat = new Cat("Misty", "Ragdoll"); myCat.breathe(); // Misty is breathing. myCat.meow(); // Misty is meowing.


Nous pouvons créer un chien héritant également du mammifère :


 function Dog(name, breed) { this.name = name; this.breed = breed; } Dog.prototype = Object.create(Mammal.prototype); Dog.prototype.constructor = Dog; Dog.prototype.bark = function() { console.log(`${this.name} is barking.`); }; const myDog = new Dog('Buddy', 'Golden Retriever'); myDog.breathe(); // Buddy is breathing. myDog.bark(); // Buddy is barking.


Nous avons créé l'héritage classique de base, mais pourquoi est-ce important ? Je pensais que nous parlions du code source !


Oui, c'est vrai, mais les prototypes sont au cœur de la construction de modules efficaces et flexibles, au-delà de l'héritage. Même les modules simples et bien écrits sont remplis d'objets prototypes. Nous ne faisons que poser les bases.


L'alternative à l'héritage est la composition d'objets, qui prend deux ou plusieurs objets et les fusionne pour former un « super » objet.


Les mixins permettent aux objets d'emprunter des méthodes à d'autres objets sans utiliser l'héritage. Ils sont pratiques pour partager le comportement entre des objets non liés.


C'est ce que fait notre première exploration : la bibliothèque merge-descriptors que nous avons promis de couvrir en premier.

Module de fusion de descripteurs

Nous avons déjà vu où et comment il est utilisé dans Express. Nous le connaissons maintenant pour la composition d'objets.


A la ligne 17, voici notre première bibliothèque :


 var mixin = require('merge-descriptors');


L'utilisation se trouve aux lignes 42 et 43 :


 mixin(app, EventEmitter.prototype, false); mixin(app, proto, false);


Avec ce que nous savons, nous pouvons déjà déduire que mixin compose EventEmitter.prototype et proto dans un objet appelé app .


Nous aborderons app lorsque nous commencerons à parler d’Express.


Voici l'intégralité du code source des merge-descriptors :


 'use strict'; function mergeDescriptors(destination, source, overwrite = true) { if (!destination) { throw new TypeError('The `destination` argument is required.'); } if (!source) { throw new TypeError('The `source` argument is required.'); } for (const name of Object.getOwnPropertyNames(source)) { if (!overwrite && Object.hasOwn(destination, name)) { // Skip descriptor continue; } // Copy descriptor const descriptor = Object.getOwnPropertyDescriptor(source, name); Object.defineProperty(destination, name, descriptor); } return destination; } module.exports = mergeDescriptors;


Dès le début, regardez toujours comment la fonction est utilisée et les paramètres qu'elle prend :


 // definition mergeDescriptors(destination, source, overwrite = true) // usage var mixin = require('merge-descriptors'); mixin(app, EventEmitter.prototype, false); mixin(app, proto, false);


App est notre destination. Nous savons que mixin signifie composition d'objets. En gros, ce que fait ce paquet, c'est composer l'objet source dans l'objet de destination, avec une option d'écrasement.


Le remplacement, par hypothèse, se produit si app (destination) possède une propriété exacte que la source possède, en cas true remplacement, sinon laissez cette propriété intacte et ignorez.


Nous savons que les objets ne peuvent pas avoir deux fois la même propriété. Dans les paires clé-valeur (objets), les clés doivent être uniques.

Avec Express, l'écrasement est false .


Voici les règles de base à respecter : gérez toujours les erreurs attendues :


 if (!destination) { throw new TypeError('The `destination` argument is required.'); } if (!source) { throw new TypeError('The `source` argument is required.'); }


C'est là que ça devient intéressant : la ligne 12.


 for (const name of Object.getOwnPropertyNames(source)) {


D'en haut, nous savons ce que signifie OwnProperty , donc getOwnPropertyNames signifie clairement obtenir les clés de ses propres propriétés.


 const person = { // data name: "Jane", age: 0, // behavior grow() { this.age += 1; } }; Object.getOwnPropertyNames(person); // [ 'name', 'age', 'grow' ]


Il renvoie les clés sous forme de tableau, et nous bouclons sur ces clés dans l'instance suivante :


 for (const name of Object.getOwnPropertyNames(source)) {


Ce qui suit vérifie si la destination et la source ont la même clé sur laquelle nous bouclons actuellement :


 if (!overwrite && Object.hasOwn(destination, name)) { // Skip descriptor continue; }


Si overwrite est faux, ignorez cette propriété ; n'écrasez pas. C'est ce que fait continue : il propulse la boucle vers l'itération suivante et n'exécute pas le code en dessous, qui est le code suivant :


 // Copy descriptor const descriptor = Object.getOwnPropertyDescriptor(source, name); Object.defineProperty(destination, name, descriptor);


Nous savons déjà ce que signifie getOwnProperty . Le nouveau mot est descriptor . Testons cette fonction sur notre propre objet person :


 const person = { // data name: "Jane", age: 0, // behavior grow() { this.age += 1; } }; Object.getOwnPropertyDescriptor(person, "grow"); // { // value: [Function: grow], // writable: true, // enumerable: true, // configurable: true // }


Elle renvoie notre fonction grow comme valeur, et la ligne suivante est explicite :


 Object.defineProperty(destination, name, descriptor);


Il s'agit de prendre notre descripteur de la source et de l'écrire dans la destination. Il copie les propriétés propres de la source vers notre objet de destination en tant que ses propres propriétés.


Faisons un exemple dans notre objet person :


 const val = { value: function isAlien() { return false; }, enumerable: true, writable: true, configurable: true, }; Object.defineProperty(person, "isAlien", val);


person doit maintenant avoir la propriété isAlien définie.


En résumé, ce module hautement téléchargé copie ses propres propriétés d'un objet source vers une destination avec une option d'écrasement.


Nous avons couvert avec succès notre premier module dans ce niveau de code source , avec des choses encore plus intéressantes à venir.


Il s'agissait d'une introduction. Nous avons commencé par couvrir les fondamentaux nécessaires à la compréhension du module et, en conséquence, à la compréhension des modèles de la plupart des modules, à savoir la composition et l'héritage d'objets. Enfin, nous avons parcouru le module merge-descriptors .


Ce modèle sera courant dans la plupart des articles. Si je pense qu'il y a des fondamentaux nécessaires à couvrir, nous les aborderons dans la première section, puis nous aborderons le code source.


Heureusement, merge-descriptors sont utilisés dans Express, qui est notre objectif de départ pour l'étude du code source. Attendez-vous donc à plus d'articles sur le code source d'Express.js jusqu'à ce que nous estimions avoir suffisamment bien utilisé Express, puis passez à un autre module ou outil comme Node.js.


En attendant, vous pouvez vous lancer dans un défi en naviguant jusqu'au fichier de test dans les descripteurs de fusion et en lisant l'intégralité du fichier. Il est important de lire la source par vous-même, d'essayer de comprendre ce qu'elle fait et ce qu'elle teste, puis de la casser, oui, et de la réparer à nouveau ou d'ajouter d'autres tests !


Si vous êtes intéressé par un contenu plus exclusif, pratique et plus long pour améliorer vos compétences en programmation, vous pouvez en trouver plus sur Ko-fi .