¿Sabías esto de JavaScript?

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.

Características como funciones variádicas (un número indefinido de parámetros pueden ser pasados a la función), tipos de datos dinámicos, hoisting, coerción, entre otros, hacen que JavaScript sea un lenguaje muy flexible, y por tal motivo, es mejor saber cómo se interpreta el código que escribimos, de lo contrario podemos caer en errores comunes y malas prácticas.


Como en la mayoría de los lenguajes de scripting, el tipo de dato de un objeto está asociado a su valor. JavaScript soporta varias formas de comprobar el tipo de un objeto (duck typing), una forma de saberlo es mediante el operador typeof.

Este es el panorama general que describe a JavaScript, y aunque algunos términos no son muy familiares, estoy seguro que si llevan tiempo programando en JavaScript, ya se habrán topado con algunos de ellos. Empecemos con el tour por JavaScript…

Hoisting en JavaScript

En la mayoría de los lenguajes de programación, el ámbito / alcance de una variable está en el bloque donde fue definida (block-level scope), por ejemplo, dentro de un bloque de instrucciones if, for, while; sin embargo en JavaScript (ECMAScript 5.1) esto no es así, puesto que el alcance de una variable es el de la función en la cual fue declarada (function-level scope).

El hoisting o “elevamiento” es una de las particularidades de JavaScript y se da cuando definimos una variable en el interior de una función (en el scope), entonces el intérprete interno pasa a declararla al comienzo de su contexto (la eleva); veamos el siguiente ejemplo:

    (function () {
        for(var x = 0; x < 3; x += 1) {
            console.log(x);
        }
        console.log(x, "todavía existe!!");
    }());

❓ ¿Que es lo que ocurrió?, como se mencionó anteriormente, el intérprete interno se encarga de elevar la declaración de variables al comienzo del contexto de ejecución. Veamos lo que realmente hace el navegador cuando interpreta el código anterior:

    (function () {
        var x; // <- la variable es elevada
        for(x = 0; x < 3; x += 1) {
            console.log(x);
        }
        console.log(x, "todavía existe!!");
    }());
Ahora observemos con más detalle el siguiente ejemplo:
    var x = 5;

    (function () {
        console.log("x:", x); //se espera 5
        var x = 10;
        console.log("x:", x); //se espera 10
    }());

Nuevamente ocurre algo inesperado, ya que la primera vez que imprimimos la variable se esperaba el valor de la variable global (5), sin embargo obtenemos undefined.💡 Veamos cómo lo interpreta el navegador:

    var x = 5;

    (function () {
        var x; // <- la variable es elevada
        console.log("x:", x); //-> undefined
        x = 10;
        console.log("x:", x); //-> 10
    }());
image hoisting

Nota: Recomiendo que lean al artículo completo dedicado a este tema: ¿Qué es hoisting?


Coerción en JavaScript

Se podría decir que la coerción es la acción de forzar a que un objeto se comporte como si fuera de otro tipo. Sin lugar a dudas, éste es uno de los aspectos más interesantes de JavaScript. Al ser un lenguaje débilmente tipado y dinámico, no es necesario especificar el tipo de dato para las variables porque el tipo está asociado al valor, esto hace que en tiempo de ejecución podamos cambiar el valor de las variables sin ningún problema.

JavaScript soporta varias formas de comprobar el tipo de un objeto (duck typing) y una forma de saberlo es a través del operador typeof.

Una de las ventajas de esta característica, es que podemos comprobar la existencia de un objeto mediante su valor, JavaScript se encarga de hacer coerción al tipo boolean en una expresión lógica, por ejemplo:

    var var1, // -> undefined
        var2 = 9;

    if (var1) { } // -> coerción a false
    if (var2) { } // -> coerción a true

    if (!var1) { } // -> coerción undefined a true
    if (!var2) { } // -> coerción 9 a false

    if (!!var1) { } // -> boolean absoluto: false
    if (!!var2) { } // -> boolean absoluto: true

    //asigna 0 si var1 hace coerción a false
    var1 = var1 || 0;

    //boolean absoluto: false
    var value = !!("" || null);

Nótese el uso del operador !(NOT) el cual toma el operando de la derecha y lo fuerza a su representación boolean pero negado. Es decir que si la coerción sobre un objeto lo resuelve como true, entonces el operador !(NOT) lo invierte a false.

De igual forma opera la doble negación !!(DOUBLE NOT), de manera que si el primer !(NOT) invierte el valor boolean, entonces el segundo !(NOT) nuevamente lo invierte para obtener el valor boolean absoluto de un objeto.

Valores Truthy and Falsy

Conocer los valores falsos que resultan de una comparación o de la coerción forzada que hace JavaScript es muy importante y puede ayudarnos a evitar errores de lógica en nuestro código. La siguiente tabla de valores falsy está basada en ECMAScript 5.

