Hire a web Developer and Designer to upgrade and boost your online presence with cutting edge Technologies

Thursday, January 18, 2024

How Marketing Changed OOP In JavaScript

 Discussing the decisions surrounding JavaScript prototypes, the article by Juan Diego Rodriguez scrutinizes their origin, examines missteps in the design, and explores how these factors have affected the way we write JavaScript today.

Even though JavaScript’s name was coined from the Java language, the two languages are worlds apart. JavaScript has more in common with Lisp and Scheme, sharing features such as first-class functions and lexical scoping.

JavaScript also borrows its prototypal inheritance from the Self language. This inheritance mechanism is perhaps what many — if not most — developers do not spend enough time to understand, mainly because it isn’t a requirement to start working with JavaScript. That characteristic can be seen as either a design flaw or a stroke of genius. That said, JavaScript’s prototypal nature was marketed and hidden behind a “Java for the web” mask. We’ll elaborate more on that as we go on.

JavaScript isn’t confident in its own prototypal nature, so it gives developers the tools to approach the language without ever having to touch a prototype. This was an attempt to be easily understood by every developer, especially those coming from class-based languages, such as Java, and would later become one of JavaScript’s biggest enemies for years to come: You don’t have to understand how JavaScript works to code in JavaScript.

What Is Classical Object-Oriented Programming?

Classical object-oriented programming (OOP) revolves around the concept of classes and instances and is widely used in languages like Java, C++, C#, and many others. A class is a blueprint or template for creating objects. It defines the structure and behavior of objects that belong to that class and encapsulates properties and methods. On the other hand, objects are instances of classes. When you create an object from a class, you’re essentially creating a specific instance that inherits the structure and behavior defined in the class while also giving each object an individual state.

OOP has many fundamental concepts, but we will focus on inheritance, a mechanism that allows one class to take on the properties and methods of another class. This facilitates code reuse and the creation of a hierarchy of classes.

What’s Prototypal OOP In JavaScript?

I will explain the concepts behind prototypal OOP in Javascript, but for an in-depth explanation of how prototypes work, MDN has an excellent overview on the topic.

Prototypal OOP differs from classical OOP, which is based on classes and instances. In prototypal OOP, there are no classes, only objects, and they are created directly from other objects.

If we create an object, it will have a built-in property called prototype that holds a reference to its “parent” object prototype so we can access its prototype’s methods and properties. This is what allows us to access methods like .sort() or .forEach() from any array since each array inherits methods from the Array.prototype object.

The prototype itself is an object, so the prototype will have its own prototype. This creates a chain of objects known as the prototype chain. When you access a property or method on an object, JavaScript will first look for it on the object itself. If it’s not found, it will traverse up the prototype chain until it finds the property or reaches the top-level object. It will often end in Object.prototype, which has a null prototype, denoting the end of the chain.

A crucial difference between classical and prototypal OOP is that we can’t dynamically manipulate a class definition once an object is created. But with JavaScript prototypes, we can add, delete, or change methods and properties from the prototype, affecting the objects down the chain.

“Objects inherit from objects. What could be more object-oriented than that?”

Douglas Crockford

What’s The Difference In JavaScript? Spoiler: None

So, on paper, the difference is simple. In classical OOP, we instantiate objects from a class, and a class can inherit methods and properties from another class. In prototypal OOP, objects can inherit properties and methods from other objects through their prototype.

However, in JavaScript, there is not a single difference beyond syntax. Can you spot the difference between the following two code excerpts?

// With Classes

class Dog {
  constructor(name, color) {
    this.name = name;

    this.color = color;
  }

  bark() {
    return `I am a ${this.color} dog and my name is ${this.name}.`;
  }
}

const myDog = new Dog("Charlie", "brown");

console.log(myDog.name); // Charlie

console.log(myDog.bark()); // I am a brown dog and my name is Charlie.
// With Prototypes

function Dog(name, color) {
  this.name = name;

  this.color = color;
}

Dog.prototype.bark = function () {
  return `I am a ${this.color} dog and my name is ${this.name}.`;
};

const myDog = new Dog("Charlie", "brown");

console.log(myDog.name); // Charlie

