POO en JavaScript – Prototipos

JavaScript logo
JavaScript es un lenguaje de programación interpretado, definido como orientado a objetos basado en prototipos (classless), imperativo, débilmente tipado y dinámico.

JavaScript sigue algunos de los principios de la programación orientada a objetos, con un enfoque prototipado en donde los objetos ya existentes pueden servir de prototipo para los que se necesite crear. Además de su orientación POO, JavaScript permite trabajar fuertemente el paradigma de la programación funcional.

¿Por qué es classless?

JavaScript es un lenguaje basado en objetos que en lugar de estar basado en clases, se basa en prototipos. Debido a esta diferencia, puede resultar menos evidente que JavaScript le permite crear jerarquías de objetos y herencia de propiedades y valores.

En lenguajes basados en clases los objetos pueden ser de dos tipos generales: las clases y las instancias. Una clase es una plantilla que define la funcionalidad (métodos y propiedades) de los objetos. Las instancias son objetos “utilizables” creados a partir de una clase y conservan los datos del objeto.

Los lenguajes basados en prototipos como JavaScript no hacen esta distinción, simplemente tienen objetos, que son clase e instancia simultáneamente: son clase con respecto a los objetos que se crean como instancia suya, y son objetos en si, tomados como instancia de una clase implícita, establecida en su creación, y que se conoce como su prototipo.

JavaScript tiene la noción de un objeto prototipo, que es utilizado como una plantilla de la cual se obtiene las propiedades para crear un nuevo objeto. Cualquier objeto puede especificar sus propiedades, tanto al ser creado como en tiempo de ejecución. Adicionalmente, cualquier objeto puede servir de prototipo para otro objeto, permitiendo que el segundo objeto comparta las propiedades del primero. Y el prototipo puede ser modificado dinámicamente de modo que se afecten todos los objetos que pertenezcan a ese prototipo.

Definición de una clase

En los lenguajes basados en clases, se define una clase mediante un constructor.
El constructor es un método que permite crear instancias de una clase (el cual puede ser sobrecargado) y la definición de la clase está separada del constructor. Para crear una nueva instancia, utilizamos el operador new junto al constructor.

JavaScript sigue un modelo similar, pero sin tener la definición de clase separada del constructor… ❓ ¡momento! ¿no habíamos dicho que JavaScript es classless?

Bueno, efectivamente JavaScript es classless, y por eso mencioné primeramente la definición de una clase en los lenguajes basados en clases. JavaScript puede emular una clase definiendo una función como el constructor, y para crear un nuevo objeto, utilizamos el operador new junto a la función constructor.

    // constructor
    function Persona (nombre) {
        // implementación
    }

    // "instancia"
    var david = new Persona("David");

Herencia

En lenguajes basados en clases, se crea una jerarquía de clases mediante la herencia. En una definición de clase se puede especificar que la nueva clase hereda las propiedades de alguna clase ya existente, por lo tanto es una subclase.

JavaScript implementa la herencia mediante prototipos, asociando un objeto prototipo con cualquier función constructora. La herencia realmente se basa en la clonación de un prototipo y el mecanismo que determina cuales son las propiedades heredadas (compartidas) se denomina delegación automática.

Para implementar la herencia primero definimos el constructor del objeto base con todas sus propiedades, luego definimos el constructor de la subclase con sus propiedades, y finalmente pasamos el prototipo del objeto base o creamos una instancia del objeto base y lo establecemos como el prototipo de la subclase, de este modo heredará las propiedades del objeto base mediante la delegación automática.

En un futuro artículo: POO en JavaScript – Constructores veremos la diferencia entre a) pasar el prototipo del objeto base como prototipo de la subclase, y b) crear una instancia del objeto base como prototipo de la subclase.


Nota: el término clase en JavaScript no tiene significado técnico, ya que no hay separación entre clase y constructor, al igual que el término instancia lo podemos utilizar informalmente para dar a entender que un objeto es creado a partir de una función constructora. Del mismo modo, los términos clase base y subclase no tienen significado técnico, pero se usan informalmente para referirse a objetos que están por encima o por debajo de la cadena de prototipos.


