Seleccionar página

class-objectEs posible que sea un ferviente defensor de la programación funcional y considere que las clases son algo absolutamente innecesario; también es posible que considere que la sintáxis de las clases que ahora están disponibles en Javascript es superflua y que es posible implementar prácticamente el mismo comportamiento con funciones, prototipos y los métodos del objeto Object de ES5. Nosotros nos declaramos profundamente agnóticos a las diferentes corrientes y seguimos una especie eclecticismo, incluso sincretismo, donde intentamos aprovechar todo lo que tenemos a nuestro alcance sin entrar en disquisiciones sobre los modelos -más o menos puros- de programación. En cualquier caso, conocer la sintaxis de ES6 para las clases es algo que va a ser interesante para todos. Por ello presentamos a continuación un resumen sintético de lo que ofrecen las clases de ES6. En otros artículos profundizaremos en algunos de estos aspectos.

Sintáxis básica

Para crear una clase en ES6 vamos a utilizar la palabra reservada class, en el modo “use strict”, de esta forma:

"use strict";

class Person {
    constructor(firstname, lastname, birthday) {
        this.firstname = firstname;
        this.lastname = lastname;
        this.birthday = new Date(birthday);
    }
    getAge() {
        var ageDifMs = Date.now() - this.birthday.getTime();
        var ageDate = new Date(ageDifMs);
        return Math.abs(ageDate.getUTCFullYear() - 1970);
    }
}

typeof de una clase

Si comprobamos el tipo de Person nos vamos a encontrar que typeof devuelve function, pero a diferencia de las funciones, no vamos a poder llamar directamente a Person y siempre vamos a tener que utilizar new:

// Comprobamos el tipo de Person
// Devuelve function
console.log(typeof Person);                 

// Intentamos llamar a Person como una función
// TypeError: Class constructors cannot be invoked without 'new'
Person('Pablo', 'Almunia', '07-08-1966');

// Creamos un objeto con new
var p = new Person('Pablo', 'Almunia', '07-08-1966');

Definición y uso

A diferencia de las funciones, las clases no pueden ser utilizadas antes de ser definidas. Las funciones en Javascript sufren un proceso denominado hoisting por el que las variables declaradas con var y las funciones son desplazadas al principio del alcance donde se encuentra y como consecuencia pueden ser llamadas antes de su declaración. En el caso de las clases esto no funciona así y si intentamos utilizar una clase antes de definirla obtendremos un error:

"use strict";

// Producirá un error, ya que estamos usando una clase antes de declararla
// ReferenceError: Person is not defined
var p = new Person('Pablo', 'Almunia', '07-08-1966');

class Person {
    constructor(firstname, lastname, birthday) {
        this.firstname = firstname;
        this.lastname = lastname;
        this.birthday = new Date(birthday);
    }
    get age() {
        var ageDifMs = Date.now() - this.birthday.getTime();
        var ageDate = new Date(ageDifMs);
        return Math.abs(ageDate.getUTCFullYear() - 1970);
    }
}

Expresiones de clases

De forma similar a las funciones, es posible asignar la definición de una clase a una variable. A diferencia de las funciones, no se aceptan clases anónimas, pero al igual que en caso de las funciones, el nombre de la clase queda oculto y sólo es posible utilizar el nombre de la variable que se ha utilizado en la asignación:

"use strict";

const People = class Person {
    constructor(firstname, lastname, birthday) {
        this.firstname = firstname;
        this.lastname = lastname;
        this.birthday = new Date(birthday);
    }
    getClassName() {
        return Person.name;
    }
};

var p = new People();
console.log(p.getClassName());

var p2 = new Person();

Constructor

Dentro de la definición de la clase no es posible escribir código directamente, ya que como tal no se comporta como una función, lo que vamos a encontrar es la definición de métodos. Uno de estos métodos se comporta de una manera especial: constructor. Esta función será llamada cuando se instancie un objeto de esta clase por medio de new. Este pseudométodo tiene algunas características singulares, como que representa la función de la propia clase:

// Es verdadero
console.log(Person === Person.prototype.constructor);

El constructor no tiene por qué devolver un objeto, de forma implícita devuelve el objeto this, pero podemos devolver lo que queramos y por lo tanto podemos hacer que haga cosas bastante curiosas sobrescribiendo el resultado de llamar a new:

"use strict";

