POO en JavaScript – Prototipos

OOP in JS
JavaScript es un lenguaje de programación interpretado*, definido como orientado a objetos basado en prototipos (class-less), 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 del paradigma OOP, JavaScript también permite trabajar el paradigma de programación funcional.

¿Por qué es class-less?

🙄 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.

💡 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 su función es conservar los datos dentro del objeto.

😳 En un lenguaje basado en prototipos como JavaScript no se hace esta distinción, simplemente tiene 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 sí, tomados como instancia de una clase implícita, de la cual “heredan” 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 o “hereden” 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 en donde la definición de la clase está separada del constructor.

    // clase en C#
    public class Employee {
    
        // constructor
        public Employee (string nombre) {
            // implementación
        }
    }

    // instancia
    Employee david = new Employee("David");

JavaScript sigue un modelo similar, pero sin tener la definición de la clase separada del constructor… ^^’ ¿pero no habíamos dicho que JavaScript es class-less?

Bueno, efectivamente JavaScript (ES5-) es class-less, sin embargo se puede emular una clase definiendo una función como el constructor y utilizando el operador new para crear una nueva instancia.

    // clase y constructor (JS)
    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.

    // HomeController hereda de Controller (C#)
    public class HomeController : Controller {
    
    }

💡 JavaScript implementa la herencia mediante prototipos, asociando un objeto prototipo con una función constructora. 😮 La herencia realmente se basa en la clonación de un prototipo en donde las propiedades heredadas se determinan mediante la delegación automática, esto es, el orden o jerarquía en que las propiedades compartidas son resueltas en la cadena de prototipos de un objeto.


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.


1. Para implementar la herencia primero definimos el constructor del objeto base con todas sus propiedades:

    function Runner () {
        this.tasks = [];
        this.add = function (describe, action) {};
        this.run = function () {};
    }

2. Luego definimos el constructor de la subclase con sus propiedades:

    function RunnerPlus () {
        this.beforeRun = function () {};
        this.afterRun = function () {};
    }

3. Y finalmente pasamos el prototipo (o instancia) del objeto base y se lo asignamos al prototipo de la subclase, de este modo heredará las propiedades del objeto base.

    RunnerPlus.prototype = new Runner();

    //reparamos el constructor original
    //que fue sobrescrito por la herencia
    RunnerPlus.prototype.constructor = RunnerPlus;

⭐ En el ejemplo anterior, cada vez que instanciamos un objeto Runner o RunnerPlus, se crean repetidamente los métodos definidos en su clase/constructor, lo cual no es recomendado ya que estamos ocupando más memoria de lo necesario. La forma más eficiente de crear métodos en una “clase” es hacerlo a través de su prototype. 😎 Ésta es una buena práctica ya que los métodos se registran sólo una vez en el prototipo de la clase/constructor y su referencia es compartida entre todas las instancias, aprovechando mejor la memoria. Veamos esto en el siguiente ejemplo.

Aplicando conceptos

Primero vamos a crear el constructor del objeto base Runner y definiremos sus propiedades y las del prototipo (propiedades compartidas por todas las instancias)

❗ Usaremos Strict mode en el constructor para evitar que se creen propiedades en el objeto global cuando no se use el operador new al crear una instancia.

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

    //definimos el constructor del objeto base y
    //establecemos sus propiedades
    function Runner () {
        "use strict";
        this.tasks = [];
    }

    //definimos las propiedades compartidas
    //en el prototipo del objeto base
    Runner.prototype = {
        add: function (describe, action) {
            if (typeof action !== "function") {
                throw new TypeError("{action} must be a function");
            }
            this.tasks.push({
                title: describe,
                action: action
            });
        },
        run: function () {
            this.tasks.forEach(function(task, i) {
                var title = task.title || ("task [" + i + "]");
                console.warn("Running...", title);
                task.action(i);
            });
        }
    };

Ahora vamos a crear la “subclase” RunnerPlus que heredará de la clase Runner.

    //definimos el constructor de la "subclase"
    function RunnerPlus () {
        "use strict";
        this.callbacks = {
            beforeRun: null
        };
    }

    //heredamos del objeto base
    //mediante instanciación
    RunnerPlus.prototype = new Runner;

    //corregimos el constructor
    //que fue sobrescrito por la herencia
    RunnerPlus.prototype.constructor = RunnerPlus;

    //extendemos el prototipo de la "subclase"
    //estableciendo las propiedades compartidas
    RunnerPlus.prototype.clear = function () {
        this.callbacks.beforeRun = null;
        this.tasks.length = 0;
    };

    RunnerPlus.prototype.beforeRun = function (callback) {
        if (typeof callback !== "function") {
            throw new TypeError("{callback} must be a function");
        }
        this.callbacks.beforeRun = callback;
    };

Y como JavaScript permite hacer override, sobrescribimos el método run().

    //monkey-patch del antiguo método run
    var legacyRun = RunnerPlus.prototype.run;

    //@override
    RunnerPlus.prototype.run = function() {
        if (this.callbacks.beforeRun) {
            this.callbacks.beforeRun();
        }
        //establece el contexto de ejecución
        //con la instancia actual (this)
        legacyRun.call(this);
    }