En este ejemplo primero vamos a crear el constructor del objeto base llamado Person y definiremos las propiedades de la instancia y del prototipo (propiedades compartidas por todas las instancias)

❗ Usaremos Strict mode en los constructores para evitar que se creen propiedades en el objeto global en caso de olvidar anteponer el operador new cuando se crea una instancia.

Nota: por convención de nombres, el constructor debe empezar en mayúscula.

    //definimos el constructor del objeto base y
    //establecemos las propiedades por instancia
    function Person (name, age) {
        "use strict";
        this.name = name || "unnamed";
        this.age = +age || 0;
    }

    //definimos el prototipo del objeto base
    //estableciendo las propiedades compartidas
    //(delegación automática)
    Person.prototype = {
        setAge: function (age) {
            this.age = +age || 0;
        },
        speak: function (message) {
            message = message ? ", " + message : "";
            return "Hi, my name is " + this.name + message;
        }
    };

Ahora vamos a crear la “subclase” Teacher que heredará del objeto Person.

    //definimos el constructor de la "subclase" y
    //establecemos las propiedades por instancia
    function Teacher (name) {
        "use strict";
        this.name = name;
        this.subjects = [];
    }

    //heredamos del objeto base
    //mediante instanciación
    Teacher.prototype = new Person;

    //corregimos el constructor
    //porque fue sobrescrito por la herencia
    Teacher.prototype.constructor = Teacher;

    //extendemos el prototipo de la "subclase"
    //estableciendo las propiedades compartidas
    //(delegación automática)
    Teacher.prototype.addSubject = function (matter) {
        if (matter) this.subjects.push(matter);
    };

JavaScript también permite hacer sobrescritura de métodos (override)

    //@override (sobrescribe toString)
    Person.prototype.toString = function() {
        return "[Person Object]";
    }

    //@override (sobrescribe toString)
    Teacher.prototype.toString = function() {
        return "[Teacher Person]";
    };

Ahora creamos una instancia para cada tipo de objeto.

    //"instancias" de Person y Teacher
    var luis = new Person("Luis"),
        juan = new Teacher("Juan");

    //invoca el método definido en el prototipo de Teacher
    juan.addSubject("Design Patterns");

    //invoca un método definido en el prototipo de Person
    juan.setAge(32);

    console.log(luis.toString(), luis); //[Person Object]
    console.log(juan.toString(), juan); //[Teacher Person]

El código anterior produce la siguiente salida, en donde podemos ver la cadena de prototipos de los dos objetos, definida en la seudo-propiedad oculta __proto__

cadena de prototipos

La propiedad __proto__ determina la cadena de prototipos que se usará para devolver el valor de una propiedad cuando se acceda a ella. Recomiendo que lean todo el magnífico artículo Detalles del modelo de objetos de MDN.

Determinar si A es instancia de B

La forma más fácil de comprobar si un objeto A es una instancia de B o C es mediante el operador instanceof, el cual comprueba si un objeto tiene en su cadena de prototipos la propiedad prototype del constructor. (ver stackoverflow)

Otra forma de hacerlo es mediante el método isPrototypeOf el cual comprueba si un objeto está en la cadena de prototipos de otro objeto.

    //veamos el prototipo de juan
    Object.getPrototypeOf(juan);

    juan instanceof Teacher;
    Teacher.prototype.isPrototypeOf(juan);
    //true, porque Teacher.prototype está en Object.getPrototypeOf(juan)

    juan instanceof Person;
    Person.prototype.isPrototypeOf(juan);
    //true, porque Person.prototype está en Object.getPrototypeOf(juan)

    juan instanceof Object;
    Object.prototype.isPrototypeOf(juan);
    //true, porque Object.prototype está en Object.getPrototypeOf(juan)

    juan instanceof Date;
    Date.prototype.isPrototypeOf(juan);
    //false, porque Date.prototype no está en Object.getPrototypeOf(juan)

Añadir propiedades locales

Crear nuevas propiedades o métodos en un objeto concreto es muy fácil y podemos hacerlo de dos formas básicas: utilizando la notación punto o mediante el indexador de propiedades (tipo hash/dictionary).

