La palabra clave this en JavaScript

viernes, 27 de junio de 2014

Logo JavaScript

La palabra clave this es diferente respecto a otros lenguajes como puede ser C# o Java. En estos lenguages this hace referencia al objeto que contiene el método o función desde donde accedemos a this y siempre tiene la misma identidad.

En javascript es un poco más complejo el significado de this, y a los desarrolladores como yo que venimos de lenguajes orientados a objetos, es normal que tengamos momentos frustrantes de no saber que esta pasando.
En JavaScript la identidad de this depende del objeto propietario de la función desde donde estamos accediendo a this, pero si no accedemos desde una función o esta no pertenece a un objeto, entonces estamos en contexto global y this hace referencia, en caso de un navegador web, al objeto window.

Vamos a ver más en detalle cada caso.

Contexto Global

Hemos visto que accediendo a this desde el contexto global, el propietario de this va ser el objeto window, este es un ejemplo sencillo de acceder a this desde contexto global:
alert(this.outerWidth);

en este ejemplo claramente this hace referencia a window y nos va a devolver el ancho de la ventana.

Este caso sin embargo, es fácil que los que venimos de lenguajes orientados a objetos, nos resulte extraño.
var obj = {
prop1: "xurxo",
prop2: "developer",
fullprop: this.prop1 + " " + this.prop2
}
alert(obj.fullprop);

Definimos un objeto con notación literal y para obtener el valor de una propiedad que se calcula con otras dos del mismo objeto, estamos usando this, pero como no estamos consultando this dentro de una función que pertenezca a un objeto, this hace referencia a window que no tiene una propiedad prop1 y prop2, con lo que el resultado que se muestra en el mensaje sera 'undefined undefined'.

Para poder acceder a this y que haga referencia a nuestro objeto la propiedad fullprop debe ser una función.

Contexto de Función

Notación literal

Para conseguir que this haga referencia al objeto dentro del cual consultamos this, hemos visto que tenemos que hacer que nuestra propiedad desde donde accedemos a this sea una función.
var obj = {
prop1: "xurxo",
prop2: "developer",
fullprop: function(){
 return this.prop1 + " " + this.prop2;   
}
}
alert(obj.fullprop());

Ahora si que nos va a devolver el resultado de la función el valor esperado.

Pero no importa donde se defina la función, sino que pertenezca al objeto. En el ejemplo anterior podríamos definirnos la función fuera de la definición del objeto con notación literal y añadirsela después, cómo la función pertenede al objeto obj, this hace referencia a obj.
var obj = {
prop1: "xurxo",
prop2: "developer"
}

function GetFull(){
 return this.prop1 + " " + this.prop2;   
}

obj.fullprop = GetFull;

alert(obj.fullprop());

Sin embargo si invocamos a la función directamente sin hacerlo a través del objeto, volvemos a estar en contexto global porque la función no pertenece a un objeto y nos devolverá de nuevo 'undefined undefined' porque this hace referencia al objeto window y este no tiene esas propiedades.

var obj = {
prop1: "xurxo",
prop2: "developer"
}

function GetFull(){
 return this.prop1 + " " + this.prop2   
}

obj.fullprop = GetFull;

alert(GetFull());

Función Constructora

Si definimos el objeto con una función constructora es imprescindible que indiquemos la palabra clave new para crearnos un objeto nuevo, de lo contrario estaríamos utilizando this haciendo referencia a window.
function Person(p1,p2){
 this.prop1 = p1;
 this.prop2 = p2;
 this.fullprop = this.prop1 + " " + this.prop2;   
}

var xurxo = new Person("xurxo","developer");

alert(xurxo.fullprop());

Este ejemplo se comporta de la forma esperada que es crear un nuevo objeto person con 3 con sus propedades, sin embargo, si no indicamos el new.
function Person(p1,p2){
 this.prop1 = p1;
 this.prop2 = p2;
 this.fullprop = this.prop1 + " " + this.prop2;   
}

