Introducción a la programación orientada a objetos en JavaScript: objetos, prototipos y clases

En muchos lenguajes de programación, las clases son un concepto bien definido. En JavaScript ese no es el caso. O al menos ese no fue el caso. Si busca OOP y JavaScript, se encontrará con muchos artículos con muchas recetas diferentes sobre cómo puede emular un classen JavaScript.

¿Existe una forma KISS simple de definir una clase en JavaScript? Y si es así, ¿por qué tantas recetas diferentes para definir una clase?

Antes de responder a esas preguntas, comprendamos mejor qué Objectes JavaScript .

Objetos en JavaScript

Comencemos con un ejemplo muy simple:

const a = {}; a.foo = 'bar';

En el fragmento de código anterior, se crea un objeto y se mejora con una propiedad foo. La posibilidad de agregar cosas a un objeto existente es lo que hace que JavaScript sea diferente de los lenguajes clásicos como Java.

Más en detalle, el hecho de que un objeto se pueda mejorar hace posible crear una instancia de una clase "implícita" sin la necesidad de crear realmente la clase. Aclaremos este concepto con un ejemplo:

function distance(p1, p2) { return Math.sqrt( (p1.x - p2.x) ** 2 + (p1.y - p2.y) ** 2 ); } distance({x:1,y:1},{x:2,y:2});

En el ejemplo anterior, no necesitaba una clase Point para crear un punto, solo extendí una instancia de Objectagregar xy ypropiedades. A la función distancia no le importa si los argumentos son una instancia de la clase Pointo no. Hasta que llame distancela función con dos objetos que tienen una xy la ypropiedad de tipo Number, que va a funcionar muy bien. Este concepto a veces se denomina escritura de pato .

Hasta ahora, solo he usado un objeto de datos: un objeto que contiene solo datos y no funciones. Pero en JavaScript es posible agregar funciones a un objeto:

const point1 = { x: 1, y: 1, toString() { return `(${this.x},${this.y})`; } }; const point2 = { x: 2, y: 2, toString() { return `(${this.x},${this.y})`; } };

Esta vez, los objetos que representan un punto 2D tienen un toString()método. En el ejemplo anterior, el toStringcódigo se ha duplicado y esto no es bueno.

Hay muchas formas de evitar esa duplicación y, de hecho, en diferentes artículos sobre objetos y clases en JS encontrarás diferentes soluciones. ¿Alguna vez ha oído hablar del "patrón de módulo revelador"? Contiene las palabras "patrón" y "revelador", suena genial y "módulo" es imprescindible. Así que debe ser la forma correcta de crear objetos ... excepto que no lo es. Revelar el patrón del módulo puede ser la elección correcta en algunos casos, pero definitivamente no es la forma predeterminada de crear objetos con comportamientos.

Ahora estamos listos para presentar clases.

Clases en JavaScript

¿Qué es una clase? De un diccionario: una clase es "un conjunto o categoría de cosas que tienen alguna propiedad o atributo en común y se diferencian de otras por su tipo, tipo o calidad".

En los lenguajes de programación solemos decir "Un objeto es una instancia de una clase". Esto significa que, usando una clase, puedo crear muchos objetos y todos comparten métodos y propiedades.

Dado que los objetos se pueden mejorar, como hemos visto anteriormente, existen formas de crear objetos compartiendo métodos y propiedades. Pero queremos el más simple.

Afortunadamente, ECMAScript 6 proporciona la palabra clave class, lo que facilita la creación de una clase:

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

Entonces, en mi opinión, esa es la mejor manera de declarar clases en JavaScript. Las clases suelen estar relacionadas con la herencia:

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

Como puede ver en el ejemplo anterior, para ampliar otra clase basta con utilizar la palabra clave extends.

Puede crear un objeto a partir de una clase usando el newoperador:

const p = new Point(1,1); console.log(p instanceof Point); // prints true

Una buena forma orientada a objetos de definir clases debería proporcionar:

  • una sintaxis simple para declarar una clase
  • una forma sencilla de acceder a la instancia actual, también conocida como this
  • una sintaxis simple para extender una clase
  • una forma sencilla de acceder a la instancia de superclase, también conocida como super
  • posiblemente, una forma sencilla de saber si un objeto es una instancia de una clase en particular. obj instanceof AClassdebería devolver truesi ese objeto es una instancia de esa clase.

La nueva classsintaxis proporciona todos los puntos anteriores.

Antes de la introducción de la classpalabra clave, ¿cuál era la forma de definir una clase en JavaScript?

Además, ¿qué es realmente una clase en JavaScript? ¿Por qué hablamos a menudo de prototipos ?

Clases en JavaScript 5

Desde la página de Mozilla MDN sobre clases:

Las clases de JavaScript, introducidas en ECMAScript 2015, son principalmente azúcar sintáctico sobre la herencia existente basada en prototipos de JavaScript . La sintaxis de la clase no introduce un nuevo modelo de herencia orientado a objetos en JavaScript.

El concepto clave aquí es la herencia basada en prototipos . Dado que hay muchos malentendidos sobre qué es ese tipo de herencia, procederé paso a paso, pasando de una classpalabra functionclave a otra.

class Shape {} console.log(typeof Shape); // prints function

Parece que classy functionestán relacionados. ¿Es classsolo un alias para function? No, no lo es.

Shape(2); // Uncaught TypeError: Class constructor Shape cannot be invoked without 'new'