console.log(myDog.bark()); // I am a brown dog and my name is Charlie.

There is no difference, and JavaScript will execute the same code, but the latter example is honest about what JavaScript is doing under the hood, while the former hides it behind syntactic sugar.

Do I have a problem with the classical approach? Yes and no. An argument can be made that the classical syntax improves readability by having all the code related to the class inside a block scope. On the other hand, it’s misleading and has led thousands of developers to believe that JavaScript has true classes when a class in JavaScript is no different from any other function object.

My biggest issue isn’t pretending that true classes exist but rather that prototypes don’t.

Consider the following code:

class Dog {
  constructor(name, color) {
    this.name = name;

    this.color = color;
  }

  bark() {
    return `I am a ${this.color} dog and my name is ${this.name}.`;
  }
}

const myDog = new Dog("Charlie", "brown");

Dog.prototype.bark = function () {
  return "I am really just another object with a prototype!";
};

console.log(myDog.bark()); // I am really just another object with a prototype!"

Wait, did we just access the class prototype? Yes, because classes don’t exist! They are merely functions returning an object (called constructor functions), and, inevitably, they have a prototype which means we can access its .prototype property.

It almost looks like JavaScript tries to hide its prototypes. But why?

There Are Clues In JavaScript’s History #

In May 1995, Netscape involved JavaScript creator Brendan Eich in a project to implement a scripting language into the Netscape browser. The main idea was to implement the Scheme language into the browser due to its minimal approach. The plan changed when Netscape closed a deal with Sun Microsystems, creators of Java, to implement Java on the web. Soon enough, Brendan Eich and Sun Microsystems founder Bill Joy saw the need for a new language. A language that was approachable for people whose main focus wasn’t only programming. A language both for a designer trying to make a website and for an experienced developer coming from Java.

With this goal in mind, JavaScript was created in 10 days of intense work under the early name of Mocha. It would be changed to LiveScript to market it as a script executing “live” in the browser but in December 1995, it would ultimately be named JavaScript to be marketed along with Java. This deal with Sun Microsystems forced Brendan to accommodate his prototype-based language to Java. According to Brendan Eich, JavaScript was treated as the “sidekick language to Java” and was greatly underfunded in comparison with the Java team:

“I was thinking the whole time, what should the language be like? Should it be easy to use? Might the syntax even be more like natural language? [...] Well, I’d like to do that, but my management said, “Make it look like Java.”

Eich’s idea for JavaScript was to implement Scheme first-class functions — a feature that would allow callbacks for user events — and OOP based on prototypes from Self. He’s expressed this before on his blog:

“I’m not proud, but I’m happy that I chose Scheme-ish first-class functions and Self-ish prototypes as the main ingredients.”

JavaScript’s prototypal nature stayed but would specifically be obscured behind a Java facade. Prototypes likely remained in place because Eich implemented Self prototypes from the beginning and they later couldn’t be changed, only hidden. We can find a mixed explanation in an old comment on his blog:

“It is ironic that JS could not have class in 1995 because it would have rivaled Java. It was constrained by both time and a sidekick role.”

Either way, JavaScript became a prototype-based language and the most popular one by far.

If Only JavaScript Embraced Its Prototypes #

In the rush between the creation of JavaScript and its mass adoption, there were several other questionable design decisions surrounding prototypes. In his book, JavaScript: The Good Parts, Crockford explains the bad parts surrounding JavaScript, such as global variables and the misunderstanding around prototypes.

As you may have noticed, this article is inspired by Crockford’s book. Although I disagree with many of his opinions about JavaScript’s bad parts, it’s important to note the book was published in 2008 when ECMAScript 4 (ES4) was the stable version of JavaScript. Many years have passed since its publication, and JavaScript has significantly changed in that time. The following are features that I think could have been saved from the language if only JavaScript had embraced its prototypes.

The this Value In Different Contexts #

The this keyword is another one of the things JavaScript added to look like Java. In Java, and classical OOP in general, this refers to the current instance on which the method or constructor is being invoked, just that. However, in JavaScript, we didn’t have class syntax until ES6 but still inherited the this keyword. My problem with this is it can be four different things depending on where is invoked!