var xurxo = Person("xurxo","developer");

alert(xurxo.fullprop());

Realmente solo tenemos una función que crea unas propiedades en el objeto window, como la función Person no devuelve nada xurxo es undefined  y nos va a dar un error porque no puede encontrar fullprop en undefined.

Jerarquia de objetos

Si tenemos un objeto padre y dentro de un hijo tenemos una función, dentro de esta función this va a apuntar al hijo. Siempre apunta al objeto más inmediato.
var obj = {
name: "padre",
child: {
 name: "hijo",
 ShowName: function(){
  alert(this.name);
 }
}
}

obj.child.ShowName();

en este ejemplo el mensaje nos mostrará que this es el hijo.

Función dentro de una función

En caso de que tengamos una función dentro de una función

var obj = {
prop1: "xurxo",
prop2: "developer",
fullprop: function(){
 var nestedFunc = function(){
 return this.prop1 + " " + this.prop2;   
 }  

 return nestedFunc();
  }
}
alert(obj.fullprop());

this dentro de la función más interna esta en contexto global porque no esta contenida dentro de un objeto y apunta a window, en este caso volvemos a obtener undefined undefined. Para solucionarlo en la función más externa podemos almacenar una copia de this para que se utilice dentro de la función más interna.
var obj = {
prop1: "xurxo",
prop2: "developer",
fullprop: function(){
 self = this;

        var nestedFunc = function(){
            return self.prop1 + " " + self.prop2;   
 }  

 return nestedFunc();
  }
}
alert(obj.fullprop());

call y apply

Existe la posibilidad de modificar la identidad de this en tiempo de ejecución mediante las funciones Call y Apply. Son funciones que se pueden utilizar para invocar una función, son similares y lo que cambia es como se invocan . El primer parámetro, en ambas funciones, es el objeto a usar como this , si usamos call los parámetros posteriores se pasan como argumentos y si usamos apply los parámetros de la función se pasan contenidos dentro de un array.

function Order(price){
 this.price = price;
}

var order1 = new Order(400);
var order2 = new Order(1000);
 
function CalculateTotal(percntageTax,shippingCost){
  this.totalPrice = this.price + (this.price * (percntageTax/100)) + shippingCost;
}

CalculateTotal.call(order1, 15,5 );
CalculateTotal.apply(order2, [15,0]);

alert(order1.totalPrice);
alert(order2.totalPrice);

bind

El objetivo de bind sirve también para  indicar en tiempo de ejecución el objeto this a una función.

Mediante Bind se crea una nueva función basada en una original, la nueva función queda enlazada de forma que toma el cuerpo de la original y se le debe pasar por parámetro objeto a usar como this más los parámetros que requiere la función original.

function Order(price){
 this.price = price;
}

var order1 = new Order(400);
 
function CalculateTotal(percentageTax,shippingCost){
  this.totalPrice = this.price + (this.price * (percntageTax/100)) + shippingCost;
}

var bindedCalculateTotal = CalculateTotal.bind(order1,15,0);

bindedCalculateTotal();

alert(order1.totalPrice);

La principal diferencia entre call/apply y bind es que las dos primeras ejecutan en el momento una función, la segunda no ejecuta la función sino que devuelve una nueva basada en la original.

Resumen

Como hemos podido comprobar this en JavaScript tiene un significado bastante más complejo que en lenguajes orientados a objetos como pueden ser Java o C#.

Hemos visto los casos donde this hace referencia un objeto creado por nosotros y cuando hace referencia al objeto window.

Por último tambien hemos repasado funciones como call, apply y bind donde podemos cambiar el significado de this en tiempo de ejecución.

Libros Relacionados

JavaScript & jQuery: The Missing Manual

JavaScript & JQuery: Interactive Front-End Web Development

Javascript: the Good Parts

No hay comentarios:

Publicar un comentario