Entonces, parece que las personas que introdujeron la classpalabra clave querían decirnos que una clase es una función que se debe llamar usando el newoperador.

var Shape = function Shape() {} // Or just function Shape(){} var aShape = new Shape(); console.log(aShape instanceof Shape); // prints true

El ejemplo anterior muestra que podemos usar functionpara declarar una clase. Sin embargo, no podemos obligar al usuario a llamar a la función utilizando el newoperador. Es posible lanzar una excepción si el newoperador no se utilizó para llamar a la función.

De todos modos, le sugiero que no ponga esa marca en cada función que actúa como una clase. En su lugar, utilice esta convención: cualquier función cuyo nombre comience con una letra mayúscula es una clase y debe llamarse utilizando el newoperador.

Sigamos adelante y descubramos qué es un prototipo :

class Shape { getName() { return 'Shape'; } } console.log(Shape.prototype.getName); // prints function getName() ...

Cada vez que declaras un método dentro de una clase, en realidad agregas ese método al prototipo de la función correspondiente. El equivalente en JS 5 es:

function Shape() {} Shape.prototype.getName = function getName() { return 'Shape'; }; console.log(new Shape().getName()); // prints Shape

A veces, las funciones de clase se denominan constructores porque actúan como constructores en una clase regular.

Puede preguntarse qué sucede si declara un método estático:

class Point { static distance(p1, p2) { // ... } } console.log(Point.distance); // prints function distance console.log(Point.prototype.distance); // prints undefined

Dado que los métodos estáticos están en una relación 1 a 1 con las clases, la función estática se agrega a la función constructora, no al prototipo.

Recapitulemos todos estos conceptos en un ejemplo simple:

function Point(x, y) { this.x = x; this.y = y; } Point.prototype.toString = function toString() { return '(' + this.x + ',' + this.y + ')'; }; Point.distance = function distance() { // ... } console.log(new Point(1,2).toString()); // prints (1,2) console.log(new Point(1,2) instanceof Point); // prints true

Hasta ahora, hemos encontrado una forma sencilla de:

  • declarar una función que actúa como una clase
  • acceder a la instancia de la clase usando la thispalabra clave
  • crear objetos que son en realidad una instancia de esa clase ( new Point(1,2) instanceof Pointdevuelve true)

Pero, ¿qué pasa con la herencia? ¿Qué hay de acceder a la superclase?

class Hello { constructor(greeting) { this._greeting = greeting; } greeting() { return this._greeting; } } class World extends Hello { constructor() { super('hello'); } worldGreeting() { return super.greeting() + ' world'; } } console.log(new World().greeting()); // Prints hello console.log(new World().worldGreeting()); // Prints hello world

Above is a simple example of inheritance using ECMAScript 6, below the same example using the the so called prototype inheritance:

function Hello(greeting) { this._greeting = greeting; } Hello.prototype.greeting = function () { return this._greeting; }; function World() { Hello.call(this, 'hello'); } // Copies the super prototype World.prototype = Object.create(Hello.prototype); // Makes constructor property reference the sub class World.prototype.constructor = World; World.prototype.worldGreeting = function () { const hello = Hello.prototype.greeting.call(this); return hello + ' world'; }; console.log(new World().greeting()); // Prints hello console.log(new World().worldGreeting()); // Prints hello world

This way of declaring classes is also suggested in the Mozilla MDN example here.

Using the class syntax, we deduced that creating classes involves altering the prototype of a function. But why is that so? To answer this question we must understand what the new operator actually does.

New operator in JavaScript

The new operator is explained quite well in the Mozilla MDN page here. But I can provide you with a relatively simple example that emulates what the new operator does:

function customNew(constructor, ...args) { const obj = Object.create(constructor.prototype); const result = constructor.call(obj, ...args); return result instanceof Object ? result : obj; } function Point() {} console.log(customNew(Point) instanceof Point); // prints true

Note that the real new algorithm is more complex. The purpose of the example above is just to explain what happens when you use the new operator.

When you write new Point(1,2)what happens is:

  • The Point prototype is used to create an object.
  • The function constructor is called and the just created object is passed as the context (a.k.a. this) along with the other arguments.
  • If the constructor returns an Object, then this object is the result of the new, otherwise the object created from the prototype is the result.

So, what does prototype inheritance mean? It means that you can create objects that inherit all the properties defined in the prototype of the function that was called with the new operator.

If you think of it, in a classical language the same process happens: when you create an instance of a class, that instance can use the this keyword to access to all the functions and properties (public) defined in the class (and the ancestors). As opposite to properties, all the instances of a class will likely share the same references to the class methods, because there is no need to duplicate the method’s binary code.

Functional programming

Sometimes people say that JavaScript is not well suited for Object Oriented programming, and you should use functional programming instead.

While I don’t agree that JS is not suited for O.O.P, I do think that functional programming is a very good way of programming. In JavaScript functions are first class citizens (e.g. you can pass a function to another function) and it provides features like bind , call or apply which are base constructs used in functional programming.

In addition RX programming could be seen as an evolution (or a specialization) of functional programming. Have a look to RxJs here.

Conclusion

Use, when possible, ECMAScript 6 class syntax:

class Point { toString() { //... } }

or use function prototypes to define classes in ECMAScript 5:

function Point() {} Point.prototype.toString = function toString() { // ... }

Hope you enjoyed the reading!