valor coerción
false false
0 false
“” false
NaN false
null false
undefined false

Todos los demás valores son verdaderos/truthy, incluyendo los textos "0" y "false" (entre comillas), las funciones, arrays y objetos vacíos.

Cuando JavaScript intenta hacer coerción en un objeto, primero busca si la propiedad valueOf() retorna un valor primitivo, si no puede obtener un valor, entonces intenta obtenerlo del método toString()

    (function (u) { //u = undefined

        var fn = function() {},
            o = { valueOf: function() { return 7 } };

        //coerción en diferentes tipos de objetos
        write("a.", fn);   //true:  function
        write("b.", fn()); //false: undefined
        write("c.", u);    //false: undefined
        write("d.", 0);    //false: number
        write("e.", "");   //false: string
        write("f.", +"");  //false: number
        write("g.", +"x"); //false: number (NaN)
        write("h.", null); //false: object [Null]
        write("i.", []);   //true:  object [Array]
        write("j.", {});   //true:  object [Object]
        write("k.", o);    //true:  object [Object]
        write("l.", +o);   //true:  number

        function pad (val) {
            var len = 10, c = " ";
            return (val + new Array(len).join(c)).slice(0, len);
        }

        function write (item, obj) {
            console.log(item, "typeof:", pad(typeof obj),
                "!!abs:", !!obj, "valor:", obj);
        }
    }());

Nota: Recomiendo que lean el artículo completo dedicado a este tema: ¿Qué es coerción?


Funciones variádicas en JavaScript

Es otra de las características flexibles de este maravilloso lenguaje de programación.
En JavaScript no se necesita ningún truco especial (como la sobrecarga) para hacer que una función reciba un número indeterminado de argumentos, sencillamente, si un argumento no es especificado en el llamado de la función, éste tomará el valor undefined.

Una función variádica es una función de aridad indefinida, es decir, que acepta una cantidad de argumentos variable.

Veamos el siguiente ejemplo:

    function fnMessage (name, message) {
        console.log("\nHola", name);
        console.log(" >", message);
    }
    fnMessage("Jherax"); //1 arg
    fnMessage("David", "JavaScript rules!!"); //2 args
    fnMessage("Coco", "Variadic", "fiction"); //3 args

En el ejemplo anterior vemos que la función fnMessage especifica explícitamente dos argumentos (name, message). La función es llamada 3 veces especificando un número diferente de argumentos, sin embargo en la última llamada nunca capturamos el último argumento, ya que en la definición de la función no fue especificado.

💡 Una solución rápida podría ser el crear una función que especifique la suficiente cantidad de argumentos que podamos llegar a necesitar, y como lo mencionamos anteriormente, no habría problema porque si no se especifica un argumento en el llamado de la función, éste tomará el valor de undefined.😡 Sin embargo ésta solución no es la más adecuada debido a que necesitamos especificar un número determinado de parámetros en la definición de la función, lo cual hace que esta implementación no sea escalable.

🙄 Para solucionar este inconveniente contamos con el objeto especial arguments, el cual es una variable local que disponemos dentro de cualquier función, y contiene todos los argumentos recibidos por la función, empezando con el primer argumento en el índice cero [0].

❗ El objeto arguments no es realmente un Array, es similar a uno (Array-like), ya que contiene una lista de elementos y posee la propiedad length, pero aparte de eso, no posee ninguna otra de las propiedades definidas en el prototipo de Array.

Rescribiendo la función anterior para que reciba múltiples argumentos…
    function fnMessage (name) {
        var i, length = arguments.length;
        console.log("\nHola", name);
        //recorremos los argumentos, excepto el primero
        for (i = 1; i < length; i += 1) {
            console.log(" >", arguments[i]);
        }
    }
    fnMessage("Jherax");
    fnMessage("David", "JavaScript rules!!");
    fnMessage("David", "JavaScript", "SPA");

Nota: Recomiendo que lean el artículo completo dedicado a este tema: ¿Qué son las funciones variádicas?


POO en JavaScript – Prototipos

JavaScript logo
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.


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 (las 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.

Nota: Recomiendo que lean el artículo completo dedicado a este tema: POO en JavaScript – Prototipos y de igual manera, en el artículo Definir propiedades de un objeto podrán encontrar diferentes mecanismos que podemos utilizar en JavaScript para establecer propiedades en un objeto.

Conclusión

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.

Happy coding!

5 thoughts on “¿Sabías esto de JavaScript?

  1. Excelente compilación de lo que en esencia es JavaScript y de sus fortalezas y tips para saber manejar POO con este lenguaje, gracias por el aporte ya que me ha servido para aclarar dudas y comprender mejor como funciones las “clases”.

  2. Excelente post, me servirá de base junto con otras cosas que estoy recopilando para hacer un post sobre Hoisting😀
    Saludos desde Venezuela😉

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