paint-brush
How To Study Source Code: Object Prototypes and Mixins in Express.jsby@luminix
187 reads

How To Study Source Code: Object Prototypes and Mixins in Express.js

by Lumin-ixAugust 19th, 2024
Read on Terminal Reader
Read this story w/o Javascript

Too Long; Didn't Read

Studying source code can undoubtedly change the trajectory of your dev career. Even looking beneath the surface just one level can set you apart from most average developers. This is what this series is about: not settling for the API, but going beyond, learning to recreate these tools. Breaking out of being average in this world of AI hype is what makes a developer valuable beyond average!
featured image - How To Study Source Code: Object Prototypes and Mixins in Express.js
Lumin-ix HackerNoon profile picture

Studying source code can undoubtedly change the trajectory of your dev career. Even looking beneath the surface just one level can set you apart from most average developers.


It's the first step to mastery!


Here’s a personal story: In my current gig at an AI/ML startup, the team couldn't figure out how to get Neo4j data from the server to the frontend for a visualization, and they had a presentation in 12 hours. I was brought in as a freelancer, and you could clearly see the panic. The problem was that the data returned by Neo4j was not in the correct format expected by the visualization tool, neo4jd3.


Imagine this: Neo4jd3 expects a triangle, and Neo4j returns a square. That’s an incompatible mismatch right there!


neo4jd3


We might do graph data science with JavaScript and Neo4j in Mastered soon! This image is nostalgic.


There were only two choices: redo the entire Neo4j backend or study Neo4jd3's source code, figure out the expected format, and then create an adapter to transform the square into a triangle.


neo4jd3 <- adapter <- Neo4j

adapter viz



My brain defaulted to reading the source code, and I created an adapter: neo4jd3-ts.


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


The adapter is NeoDatatoChartData, and everything else is history. I took this lesson to heart, and every chance I get, I go a level lower in every tool I use. It has become so prevalent that sometimes I don't even read the documentation.


This approach changed my career tremendously. Everything I do looks like magic. In a few months, I was leading critical server migrations and projects, all because I took a step towards the source.


This is what this series is about: not settling for the API, but going beyond, learning to recreate these tools. Breaking out of being average in this world of AI hype is what makes a developer valuable beyond average!


My plan with this series is to study popular JavaScript libraries and tools, figuring out together how they work and what patterns we can learn from them, one tool at a time.


Since I am mostly a backend engineer (full stack, yes, but handling the backend 90% of the time), there's no better tool to start with than Express.js.


My assumption is that you have programming experience and a good grasp of the fundamentals of programming! You might be classified as an advanced beginner.


It'll be really hard and tedious to try and learn/teach source code while teaching fundamentals. You can join the series, but expect it to be hard. I cannot cover everything, but I will try as much as I can.


This article is pre-Express for a reason: I decided to cover a very small library Express depends on, merge-descriptors, which, as I write this, has 27,181,495 downloads and a mere 26 lines of code.


This will give us an opportunity to establish a structure and allow me to introduce object fundamentals which are crucial in building JavaScript modules.

Setup

Before we proceed, make sure you have the Express source code and merge-descriptors in your system. This way, you can open it in an IDE and I can guide you with line numbers on where we are looking.


Express is a beefy library. We'll cover as much as we can over a few articles before we move on to another tool.


Open your Express source in your IDE, preferably with line numbers, navigate to the lib folder, and open the express.js file, the entry file.


On line 17, here is our first library:


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


Usage is in lines 42 and 43:


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


Before we even explore what is happening here, we need to take a step back and talk about objects in JavaScript, beyond the data structure. We'll discuss composition, inheritance, prototypes, and mixins—the title of this article.

The JavaScript Object

Close the Express source code, and create a new folder somewhere to follow along as we learn these crucial object fundamentals.


An object is an encapsulation of data and behavior, at the core of Object-Oriented Programming (OOP). Fun fact: almost everything in JavaScript is an object.


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

Everything between the opening and closing braces in the person object is called object own properties. This is important.


Own properties are those directly on the object. name, age, and grow are person's own properties.


This is important because every JavaScript object has a prototype property. Let's encode the above object into a function blueprint to allow us to dynamically create person objects.


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);

The prototype is how JavaScript objects inherit properties and methods from other objects. The difference between Own Properties and Prototype is when accessing a property on an object:


john.name; // access


JavaScript will first look in Own Properties, as they take high precedence. If it does not find the property, it looks in the object's own prototype object recursively until it finds null and throws an error.


A prototype object can inherit from another object via its own prototype. This is called a prototype chain.


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


However, print works on john:


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


This is why JavaScript is defined as a prototype-based language. We can do more with prototypes than just adding properties and methods, such as inheritance.


The "hello world" of inheritance is the mammal object. Let's recreate it with JavaScript.


// our Mammal blueprint
function Mammal(name) {
  this.name = name;
}

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


In JavaScript, there's a static function inside the Object object:


