Estudiar el código fuente puede cambiar sin duda la trayectoria de tu carrera como desarrollador. Incluso mirar más allá de la superficie, aunque sea un nivel, puede diferenciarte de la mayoría de los desarrolladores promedio.
¡Es el primer paso hacia la maestría!
Aquí hay una historia personal: en mi trabajo actual en una startup de IA/ML, el equipo no podía descubrir cómo obtener datos de Neo4j desde el servidor hasta la interfaz para una visualización, y tuvieron que hacer una presentación en 12 horas. Me contrataron como freelance y se podía ver claramente el pánico. El problema era que los datos devueltos por Neo4j no estaban en el formato correcto esperado por la herramienta de visualización, neo4jd3 .
Imagínese lo siguiente: Neo4jd3 espera un triángulo y Neo4j devuelve un cuadrado. ¡Ahí está la incompatibilidad!
¡Pronto podremos hacer ciencia de datos gráficos con JavaScript y Neo4j en Mastered ! Esta imagen es nostálgica.
Sólo había dos opciones: rehacer todo el backend de Neo4j o estudiar el código fuente de Neo4jd3, averiguar el formato esperado y luego crear un adaptador para transformar el cuadrado en un triángulo.
neo4jd3 <- adapter <- Neo4j
Mi cerebro decidió leer el código fuente y creé un adaptador: neo4jd3-ts .
import createNeoChart, { NeoDatatoChartData } from "neo4jd3-ts";
El adaptador es NeoDatatoChartData
y todo lo demás es historia. Tomé esta lección en serio y, cada vez que tengo la oportunidad, bajo un nivel más bajo en cada herramienta que uso. Se ha vuelto tan común que a veces ni siquiera leo la documentación.
Este enfoque cambió mi carrera enormemente. Todo lo que hago parece magia. En pocos meses, estaba liderando migraciones y proyectos críticos de servidores, todo porque di un paso hacia la fuente.
De eso se trata esta serie: no conformarse con la API, sino ir más allá y aprender a recrear estas herramientas. ¡Dejar de ser promedio en este mundo de exageraciones sobre la IA es lo que hace que un desarrollador sea más valioso que el promedio!
Mi plan con esta serie es estudiar las bibliotecas y herramientas de JavaScript más populares, descubriendo juntos cómo funcionan y qué patrones podemos aprender de ellas, una herramienta a la vez.
Dado que soy principalmente un ingeniero backend (full stack, sí, pero manejo el backend el 90% del tiempo), no hay mejor herramienta para comenzar que Express.js.
Supongo que tienes experiencia en programación y un buen conocimiento de los fundamentos de la programación. Es posible que se te clasifique como principiante avanzado.
Será muy difícil y tedioso intentar aprender/enseñar el código fuente mientras se enseñan los fundamentos. Puedes unirte a la serie, pero prepárate para que sea difícil. No puedo cubrir todo, pero intentaré hacer todo lo que pueda.
Este artículo es anterior a Express por una razón: decidí cubrir una biblioteca muy pequeña de la que depende Express, merge-descriptors , que, mientras escribo esto, tiene 27.181.495 descargas y apenas 26 líneas de código.
Esto nos dará la oportunidad de establecer una estructura y me permitirá presentar los fundamentos de los objetos que son cruciales en la creación de módulos de JavaScript.
Antes de continuar, asegúrese de tener el código fuente de Express y los descriptores de combinación en su sistema. De esta manera, puede abrirlo en un IDE y yo puedo guiarlo con números de línea sobre dónde estamos buscando.
Express es una biblioteca muy completa. Trataremos todo lo que podamos en unos pocos artículos antes de pasar a otra herramienta.
Abra el código fuente de Express en su IDE, preferiblemente con números de línea, navegue a la carpeta lib
y abra el archivo express.js
, el archivo de entrada.
En la línea 17, aquí está nuestra primera biblioteca:
var mixin = require('merge-descriptors');
El uso está en las líneas 42 y 43:
mixin(app, EventEmitter.prototype, false); mixin(app, proto, false);
Antes de explorar lo que sucede aquí, debemos dar un paso atrás y hablar sobre los objetos en JavaScript, más allá de la estructura de datos. Hablaremos de composición, herencia, prototipos y mixins, el título de este artículo.
Cierre el código fuente de Express y cree una nueva carpeta en algún lugar para seguir mientras aprendemos estos conceptos fundamentales de los objetos.
Un objeto es una encapsulación de datos y comportamiento, que es el núcleo de la programación orientada a objetos (POO) . Dato curioso: casi todo en JavaScript es un objeto.
const person = { // data name: "Jane", age: 0, // behavior grow(){ this.age += 1; } };
Todo lo que se encuentra entre las llaves de apertura y cierre en el objeto person
se denomina propiedades propias del objeto . Esto es importante.
Las propiedades propias son aquellas que están directamente relacionadas con el objeto. name
, age
y grow
son propiedades propias de person
.
Esto es importante porque cada objeto de JavaScript tiene una propiedad prototype
. Codifiquemos el objeto anterior en un modelo de función que nos permita crear objetos person
de forma dinámica.
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);
El prototipo es la forma en que los objetos de JavaScript heredan propiedades y métodos de otros objetos. La diferencia entre Own Properties
y Prototype
es cuando se accede a una propiedad de un objeto:
john.name; // access
JavaScript buscará primero en Own Properties
, ya que tienen una alta prioridad. Si no encuentra la propiedad, busca en el prototype
del objeto de forma recursiva hasta que encuentra un valor nulo y genera un error.
Un objeto prototipo puede heredar de otro objeto a través de su propio prototipo. Esto se denomina cadena de prototipos.
console.log(john.hasOwnProperty('name')); // true console.log(john.hasOwnProperty('print')); // false, it's in the prototype
Sin embargo, print
sobre john
:
john.print(); // "John is 32"
Por eso, JavaScript se define como un lenguaje basado en prototipos. Con los prototipos podemos hacer más que simplemente añadir propiedades y métodos, como la herencia.
El "hola mundo" de la herencia es el objeto mamífero. Vamos a recrearlo con JavaScript.
// our Mammal blueprint function Mammal(name) { this.name = name; } Mammal.prototype.breathe = function() { console.log(`${this.name} is breathing.`); };
En JavaScript, hay una función estática dentro del objeto Object
:
Object.create();
Crea un objeto de manera similar a {}
y a new functionBlueprint
, pero la diferencia es que create
puede tomar un prototipo como parámetro para heredar.
// 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;
Ahora Cat
tendrá el método breathe
que se encuentra en Mammal
, pero lo importante es saber que Cat
está señalando a Mammal
como su prototipo.
Plano de mamífero : primero definimos la función Mammal
y agregamos un método breathe
a su prototipo.
Herencia de Cat : creamos la función Cat
y establecemos Cat.prototype
en Object.create(Mammal.prototype)
. Esto hace que el prototipo Cat
herede de Mammal
, pero cambia el puntero constructor
a Mammal
.
Corrección del constructor : corregimos el Cat.prototype.constructor
para que apunte a Cat
, lo que garantiza que el objeto Cat
conserve su identidad al tiempo que hereda los métodos de Mammal
. Por último, agregamos un método meow
a Cat
.
Este enfoque permite que el objeto Cat
acceda a métodos tanto de Mammal
(como breathe
) como de su propio prototipo (como meow
).
Necesitamos corregirlo. Vamos a crear el ejemplo completo:
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.`); };
Para entender Cat.prototype.constructor = Cat
, es necesario conocer los punteros. Cuando heredamos de Mammal
con Object.create
, cambia el puntero de nuestro prototipo Cat
a Mammal
, lo cual es incorrecto. Aún queremos que nuestro Cat
sea su propio individuo, a pesar de tener el padre Mammal
.
Por eso tenemos que corregirlo.
En este ejemplo, Cat
hereda de Mammal
mediante la cadena de prototipos. El objeto Cat
puede acceder a los métodos breathe
y meow
.
const myCat = new Cat("Misty", "Ragdoll"); myCat.breathe(); // Misty is breathing. myCat.meow(); // Misty is meowing.
Podemos crear un perro también heredando del mamífero:
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.
Hemos creado una herencia clásica básica, pero ¿por qué es importante? ¡Creía que estábamos cubriendo el código fuente!
Sí, es cierto, pero los prototipos son el núcleo de la creación de módulos eficientes y flexibles, más allá de la herencia. Incluso los módulos simples y bien escritos están plagados de objetos prototipo. Solo estamos sentando las bases.
La alternativa a la herencia es la composición de objetos, que toma dos o más objetos y los fusiona para formar un "súper" objeto.
Los mixins permiten que los objetos tomen prestados métodos de otros objetos sin usar la herencia. Son útiles para compartir comportamientos entre objetos no relacionados.
Esto es lo que hace nuestra primera exploración: la biblioteca merge-descriptors
que prometimos cubrir primero.
Ya hemos visto dónde y cómo se utiliza en Express. Ahora sabemos cómo se utiliza en la composición de objetos.
En la línea 17, aquí está nuestra primera biblioteca:
var mixin = require('merge-descriptors');
El uso está en las líneas 42 y 43:
mixin(app, EventEmitter.prototype, false); mixin(app, proto, false);
Con lo que sabemos, ya podemos deducir que mixin
está componiendo EventEmitter.prototype
y proto
en un objeto llamado app
.
Llegaremos a app
cuando empecemos a hablar de Express.
Este es el código fuente completo para 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;
Desde el principio, fíjate siempre en cómo se utiliza la función y los parámetros que toma:
// definition mergeDescriptors(destination, source, overwrite = true) // usage var mixin = require('merge-descriptors'); mixin(app, EventEmitter.prototype, false); mixin(app, proto, false);
App
es nuestro destino. Sabemos que mixin
significa composición de objetos. Básicamente, lo que hace este paquete es componer el objeto de origen en el objeto de destino, con una opción para sobrescribirlo.
Sobrescribir, asumiendo, es si app
(destino) tiene una propiedad exacta que la fuente tiene, en caso true
sobrescribir, de lo contrario dejar esa propiedad intacta y omitir.
Sabemos que los objetos no pueden tener la misma propiedad dos veces. En los pares clave-valor (objetos), las claves deben ser únicas.
Con Express, la sobrescritura es false
.
Lo siguiente es una gestión básica, siempre maneje los errores esperados:
if (!destination) { throw new TypeError('The `destination` argument is required.'); } if (!source) { throw new TypeError('The `source` argument is required.'); }
Aquí es donde se pone interesante: línea 12.
for (const name of Object.getOwnPropertyNames(source)) {
Desde arriba, sabemos lo que significa OwnProperty
, por lo que getOwnPropertyNames
claramente significa obtener las claves de las propiedades propias.
const person = { // data name: "Jane", age: 0, // behavior grow() { this.age += 1; } }; Object.getOwnPropertyNames(person); // [ 'name', 'age', 'grow' ]
Devuelve las claves como una matriz y estamos recorriendo esas claves en la siguiente instancia:
for (const name of Object.getOwnPropertyNames(source)) {
Lo siguiente verifica si el destino y la fuente tienen la misma clave que estamos recorriendo actualmente:
if (!overwrite && Object.hasOwn(destination, name)) { // Skip descriptor continue; }
Si overwrite es falso, omite esa propiedad; no sobrescribas. Eso es lo que hace continue
: impulsa el bucle a la siguiente iteración y no ejecuta el código subyacente, que es el siguiente:
// Copy descriptor const descriptor = Object.getOwnPropertyDescriptor(source, name); Object.defineProperty(destination, name, descriptor);
Ya sabemos qué significa getOwnProperty
. La nueva palabra es descriptor
. Probemos esta función en nuestro propio objeto 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 // }
Devuelve nuestra función grow
como valor y la siguiente línea se explica por sí sola:
Object.defineProperty(destination, name, descriptor);
Toma nuestro descriptor de la fuente y lo escribe en el destino. Copia las propiedades propias de la fuente en nuestro objeto de destino como si fueran sus propias propiedades.
Hagamos un ejemplo en nuestro objeto person
:
const val = { value: function isAlien() { return false; }, enumerable: true, writable: true, configurable: true, }; Object.defineProperty(person, "isAlien", val);
Ahora person
debería tener la propiedad isAlien
definida.
En resumen, este módulo altamente descargado copia sus propias propiedades de un objeto de origen a un destino con una opción para sobrescribir.
Hemos cubierto con éxito nuestro primer módulo en este nivel de código fuente y habrá más cosas interesantes por venir.
Esta fue una introducción. Comenzamos cubriendo los conceptos básicos necesarios para comprender el módulo y, como resultado, comprender los patrones en la mayoría de los módulos, que son la composición y la herencia de objetos. Por último, exploramos el módulo merge-descriptors
.
Este patrón será el predominante en la mayoría de los artículos. Si considero que hay aspectos fundamentales que es necesario cubrir, los repasaremos en la primera sección y luego cubriremos el código fuente.
Afortunadamente, en Express se utilizan merge-descriptors
, que es nuestro objetivo como estudio inicial del código fuente. Por lo tanto, esperamos más artículos sobre el código fuente de Express.js hasta que consideremos que hemos tenido una experiencia lo suficientemente buena con Express, luego cambiemos a otro módulo o herramienta como Node.js.
Mientras tanto, lo que puedes hacer como desafío es navegar hasta el archivo de prueba en los descriptores de combinación y leer el archivo completo. Leer el código fuente por tu cuenta es importante. Intenta averiguar qué hace y qué prueba, y luego rómpelo. Sí, y arréglalo de nuevo o agrega más pruebas.
Si estás interesado en contenido más exclusivo, práctico y más extenso para mejorar tus habilidades de programación, puedes encontrar más en Ko-fi .