Nota: Existen más formas de definir o modificar propiedades en un objeto y manipular los descriptores de dicha propiedad, por ejemplo utilizando los métodos Object.defineProperty, Object.defineProperties y Object.create.

Recomiendo leer el artículo Definir propiedades de un objeto para ver en detalle los diferentes mecanismos que podemos utilizar en JavaScript para establecer propiedades en un objeto.

    //agregamos una propiedad a cada instancia
    luis["isBiker"] = true;
    juan.isRoller = true;

    console.log(luis);
    console.log(juan);

Añadir propiedades al objeto base

A diferencia de los lenguajes basados en clases, en JavaScript podemos agregar o quitar propiedades de un objeto. Si se añade una propiedad a un objeto que es prototipo de otros, entonces todos los objetos relacionados con ese prototipo tendrán acceso a la nueva propiedad, debido al mecanismo de delegación automática.

    //agregamos una propiedad al prototipo del objeto base
    Person.prototype.setGenre = function (genre) {
        this.genre = genre;
    };

    console.log(luis); //setGenre accesible por prototipo
    console.log(juan); //setGenre accesible por herencia

Eliminar propiedades de un objeto

Para remover una propiedad de un objeto, lo hacemos utilizando el operador delete. Si la operación delete es exitosa, el resultado es true y la propiedad del objeto es eliminada, sin embargo, si el objeto posee una propiedad con el mismo nombre en la cadena de prototipos, el objeto conserva la propiedad del prototipo debido a la delegación automática.

    delete juan.name; //-> true, queda "name" en el prototipo padre (Person)
    delete luis.name; //-> true, "name" no está en el prototipo padre (Object)

    console.log(juan.name); //-> "unnamed"
    console.log(luis.name); //-> undefined

El operador delete sólo es efectivo en las propiedades configurables de un objeto, no tiene ningún efecto si lo aplicamos sobre una variable o una función local; sin embargo puede ser aplicado sobre las variables/funciones globales, ya que realmente son accedidas a través del objeto global window.

(function() {

    //verificamos cual es el ámbito global
    //(generalmente el objeto window)
    console.log("Global scope", this);

    var x = 42; //variable local
    var point = { x: 10, y: 15 }; //objeto local

    //si no se especifica el keyword "var",
    //se crea como una propiedad del objeto global
    z = 100;

    //método del objeto global
    func = function() {
        var n = 5;
        console.log("delete n:", delete n); //-> false
        //no se elimina porque es una variable local
        return n;
    };

    //ejecutamos el método global
    func();

    //intentamos eliminar las propiedades
    console.log("delete x:", delete x); //-> false
    console.log("delete z:", delete z); //-> true
    console.log("delete point:", delete point); //-> false
    console.log("delete point.x:", delete point.x); //-> true
    console.log("delete func:", delete func); //-> true

    //"func" y "z" se pudieron eliminar porque son parte
    //del objeto global, es decir: window.func y window.z
 }());

Conclusiones

Si bien, en este artículo apenas rasguñamos la superficie de la POO en JavaScript, ya tenemos una idea de lo que podemos hacer con un lenguaje de programación orientado a objetos basado en prototipos. JavaScript es asombroso por su flexibilidad, y que al ser interpretado, rompe algunas barreras de los lenguajes estrictos y compilados, sin embargo debemos estar atentos a evitar las malas prácticas, ya que muchas veces por desconocimiento del lenguaje, acabamos escribiendo código que va en contra de los paradigmas del lenguaje.

Ésta fue una introducción a las buenas prácticas, un llamado a que estudiemos mas a fondo las reglas que definen a JavaScript, de manera que podamos escribir código legible, extensible y fácil de mantener. Espero que este material sea de utilidad para ustedes, y en el siguiente artículo continuaré con algo más de POO en JavaScript.

⭐ Como siempre, dejo una lectura recomendada: Detalles del modelo de objetos.

Happy coding!

6 thoughts on “POO en JavaScript – Prototipos

  1. Carlos says:

    Hola, qué libro me recomiendas o por donde empezar para iniciar correctamente con javascript? El problema es que hay mucho material disperso, sin orden aunque muy bueno.
    Gracias por el post. Saludos

Comentarios

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s