Object.create();


It creates an object similarly to {} and new functionBlueprint, but the difference is create can take a prototype as a parameter to inherit from.


// 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;


Now Cat will have the breathe method found in Mammal, but the important thing to know is Cat is pointing to Mammal as its prototype.

Explanation:

  1. Mammal Blueprint: We first define the Mammal function and add a breathe method to its prototype.


  2. Cat Inheritance: We create the Cat function and set Cat.prototype to Object.create(Mammal.prototype). This makes the Cat prototype inherit from Mammal, but it changes the constructor pointer to Mammal.


  3. Correcting Constructor: We correct the Cat.prototype.constructor to point back to Cat, ensuring that the Cat object retains its identity while inheriting methods from Mammal. Finally, we add a meow method to Cat.


This approach allows the Cat object to access methods from both Mammal (like breathe) and its own prototype (like meow).


We need to correct that. Let's create the full example:


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.`);
};


To understand the Cat.prototype.constructor = Cat, you need to know about pointers. When we inherit from Mammal with Object.create, it changes the pointer of our Cat prototype to Mammal, which is wrong. We still want our Cat to be its own individual, despite having the parent Mammal.


That's why we have to correct it.


In this example, Cat inherits from Mammal using the prototype chain. The Cat object can access both breathe and meow methods.


const myCat = new Cat("Misty", "Ragdoll");

myCat.breathe(); // Misty is breathing.
myCat.meow(); // Misty is meowing.


We can create a dog also inheriting from the mammal:


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.


We have created basic classical inheritance, but why is this important? I thought we were covering source code!


Yes, true, but prototypes are the core of building efficient and flexible modules, beyond inheritance. Even simple, well-written modules are ridden with prototype objects. We are just laying the basics.


The alternative to inheritance is object composition, which loosely takes two or more objects and merges them together to form a "super" object.


Mixins allow objects to borrow methods from other objects without using inheritance. They're handy for sharing behavior between unrelated objects.


Which is what our first exploration does: the merge-descriptors library we promised to cover first.

Merge-Descriptors Module

We have already seen where and how it's used in Express. Now we know it for object composition.


In line 17, here is our first library:


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


Usage is in lines 42 and 43:


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


With what we know, we can already deduce that mixin is composing EventEmitter.prototype and proto into an object called app.


We will get to app when we start talking about Express.


This is the entire source code for 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;


From the beginning, always look at how the function is used and the parameters it takes:


// definition
mergeDescriptors(destination, source, overwrite = true)

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

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


App is our destination. We know mixin means object composition. Roughly, what this package is doing is composing the source object into the destination object, with an option to overwrite.


Overwrite, on assumption, is if app (destination) has an exact property the source has, on true overwrite, else leave that property untouched and skip.


We know objects cannot have the same property twice. In key-value pairs (objects), keys should be unique.

With Express, the overwrite is false.


The following is basic housekeeping, always handle expected errors:


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

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


This is where it gets interesting: line 12.


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


From above, we know what OwnProperty means, so getOwnPropertyNames clearly means get the keys of own properties.


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

Object.getOwnPropertyNames(person); // [ 'name', 'age', 'grow' ]


It returns the keys as an array, and we are looping over those keys in the following instance:


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


The following is checking if the destination and source have the same key we are currently looping over:


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


If overwrite is false, skip that property; don't overwrite. That is what continue does—it propels the loop over to the next iteration and doesn't run the code underneath, which is the following code:


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


We already know what getOwnProperty means. The new word is descriptor. Let's test this function on our own person object:


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
// }


It returns our grow function as the value, and the next line is self-explanatory:


Object.defineProperty(destination, name, descriptor);


It's taking our descriptor from the source and writing it into the destination. It's copying source own properties to our destination object as its own properties.


Let's do an example in our person object:


const val = {
    value: function isAlien() { return false; },
    enumerable: true,
    writable: true,
    configurable: true,
};

Object.defineProperty(person, "isAlien", val);


Now person should have the isAlien property defined.


In summary, this highly downloaded module copies own properties from a source object into a destination with an option to overwrite.


We have successfully covered our first module in this source code tier, with more exciting stuff to come.


This was an introduction. We started by covering the fundamentals needed to understand the module and, as a byproduct, understand the patterns in most modules, which is object composition and inheritance. Lastly, we navigated the merge-descriptors module.


This pattern will be prevalent in most articles. If I feel there are necessary fundamentals to cover, we will go through them in the first section and then cover the source code.


Luckily, merge-descriptors is used in Express, which is our focus as our start source code study. So expect more Express.js source code articles until we feel we have had a good enough run of Express, then switch to another module or tool like Node.js.


What you can do in the meantime as a challenge is navigate to the test file in merge descriptors, read the entire file. Reading source on your own is important, try and figure out what it does and is testing then break it, yes and fix it again or add more tests!


If you are interested in more exclusive practical and longer content to elevate your programming skills, you can find more on Ko-fi.