Esto no es lo que parece, el “this” en JavaScript


Leandro's Pool

JavaScript es un lenguaje muy dinámico, tanto que lo podemos utilizar como lenguaje orientado a objetos, como lenguaje funcional y redefinir una función de sistema, todo en la misma línea sin despeinarnos. Cuando estás acostumbrado a lenguajes orientados a objetos “clásicos”, algunas características de JavaScript se te hacen extrañas y pueden traerte más de un dolor de cabeza. Por eso es importante conocer bien el lenguaje en el que estás programando y no sólo algunas librerías útiles.

He visto bastante gente que lleva algún tiempo desarrollando en JavaScript y todavía no tiene claros algunos conceptos básicos del lenguaje, así que aquí va un artículo para intentar aclarar estos puntos; tampoco soy un experto en JavaScript, pero tener claros estos conceptos me ha ayudado mucho durante el desarrollo de algunas aplicaciones de Windows 8 y me ha evitado hacerme un lío con lo que ya sabía de C# y Java. Si vais a desarrollar una aplicación JavaScript moderna es necesario conocer cómo se escribe el JavaScript de hoy.

Las dos características que suelen despistar más al principio son: el ámbito de las variables y la palabra reservada this. Como es necesario comprender el primero para utilizar bien el segundo, vamos por orden.

Ámbito de variables

En JavaScript tenemos dos ámbitos para las variables:

  • El Global, visible desde toda la aplicación. En este contexto deberíamos evitar declarar variables y funciones, pues es muy fácil equivocarse y reescribir su valor, cambiando la funcionalidad de la aplicación por completo, aunque esto también se puede aprovechar a la hora de ofrecer objetos en una API. La mayoría de frameworks declaran dentro del ámbito global algunos objetos básicos, pero, para evitar conflictos con otras librerías, suelen utilizar una técnica de espacios de nombres.
  • El  Local es a nivel de función, todas las variables que se declaran dentro de una función no son visibles fuera de la misma, pero si son visibles a las funciones que declaremos internamente. Este pequeño truco nos vendrá muy bien para poder utilizar el concepto de closures.

Sin profundizar demasiado, hagamos un repaso de los efectos del ámbito en las variables. En el siguiente fragmento de código, vemos como la variable a es definida en el ámbito global y desde la función b podemos cambiar su valor.

var a=0;
function b(){
    a=5;
}

b();
log("ejecución b: "+a);

El resultado tras ejecutar b() será 5, hemos modificado el valor de la variable global. De hecho podríamos cambiar cualquier cosa de la variable, incluso asignarle el no-valor undefined. No probéis esto sin que os acompañe un adulto ;).

Si definimos una variable dentro de una función sólo es visible dentro de la misma y no entra en conflicto con las definiciones globales:

function c(){
    var a=10;
}
c();
log("ejecución c: "+a);

En este punto (supongamos que va a continuación del código anterior), el valor global de a no habrá cambiado y seguiremos teniendo el valor 5.

Las variables locales a una función se pueden declarar en casi cualquier lugar, por ejemplo dentro de un bloque if, dentro de un bucle, dentro de un bloque de llaves; el siguiente bloque de código nos muestra cómo el código tiene acceso a una variable definida dentro de un bucle, JavaScript no nos pondrá ningún problema, ni siquiera con la clausula “use strict”:

for(var i=0;i<50;i++){
    var x=i;
}
log("valor de x tras el bucle: "+x);

El resultado será:

valor de x tras el bucle: 49

Cuando tenemos una variable con ámbito de función también es visible a las funciones definidas dentro de la misma. Así:

function d(valor){
    function cuadrado(){
        return valor*valor;
    }
    return cuadrado();
}

log("ejecución d:"+d(a));

La función que definimos dentro de d no necesita que le enviemos el parámetro, pues puede acceder al mismo directamente.

Podemos tener efectos secundarios que no voy a explicar aquí, pero seguro que os parece interesante el artículo de Robert Nyman.

Debido a todo esto y con el objetivo de mantener el ámbito global lo más limpio posible, habréis visto en más de una ocasión la siguiente estructura o una de sus variantes:

(function(){
 var miVariable="incalculable";
 function miFuncion(){
  console.log("Mi variable tiene un valor " + miVariable);
 }
 miFuncion();
})();

Este código declara una función y dentro de la misma declara una variable, una función y llama a ésta última. La parte interesante de esta función es que está declarada entre paréntesis y tiene otro par de paréntesis al final.
Los primeros paréntesis convierten a la función en una expresión, al ser una expresión no crea un nombre de función, aunque lo pongamos, en el ámbito global y lo único que podemos hacer con la misma es ejecutarla, o asignarla a una variable para poder ejecutarla más tarde.
Los segundos paréntesis hacen esto mismo, ejecutar la expresión. Como la expresión es una función, todas las variables declaradas dentro se quedarán en el ámbito de la función y así evitamos que cualquier otro código pueda entrar en conflicto con el nuestro.
 

La palabra reservada this y el ámbito de ejecución