💡 Al hacer override de run, primero guardamos la referencia del método original, y luego al invocarlo dentro del nuevo método run necesitamos asegurarnos que se ejecute en el contexto de la instancia actual (para ello utilizamos .call); 😉 esto es necesario, porque en la implementación original de run() accedemos al valor this.

Ahora creamos una instancia y probamos los métodos de la clase base y de la clase extendida.

    var mytasks = new RunnerPlus();

    //add() es definido en la clase base
    mytasks.add("Testing", function(i) {
        console.log("task [" + i + "] doing something");
    });

    mytasks.add("", alert.bind(null, "pre-configured alert"));

    //beforeRun() es definido en la subclase
    mytasks.beforeRun(function() {
        console.log("Running beforeRun just once");
    });

    mytasks.run();
    mytasks.clear();

    //lista de tareas vacía
    mytasks.run();

➡ Vamos a verificar si nuestra instancia tiene registrada en su cadena de prototipos las “clases” de las cuales hereda.

    mytasks instanceof RunnerPlus // true
    mytasks instanceof Runner // true
    mytasks instanceof Object // true
    mytasks instanceof Function // false

Si inspeccionamos la instancia creada, 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, esto es, mediante delegación automática.

Recomiendo que lean 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 es instancia de otro, es mediante el operador instanceof, el cual comprueba si un objeto tiene en su cadena de prototipos las propiedades definidas en el constructor (ver stackoverflow).

    mytasks instanceof RunnerPlus // true

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
    Object.getPrototypeOf(mytasks);

    mytasks instanceof Runner; //true
    Runner.prototype.isPrototypeOf(mytasks); //true
    //Runner.prototype está en Object.getPrototypeOf(mytasks)

    mytasks instanceof RunnerPlus; //true
    RunnerPlus.prototype.isPrototypeOf(mytasks); //true
    //RunnerPlus.prototype está en Object.getPrototypeOf(mytasks)

    mytasks instanceof Object; //true
    Object.prototype.isPrototypeOf(mytasks); //true
    //Object.prototype está en Object.getPrototypeOf(mytasks)

    mytasks instanceof Function; //false
    Function.prototype.isPrototypeOf(mytasks); //false
    //Function.prototype NO está en Object.getPrototypeOf(mytasks)

Añadir propiedades locales

Crear nuevas propiedades o métodos en un objeto concreto es muy fácil y podemos hacerlo de dos formas literales básicas: utilizando la notación punto o mediante la tabla de nombres de propiedades de un objeto.

    //agregamos propiedades al objeto
    mytasks["#text"] = "some text";
    mytasks.delay = 500;

    console.log(mytasks);

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:

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.

Añadir propiedades al prototipo

En JavaScript podemos agregar o quitar propiedades de un objeto en tiempo de ejecución. Si se añade una propiedad a un prototipo, entonces todos los objetos que hereden ese prototipo tendrán acceso a la nueva propiedad, debido al mecanismo de delegación automática.

    //agregamos una propiedad al prototipo
    RunnerPlus.prototype.afterRun = function (callback) {};

    //agregamos una propiedad al prototipo base
    Runner.prototype.alert = alert.bind(null, "Bound alert");

    console.log(mytasks);

Eliminar propiedades de un objeto

Para quitar una propiedad de un objeto, utilizamos el operador delete. Si dicha operación delete es exitosa, devuelve true al eliminar la propiedad del objeto, de otro modo, devuelve false. o_O Sin embargo, si en la cadena de prototipos dicha propiedad esta definida más de una vez, se tendrá que hacer uso de delete tantas veces como se haya definido.

    mytasks.alert = alert.bind(null, "Hey!");
    mytasks.alert();

    delete mytasks.alert; //true, pero aun esta definida en Runner.prototype
    mytasks.alert();

    delete mytasks.alert; //true, eliminada totalmente
    console.log(mytasks.alert);

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 los objetos globales, ya que realmente son accedidos como propiedades del objeto global window.

(function() {
    //verificamos cual es el objeto global
    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,
    //a menos que se utilice "use strict";
    z = 100;

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

    //"z" se pudo eliminar porque es una propiedad
    //del objeto global, es decir: window.z
 }());

Conclusiones

Si bien, en este artículo apenas tocamos 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 rompe algunas barreras de los lenguajes estrictos, sin embargo debemos estar atentos a evitar las malas prácticas, ya que muchas veces por desconocimiento del lenguaje, terminamos escribiendo código que va en contra de los principios 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.

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

😎 Happy coding!

Advertisements

9 thoughts on “POO en JavaScript – Prototipos

  1. Fernando MolinRI says:

    Hola, una duda.:
    Al final intentas eliminar “point” y no podes porque es local. te da false.
    pero abajo de eso intentas eliminar “point.x” (que sigue siendo local, creo) y si podes, osea te da true.
    Porque esta eso asi.??? gracias.

    • point es una variable local, y x es una propiedad de point, por lo tanto point.x se puede eliminar. Todas las propiedades de los objetos se pueden eliminar, a no ser que se especifique lo contrario, po ejemplo si ha sido definida con defineProperty y el atributo writable=false

  2. 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 )

Google+ photo

You are commenting using your Google+ 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 )

Connecting to %s