class Person {
    constructor(firstname, lastname, birthday) {
        return {
            lastname,
            firstname,
            birthday: new Date(birthday)
        };
    }
    getAge() {
        var ageDifMs = Date.now() - this.birthday.getTime();
        var ageDate = new Date(ageDifMs);
        return Math.abs(ageDate.getUTCFullYear() - 1970);
    }
}

// Creamos un objeto
var p = new Person('Pablo', 'Almunia', '07-08-1966');

// Podemos llamar a las propiedades del objeto devuelto por el constructor
console.log(p.firstname, p.lastname);

// No podemos llamar el método getAge(), ya que no está disponible en el objeto 
// devuelto por el constructor, aunque exista en la definición de la clase
console.log(p.getAge());

Si no se define ningún método constructor se utilizará un constructor por defecto.

Métodos

El resto de métodos se definen en el cuerpo de la clase sin necesidad de utilizar function, basta con poner el nombre del método y paréntesis con los parámetros que recibirá (o ninguno) y escribir el cuerpo de la función.

"use strict";

class Person {
    constructor(firstname, lastname, birthday) {
        this.firstname = firstname;
        this.lastname = lastname;
        this.birthday = new Date(birthday);
    }
    getAge() {
        var ageDifMs = Date.now() - this.birthday.getTime();
        var ageDate = new Date(ageDifMs);
        return Math.abs(ageDate.getUTCFullYear() - 1970);
    }
    toString() {
        return this.firstname + ' ' +  this.lastname + ' (' + this.getAge() + ')';
    }
}

var p = new Person('Pablo', 'Almunia', '07-08-1966');

// Produce una llamada implícita a toString()
console.log('Person: ' + p);

Métodos estáticos

Si lo necesitamos podemos escribir métodos estáticos, es decir, que pueden ser invocados desde la clase sin necesidad de que se cree una instancia de la misma. Para ello tenemos que poner la palabra static antes del nombre del método:

"use strict";

class Person {
    constructor(firstname, lastname, birthday) {
        this.firstname = firstname;
        this.lastname = lastname;
        this.birthday = new Date(birthday);
    }
    static getClassName() {
        return 'Person class';
    }
}

// Devuelve "Person class"
console.log(Person.getClassName());

Propiedades

No es posible definir propiedades en el cuerpo de una clase. Para poderlas definir podemos utilizar el constructor (o desde cualquier otro método), tal y como hemos visto en el primer ejemplo, donde se definen las propiedades firstname, lastname y birthday por medio de this.

Una alternativa a esta forma de crear las propiedades es utilizar métodos get y/o set para definir dos funciones que nos sirvan para gestionar su acceso:

"use strict";

class Person {
    constructor(firstname, lastname, birthday) {
        this.firstname = firstname;
        this.lastname = lastname;
        this.birthday = new Date(birthday);
    }
    get age() {
        var ageDifMs = Date.now() - this.birthday.getTime();
        var ageDate = new Date(ageDifMs);
        return Math.abs(ageDate.getUTCFullYear() - 1970);
    }
}

var p = new Person('Pablo', 'Almunia', '07-08-1966');
console.log(p.firstname, p.lastname);
console.log(p.age);

Los métodos get y set también pueden ser estáticos si les ponemos static antes de su definición.

Herencia

Aunque en ES5 podemos utilizar la cadena de prototipos para realizar una herencia, con la sintaxis de clases de ES6 esto es mucho más sencillo y claro. Para ello debemos utilizar extends en la declaración de la clase:

"use strict";

class Person {
    constructor(firstname, lastname, birthday) {
        this.firstname = firstname;
        this.lastname = lastname;
        this.birthday = new Date(birthday);
    }
    get age() {
        var ageDifMs = Date.now() - this.birthday.getTime();
        var ageDate = new Date(ageDifMs);
        return Math.abs(ageDate.getUTCFullYear() - 1970);
    }
}

class Employee extends Person {
    constructor(firstname, lastname, birthday, position) {
        super(firstname, lastname, birthday);
        this.position = position;
    }
}

// Creamos un objeto
var p = new Employee('Pablo', 'Almunia', '07-08-1966', 'Architect');

// Consultamos sus propiedades
console.log(p.firstname, p.lastname, p.position);
console.log(p.age);

super

Para que un constructor o un método puedan llamar a miembros de la clase padre debe utilizar super que es una referencia a la clase superior. En el caso del constructor se le llamará simplemente con los parámetros, en el caso de querer llamara métodos o propiedades concretas usaremos super.nombredelmiembro.

"use strict";