1. this In The Function Invocation Pattern #

When this is invoked inside a function call, it will be bound to the global object. It will also be bound to the global object if it’s invoked from the global scope.

console.log(this); // window

function myFunction() {
  console.log(this);
}

myFunction(); // window

In strict mode and through the function invocation pattern, this will be undefined.

function getThis() {
  "use strict";

  return this;
}

getThis(); // undefined

2. this In The Method Invocation Pattern #

If we reference a function as an object’s property, this will be bound to its parent object.

const dog = {
  name: "Sparky",

  bark: function () {
    console.log(`Woof, my name is ${this.name}.`);
  },
};

dog.bark(); // Woof, my name is Sparky.

Arrow functions do not have their own this, but instead, they inherit this from their parent scope at creation.

const dog = {
  name: "Sparky",

  bark: () => {
    console.log(`Woof, my name is ${this.name}.`);
  },
};

dog.bark(); // Woof, my name is undefined.

In this case, this was bound to the global object instead of dog, hence this.name is undefined.

3. The Constructor Invocation Pattern #

If we invoke a function with the new prefix, a new empty object will be created, and this will be bound to that object.

function Dog(name) {
  this.name = name;

  this.bark = function () {
    console.log(`Woof, my name is ${this.name}.`);
  };
}

const myDog = new Dog("Coco");

myDog.bark(); // Woof, my name is Coco.

We could also employ this from the function’s prototype to access the object’s properties, which could give us a more valid reason to use it.

function Dog(name) {
  this.name = name;
}

Dog.prototype.bark = function () {
  console.log(`Woof, my name is ${this.name}.`);
};

const myDog = new Dog("Coco");

myDog.bark(); // Woof, my name is Coco.

4. The apply Invocation Pattern #

Lastly, each function inherits an apply method from the function prototype that takes two parameters. The first parameter is the value that will be bound to this inside the function, and the second is an array that will be used as the function parameters.

// Bounding `this` to another object

function bark() {
  console.log(`Woof, my name is ${this.name}.`);
}

const myDog = {
  name: "Milo",
};

bark.apply(myDog); // Woof, my name is Milo.

// Using the array parameter

const numbers = [3, 10, 4, 6, 9];

const max = Math.max.apply(null, numbers);

console.log(max); // 10

As you can see, this can be almost anything and shouldn’t be in JavaScript in the first place. Approaches like using bind() are solutions to a problem that shouldn’t even exist. Fortunately, this is completely avoidable in modern JavaScript, and you can save yourself several headaches if you learn how to dodge it; an advantage that ES6 class users can’t enjoy.

Crockford has a nice anecdote on the topic from his book:

“This is a demonstrative pronoun. Just having this in the language makes the language harder to talk about. It is like pair programming with Abbott and Costello.”

“But if we want to create a function constructor, we will need to use this.” Not necessarily! In the following example, we can make a function constructor that doesn’t use this or new to work.

function counterConstructor() {
  let counter = 0;

  function getCounter() {
    return counter;
  }

  function up() {
    counter += 1;

    return counter;
  }

  function down() {
    counter -= 1;

    return counter;
  }

  return {
    getCounter,

    up,

    down,
  };
}

const myCounter = counterConstructor();

myCounter.up(); // 1

myCounter.down(); // 0

We just created a function constructor without using this or new! And it comes with a straightforward syntax. A downside you could see is that objects created from counterConstructor won’t have access to its prototype, so we can’t add methods or properties from counterConstructor.prototype.

But do we need this? Of course, we will need to reuse our code, but there are better approaches that we will see later.

The new Prefix #

In JavaScript: The Good Parts, Crockford argues that we shouldn’t use the new prefix simply because there is no guarantee that we will remember to use it in the intended functions. I think that it’s an easy-to-spot mistake and also avoidable by capitalizing the constructor functions you intend to use with new. And nowadays, linters will warn us when we call a capitalized function without new, or vice-versa.

A better argument is simply that using new forces us to use this inside our constructor functions or “classes,” and as we saw earlier, we are better off avoiding this in the first place.

The Multiple Ways To Access Prototypes #