Cuando hablamos de orientación a objetos, habitualmente tenemos una manera de acceder a la instancia propietaria de la función que se está ejecutando con alguna clave como this o self. Es por eso que cuando vemos que JavaScript tiene la palabra clave this pensamos que lo tenemos todo controlado y nos creamos un objeto que utiliza this para todo:

function objeto(v){
    this.valor=v;
    this.desplaza=function(cantidad){
                log(this.valor+" + "+cantidad+" = "+  (this.valor+cantidad));
                log("");
            }
}

Después, llamamos a nuestra función desplaza perteneciente al objeto y parece que funciona…

var o=new objeto(5);
//llama a la función definida en el objeto
log("o.desplaza(10) >>>");
o.desplaza(10);

Y el resultado es:

o.desplaza(10) >>>
5 + 10 = 15

Como sabréis, acceder a propiedades de objetos en JavaScript es considerablemente más lento que el acceso a una variable, así que un día decidimos optimizar nuestro código guardando la llamada en una variable. Al cabo de unas horas de depuración nos volvemos a dar cuenta que JavaScript, además de orientado a objetos, es un lenguaje dinámico y funcional:

//llama a la función definida en el objeto, pero cambia el contexto al asignar
//la función a una variable
log("var desp=objeto.desplaza >>>");
var desp=objeto.desplaza;
desp(5);

El resultado puede ser mucho más alucinante si alguien ha metido algo con el mismo nombre dentro del objeto global (window):

//llama a la función definida del objeto, pero utiliza el objeto global como this
log("window.valor=100 >>>");
window.valor=100;
var desp=o.desplaza;
desp(5);

¿Qué ha pasado aquí? Al colocar la función en una variable hemos cambiado el ámbito de la llamada y ahora se ejecuta en el global (window en un navegador). Para evitar esto, habitualmente se utiliza el concepto de closure para empaquetar el this y que sea visible al código que se ejecuta.

//redefinición del objeto, guardando el this...
function objetoMejor(v){
    var that=this;
    this.valor=v;
    this.desplaza=function(cantidad){
                log(that.valor+" + "+cantidad+" = "+  (that.valor+cantidad));
                log("");
            }
}
var o2=new objetoMejor(5);
//llama a la función definida en el objeto
log("o2.desplaza(10) >>>");
o2.desplaza(10);
log("var desp=o2.desplaza >>>");
var desp2=o2.desplaza;
desp2(10);

Pero este método nos obliga a definir todo el objeto en el constructor; en ocasiones eso no nos vendrá bien, pero no todo está perdido.

Dominando el ámbito de this con call, apply y bind

Además del método pedestre that=this, JavaScript cuenta con tres funciones que nos ayudarán a dominar el ámbito de ejecución como nosotros queramos. Las llamadas call,apply y bind.

Las dos primeras funciones nos servirán para inyectar el ámbito en la llamada que nosotros queramos.

//llama a la función definida en el objeto, pero cambia el valor de this
log("o.desplaza.apply({valor:10},[5]) >>>");
o.desplaza.apply({valor:10},[5]);
log("o.desplaza.call({valor:10}5) >>>");
o.desplaza.call({valor:10},5);

En ambos casos, el primer parámetro es el objeto que queremos que haga de this en la llamada, podemos poner el propio objeto (o) o bien inyectarle el que nosotros queramos, en el ejemplo un nuevo objeto anónimo con un valor. La única diferencia entre las dos funciones es que la primera recibe un array como parámetro, la segunda utiliza todos los parámetros que le pasemos a partir del segundo.

Estas funciones nos pueden ser útiles, pero la que más me gusta es la función bind, que en lugar de ejecutar la llamada, nos devuelve una nueva función pero con el valor que nosotros le pasamos como this.

function objetoMejorMasClaro(v){
    this.valor=v;
    this.desplaza=function(cantidad){
                log(this.valor+" + "+cantidad+" = "+  (this.valor+cantidad));
                log("");
            }.bind(this);
}
var o3=new objetoMejorMasClaro(5);
log("o3.desplaza>>>");
o3.desplaza(10);
var d3=o3.desplaza;
d3(10);

Como he dicho en el párrafo anterior, bind utiliza el valor. Esto lo hace muy diferente de una closure pues en esta última se toma la variable. Observad la diferencia de resultados:

var fclosure, fbind;
(function(){
    var x=5;
    fclosure=function (){
        return x*x;
    }
    
    function cuadrado(){
        return this*this;
    }
    fbind=cuadrado.bind(x);
    
    x=3;
    
})();

log("cuadrado closure="+fclosure());
log("cuadrado bind="+fbind());

Los resultados son:

cuadrado closure=9
cuadrado bind=25

Así que tened cuidado con el bind!

El bind también nos será muy útil cuando definimos funciones de objetos, funciones dentro de prototype y cuando enviemos funciones a parámetros de callback, como en la ejecución de un setTimeout:

log("setTimeout sin bind y con bind >>>");
setTimeout(o.desplaza,500,5);
setTimeout(o.desplaza.bind(o),600,15);

Espero que os haya servido para aclarar algunos conceptos. Como lo mejor para conocerlos bien es jugar con el código, aquí os dejo un jsfiddle: http://jsfiddle.net/jmservera/WNWaM/

¡Disfrutad!

Anuncios

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s