class Person {
    constructor(firstname, lastname, birthday) {
        this.firstname = firstname;
        this.lastname = lastname;
        this.birthday = new Date(birthday);
    }
    get age() {
        var ageDifMs = Date.now() - this.birthday.getTime();
        var ageDate = new Date(ageDifMs);
        return Math.abs(ageDate.getUTCFullYear() - 1970);
    }
}

class Employee extends Person {
    constructor(firstname, lastname, birthday, position) {
        super(firstname, lastname, birthday);
        this.position = position;
    }
    get yearsUntilRetirement() {
        return 67 - super.age;
    }
}

// Creamos un objeto
var p = new Employee('Pablo', 'Almunia', '07-08-1966', 'Architect');

// Consultamos sus propiedades
console.log(p.firstname, p.lastname, p.position);
console.log(p.yearsUntilRetirement);

Constructor por defecto

Si la clase hija no dispone de constructor, se incluye un constructor por defecto que llama al constructor de la clase padre con los parámetros que se hayan pasado al invocar a new:

"use strict";

class Person {
    constructor(firstname, lastname, birthday) {
        this.firstname = firstname;
        this.lastname = lastname;
        this.birthday = new Date(birthday);
    }
    get age() {
        var ageDifMs = Date.now() - this.birthday.getTime();
        var ageDate = new Date(ageDifMs);
        return Math.abs(ageDate.getUTCFullYear() - 1970);
    }
}

class Unemployed extends Person {
    get yearsUntilRetirement() {
        return 67 - super.age;
    }
}

// Creamos un objeto
var p = new Unemployed('Pablo', 'Almunia', '07-08-1966');

// Consultamos sus propiedades
console.log(p.firstname, p.lastname, p.position);
console.log(p.yearsUntilRetirement);

Generadores e iterables

Las clases pueden tener métodos generadores, simplemente hay que utilizar el asterisco (*) antes del nombre del método y será un generador.

De esta forma podemos hacer que nuestros objetos sean iterables, es decir, que puedan ser utilizados por medio de for of y devolver las propiedades del objeto en cada llamada:

"use strict";

class Employee {
    constructor() {
        this.positions = [];
        for (let n in arguments) {
            this.positions.push(arguments[n]);
        }
    }
    * [Symbol.iterator]() {
        for (let pos of this.positions) {
            yield pos;
        }
    }
}

// Creamos un objeto
var p = new Employee('Developer', 'Analyst', 'Architec');

// Iteramos sus elementos
for (let pos of p) {
    console.log(pos);
}

Como se puede observar, se ha utilizado Symbol.iterator entre corchetes como nombre de la función generadora. En primer lugar hay que comentar que el uso de corchetes permite definir nombres de métodos calculados. En segundo lugar, Symbol.iterator nos permite indicar que la clase es iterable de forma sencilla.

Conclusión

Aunque hay algunos aspectos de las clases que trataremos posteriormente, la verdad es que su sintaxis es bastante sencilla. Aunque podamos discutir su utilidad o la conveniencia de su uso, lo cierto es que simplifican el uso de los objetos y sus prototipos que podemos hacer en ES5. Ciertamente no hay nada que no pudiéramos hacer ya, pero estamos seguros que esta forma de definir los objetos es, en la práctica, clara y fácil de manejar.

Novedades

Inspeccionar la herencia: la cadena de prototipos

Os invitamos a profundizar en las cadenas de prototipos de Javascript, os podéis sorprender en bastantes casos y siempre resulta muy instructivo. Comprender las cadenas de prototipos y la herencia nos puede ayudar a construir objetos más completos y eficientes.

Symbol: la privacidad que no es

Cuando se conocen las clases en Javascript se echan en falta algunos mecanismos para definir propiedades o métodos ocultos. Los Symbol son una potente herramienta, pero -aunque complica el acceso- no es una forma efectiva de ocultar los miembros de un objeto. Veamos porqué.

Javascript 2019: clases

Con la incorporación de nuevas funcionalidades (propiedades, miembros privados, decoradores), parece que 2019 es el año donde las clases y la programación orientada a objetos en Javascript van a dar un importante paso hacia adelante.

Desarrollando Skills Alexa con AWS Lambda y node.js por Germán Viscuso

Germán Viscuso, evangelista de la tecnología Alexa para España, nos cuenta en esta charla las posibilidades nos ofrece la tecnología de Amazon para crear Skills personalizados desarrollados en NodeJS y desplegados en Lamdas de AWS. Un nuevo mundo de posibilidades para los desarrolladores.