For the historical reasons we already reviewed, we can understand why JavaScript doesn’t embrace its prototypes. By extension, we don’t have tools to mingle with prototypes as straightforward as we would want, but rather devious attempts to manipulate the prototype chain. Things get worse when across documentation, we can read different jargon around prototypes.

The Difference Between [[Prototype]], __proto__, And .prototype #

To make the reading experience more pleasant, let’s go over the differences between these terms.

  • [[Prototype]] is an internal property that holds a reference to the object’s prototype. It’s enclosed in double square brackets, which means it typically cannot be accessed using normal notation.
  • __proto__ can refer to two possible properties:
    • It can refer to a property from any Object.prototype object that exposes the hidden [[Prototype]] property. It’s deprecated and ill-performing.
    • It can refer to an optional property we can add when creating an object literal. The object’s prototype will point to the value we give it.
  • .prototype is a property exclusive to functions or classes (excluding arrow functions). When invoked using the new prefix, the instantiated object’s prototype will point to the function’s .prototype.

We can now see all the ways we can modify prototypes in JavaScript. After reviewing, we will notice they all fall short in at least some aspect.

Using The __proto__ Literal Property At Initialization #

When creating a JavaScript object using object literals, we can add a __proto__ property. The created object will point its [[Prototoype]] to the value given in __proto__. In a prior example, objects created from our function constructor didn’t have access to the constructor prototype. We can use the __proto__ property at initialization to change this without using this or new.

function counterConstructor() {
  let counter = 0;

  function getCounter() {
    return counter;
  }

  function up() {
    counter += 1;

    return counter;
  }

  function down() {
    counter -= 1;

    return counter;
  }

  return {
    getCounter,

    up,

    down,

    __proto__: counterConstructor.prototype,
  };
}

The advantage of linking the new object’s prototype to the function constructor would be that we can extend its methods from the constructor prototype. But what good would it be if we needed to use this again?

const myCounter = counterConstructor();

counterConstructor.prototype.printDouble = function () {
  return this.getCounter() * 2;
};

myCounter.up(); // 1

myCounter.up(); // 2

myCounter.printDouble(); // 4

We didn’t even modify the count internal value but instead printed it double. So, a setter method would be necessary to manipulate its state from outside the initial function constructor declaration. However, we are over-complicating our code since we could have simply added a double method inside our function.

function counterConstructor() {
  let counter = 0;

  function getCounter() {
    return counter;
  }

  function up() {
    counter += 1;

    return counter;
  }

  function down() {
    counter -= 1;

    return counter;
  }

  function double() {
    counter = counter * 2;

    return counter;
  }

  return {
    getCounter,

    up,

    down,

    double,
  };
}

const myCounter = counterConstructor();

myCounter.up(); // 1

myCounter.up(); // 2

myCounter.double(); // 4

Using __proto__ is overkill in practice.

It’s vital to note that __proto__ must only be used when initializing a new object through an object literal. Using the __proto__ accessor in Object.prototype.__proto__ will change the object’s [[Prototoype]] after initialization, disrupting lots of optimizations done under the hood by JavaScript engines. That’s why Object.prototype.__proto__ is ill-performant and deprecated.

Object.create() #

Object.create() returns a new object whose [[Prototype]] will be the first argument of the function. It also has a second argument that lets you define additional properties to the new objects. However, it’s more flexible and readable to create an object using an object literal. Hence, its only practical use would be to create an object without a prototype using Object.create(null) since all objects created using object literals are automatically linked to Object.prototype.

Object.setPrototypeOf() #

Object.setPrototypeOf() takes two objects as arguments and will mutate the prototype chain from the former argument to the latter. As we saw earlier, switching an object’s prototype after initialization is ill-performing, so avoid it at all costs.

Encapsulation And Private Classes #

My last argument against classes is the lack of privacy and encapsulation. Take, for example, the following class syntax:

class Cat {
  constructor(name) {
    this.name = name;
  }

  meow() {
    console.log(`Meow! My name is ${this.name}.`);
  }
}

const myCat = new Cat("Gala");

myCat.meow(); // Meow! My name is Gala.

myCat.name = "Pumpkin";

myCat.meow(); // Meow! My name is Pumpkin.

We don’t have any privacy! All properties are public. We can try to mitigate this with closures:

class Cat {
  constructor(name) {
    this.getName = function () {
      return name;
    };
  }

  meow() {
    console.log(`Meow! My name is ${this.name}.`);
  }
}

const myCat = new Cat("Gala");

myCat.meow(); // Meow! My name is undefined.

Oops, now this.name is undefined outside the constructor’s scope. We have to change this.name to this.getName() so it can work properly.

class Cat {
  constructor(name) {
    this.getName = function () {
      return name;
    };
  }

  meow() {
    console.log(`Meow! My name is ${this.getName()}.`);
  }
}

const myCat = new Cat("Gala");

myCat.meow(); // Meow! My name is Gala.

This is with only one argument, so you can imagine how unnecessarily repetitive our code would be the more arguments we add. Besides, we can still modify our object methods:

myCat.meow = function () {
  console.log(`Meow! ${this.getName()} is a bad kitten.`);
};

myCat.meow(); // Meow! Gala is a bad kitten.

We can save and implement better privacy if we use our own function constructors and even make our methods immutable using Object.freeze()!

function catConstructor(name) {
  function getName() {
    return name;
  }

  function meow() {
    console.log(`Meow! My name is ${name}.`);
  }

  return Object.freeze({
    getName,

    meow,
  });
}

const myCat = catConstructor("Loaf");

myCat.meow(); // Meow! My name is Loaf.

And trying to modify the object’s methods will fail silently.

myCat.meow = function () {
  console.log(`Meow! ${this.getName()} is a bad Kitten.`);
};

myCat.meow(); // Meow! My name is Loaf.

And yes, I am aware of the recent proposal for private class fields. But do we really need even more new syntax when we could accomplish the same using custom constructor functions and closures?

So, Classes Or Prototypes In JavaScript? #

In Crockford’s more recent book, How JavaScript Works (PDF), we can see a better option than using Prototypes or Classes for code reuse: Composition!

Using prototypes feels like using a half-finished feature, while classes can lead to overcomplicated and unnecessary hierarchies (and also to `this` ). Fortunately, JavaScript is a multi-paradigm language, and forcing ourselves to only use classes or prototypes for code reusability is constraining ourselves with imaginary ropes.

As Crockford says in his more recent book:

“[I]nstead of same as except we can get a little bit of this and a little bit of that.”

— Douglas Crockford, How JavaScript Works

Instead of a function constructor or class inheriting from another, we can have a set of constructors and combine them when needed to create a specialized object.

function speakerConstructor(name, message) {
  function talk() {
    return `Hi, mi name is ${name} and I want to tell something: ${message}.`;
  }

  return Object.freeze({
    talk,
  });
}

function loudSpeakerConstructor(name, message) {
  const {talk} = speakerConstructor(name, message);

  function yell() {
    return talk().toUpperCase();
  }

  return Object.freeze({
    talk,

    yell,
  });
}

const mySpeaker = loudSpeakerConstructor("Juan", "You look nice!");

mySpeaker.talk(); // Hi, my name is Juan and I want to tell something: You look nice!

mySpeaker.yell(); // HI, MY NAME IS JUAN AND I WANT TO TELL SOMETHING: YOU LOOK NICE!

Without the need for this and new and classes or prototypes, we achieve a reusable function constructor with full privacy and encapsulation.

Conclusion #

Yes, JavaScript was made in 10 days in a rush; yes, it was tainted by marketing; and yes, it has a long set of useless and dangerous parts. Yet is a beautiful language and fuels a lot of the innovation happening in web development today, so it clearly has done something good!

I don’t think we will see a day when prototypes receive the features they deserve, nor one in which we stop using classical syntactic sugar, but we can decide to avoid them when possible.

Unfortunately, this conscious decision to stick to the good parts isn’t exclusive to JavaScript OOP since, between the rush into existence, the language brought a lot of other dubious features that we are better off not using. Maybe we can tackle them in a future article, but in the meantime, we will have to acknowledge their presence and make the conscious decision to keep learning and understanding the language to know which parts to use and which parts to ignore.

References #

No comments:

Post a Comment