¡Hola! Me parece que andas usando un bloqueador de anuncios =( ¿Nos desbloqueas? ¿Por qué?
F13

Funcion 13

Comprendiendo las variables, objetos, funciones, alcance y prototype en JavaScript

Hasta hace muy poco tiempo, JavaScript era un lenguaje muy poco valorado. No hace muchos años que en el instituto nos “enseñaron” JavaScript en programación con bondades como métodos onClick que aun a dia de hoy persisten en algunas páginas y lo que es aun peor, en la mente de algunos programadores. Pero esto ha empezado a cambiar, la versión V8 del Engine JavaScript creada por Google y grácilmente usada en Google Chrome, demuestra que JavaScript aun tien emucho que decir y es un lenguaje que aunque aun no domino tanto como PHP, comienzo a estar en el grado de nivel medio.

Pero, reconozcámoslo, JavaScript es un lenguaje un poco dificil de comprender, repleto de malas y buenas partes y, seguramente, esto ocurre por su mediocre diseño. Cuando llegamos a partes quizá más avanzadas de jQuery o programación en Node.js por ejemplo, nos vemos obligados a usar el lenguaje en su nivel más puro, y esto a veces se convierte en un obstáculo.

Variables y objetos

var usuario = 'Spidercerdo', color = 'Rosa';  

Fácil, ¿no?. Con la palabra clave var hemos creado un par de variables. Pasemos al siguiente nivel:

var cerdo = {nombre: 'Spidercerdo', color: 'Rosa'};  

Ahora, estamos creando una variable, cerdo. La variable contiene el nombre y el color en una asociación típica de clave-valor. Pronto descubrirás que todos los objetos en JavaScript funcionan de la misma forma, como si fueran un diccionario.

Acceder a los datos es muy sencillo:

console.log(cerdo.nombre);  
console.log(cerdo.color);  

Pero… ¿y si ponemos esto?

console.log(cerdo['nombre']);  
console.log(cerdo['color']);  

Veremos que también funciona. Esto empieza a ponerse interesante y quizá comencéis a vislumbrar el potencial de este enfoque, que pronto exploraremos.

Pasemos, por ahora, al siguiente nivel.

Funciones

var decirLema = function(){  
    alert('SpiderCerdo... SpiderCerdo... Hace lo que un... SpiderCerdo hace...');
};

Lo primero que podemos observar, es que se parece mucho a la forma en la que definimos un objeto (salvando las diferencias) y es que, realmente, es así. Las funciones en JavaScript son objetos con un superpoder. Pueden ser… ¡invocados!

Antes de avanzar, me gustaría aclarar algo que a mi al menos me creaba mucha confusión:

//¿Cuál es la diferencia?
var decirLema = function(){  
    alert('SpiderCerdo... SpiderCerdo... Hace lo que un... SpiderCerdo hace...');
};

function decirLema2 () {  
    alert('SpiderCerdo... SpiderCerdo... Hace lo que un... SpiderCerdo hace...');
}

La principal diferencia es que mientras que la primera se define en tiempo de ejecución, la segunda es accesible sin importar donde se coloque. Así, el siguiente código tendría el siguiente resultado:

decirLema(); // Error -> Property 'decirLema' of object [object DOMWindow] is not a function  
decirLema2(); // ¡Exito!  
var decirLema = function(){  
    alert('SpiderCerdo... SpiderCerdo... Hace lo que un... SpiderCerdo hace...');
};
function decirLema2 () {  
    alert('SpiderCerdo... SpiderCerdo... Hace lo que un... SpiderCerdo hace...');
}

No obstante, no es la única diferencia. Hablando con propiedad, decirLema es una variable que tiene una función anónima asignada, mientras que decirLema2 es una función con su nombre. Si llamamos al método .toString() sobre las funciones veremos rápidamente la diferencia. A veces, es útil conocer el nombre de una función.

Pero, volviendo al tema:

var presentarse = function (nombre, color){  
    return '¡Hola! Soy ' + nombre + ' y soy de color ' + color + '.';
}

Avanzamos un paso. Recibimos argumentos y devolvemos un valor. Aunque podríamos escribir esto:

var presentarse = function (cerdo){  
    return '¡Hola! Soy ' + cerdo.nombre + ' y soy de color ' + cerdo.color + '.';
}

Para llamarlo solo tendríamos que hacer algo como presentarse(cerdo) para invocarla. Pero si nos tomamos en serio lo que dije antes de que una función es en sí un objeto, ¿podríamos hacer esto?:

cerdo.saludar = presentarse;  

De esta forma, cerdo.saludar(cerdo) debería darnos el mismo resultado que antes. Bien. Pero, ¿no os parece que estamos escribiendo cerdo demasiadas veces? ¿Por qué le pasamos a un objeto el mismo objeto? Vamos a cambiar nuestra función para ver si podemos hacer algo al respecto:

var presentarse = function (){  
    return '¡Hola! Soy ' + this.nombre + ' y soy de color ' + this.color + '.';
}

Si la ejecutáramos sin más, tendríamos un error y aquí es donde vemos el alcance (scope). El alcance es el entorno en que la función se ejecuta. La función solo conoce su mundo y el mundo de su “padre”. En JavaScript, el alcance por defecto sin hace nada es el objeto Window, que representa la ventana/pestaña en la que se encuentra la página. Es por esto que recibimos el error: this.nombre se refiere a la propiedad del objeto Window de la actual ventana, pero seguramente esta no tenga esa propiedad, ni tampoco la de color.

No obstante, esto nos dará el resultado que esperábamos:

cerdo.saludar = presentarse;  
cerdo.saludar();  

Ahora, cuando se ejecuta la función saludar, la ejecuta en el entorno de cerdo por lo que usará las propiedades de ese objeto. Puede que te estés preguntando, ¿qué pasa si no quiero pasarle esa función como propiedad del objeto? Aunque no sé porqué querrías hacer algo así, puedes escribir presentarse.call(cerdo); que ejecutará la función presentarse usando como contexto a cerdo. Los métodos call y apply son muy útiles con código que cambia habitualmente de alcance. Espero poder ampliar este tema en el futuro.

Sigamos avanzando. Observa esta porción de código:

var cerdo1 = {nombre: 'Porky', lema: '¡Eso es todo amigos!'};  
var cerdo2 = {nombre: 'Spider Cerdo', lema: 'SpiderCerdo... SpiderCerdo... Hace lo que un... SpiderCerdo hace...!'};

cerdo1.saludar = function() {  
    return 'Hola, soy ' + this.name + ' - ' + this.lema + '.';
}
cerdo2.saludar = cerdo1.saludar;  

Si ejecutamos cerdo1.saludar(), recibiremos el mensaje espedado. Pero… ¿qué pasa si ejecutamos cerdo2.saludar()?

Algo muy importante a recordar sobre el alcance es que siempre se define en tiempo de ejecución. Esto significa que el alcance de una función es siempre el contexto en que se ejecuta, no el contexto en que se definió. Por tanto, la función cerdo2.saludar() va a devolvernos el resultado esperado. La función se definió en el contexto del objeto Window por lo que no hay ningún alcance explícito.

Vamos a crear una sencilla calculadora con lo que hemos aprendido:

var sumar = function(x,y){ return x + y };  
var restar = function(x,y){ return x - y };

var operaciones = {  
    '+': sumar,
    '-': restar
};

var calcular = function(x, y, operacion){  
    return operaciones[operacion](x, y);
};

La forma de usarlo sería por ejemplo algo así:

calcular(42, 23, '-');  
calcular(16, 15, '+');  
calcular(8, 4, '+');  

¿Fácil verdad? Vamos a complicarlo un poco y a darle orientación a objetos. La mayoría de lenguajes orientados a objetos, usan el concepto de herencia pero JavaScript no entra en la mayoría y usa prototipos (prototypes). No hay constructores de clases pero no os alarméis, ¡sigue siendo potente!

Objetos

Veamos cómo funcionan las cosas:

var Problema = function (x, y){  
    this.x = x;
    this.y = y;
}

var problema1 = new Problema(4,8);  

Tenemos una función que acepta dos parámetros, x e y y los almacena. La palabra clave new es importante ya que le dice al intérprete que estás usando Problema no como una función, preo como un constructor. Si lo quitáramos, se ejecutaría la función Problema que no devuelve nada, por lo que problema1 no tendría valor alguno. La palabra new nos da una copia del objeto despues de ser ejecutado, dejando intacto al original (por lo que podemos usarlo para hacer más objetos de ese tipo).

Problema no deja de ser una función normal y funcional. Tan solo ponemos la ‘P‘ en mayúscula como convención para que no nos olvidemos de usar la palabra new.

Teniendo en cuenta que las funciones son objetos, podemos hacer esto:

console.log(problema1.x);  
console.log(problema1.y);  

Pero nosotros queremos que nuestro problema sea capaz de autoresolverse. Demos el siguiente paso:

Problema.prototype.operaciones = {  
    '+': function(x,y){ return x + y; },
    '-': function(x,y){ return x - y; }
};
Problem.prototype.calcular = function(operacion){  
    return this.operations[operacion](this.x, this.y);
};
problem1.calcular('+');  
problem1.calcular('-');  

Bien, este es nuestro primer vistazo a prototype en Javascript. Aunque los prototipos son una de las características más avanzadas del lenguaje, es posible escribir JavaScript durante mucho año y no tener ningún motivo para usarlos aunque, sin duda, comprenderlos te abrirá muchas puertas en el conocimiento.

Antes de continuar hablando sobre prototypes, quiero que os planteéis esto. ¿Qué diferencia hay entre esto…

var Problema = function() {  
    this.calcular = function() {}; 
}

var p1 = new Problema();  
var p2 = new Problema();  

… y esto ?

Problema.prototype.calcular = function() {} var p1 = new Problema(); var p2 = new Problema();  

Básicamente, en nuestro primer trozo de código, p1 y p2 son funciones diferentes y consumirán más memoria que en el caso en que usemos prototype ya que básicamente son objetos idénticos, con acceso a esas funciones.

En JS, todos los objetos tienen una propiedad llamada __proto__. Si no me creeis, escribid lo siguiente en la consola de Google Chrome:

var incredulo = {};  
console.log(incredulo);  

Esa propiedad se refiere a otro objeto el cual se le considera el prototipo para ese objeto, algo así como el “padre” u “objeto original” desde el que el objeto se creó. Este enlace es realmente útil porque significa que todos los objetos tienen todas las propiedades y métodos de su prototipo.

El modelo de prototipos en JS funciona de manera similar el modelo de clases en otros lenguajes, en el sentido de que cuando un método no se encuentra en la definición de la clase de un objeto, se revisa su superclase, luego la superclase de la superclase y así sucesivamente. Aquí ocurre exactamente lo mismo – primero se revisa el objeto. Si el método o propiedad no se encuentra, se revisa su propiedad __proto__, luego se revisa la propiedad __proto__ de su prototipo y así sucesivamente, hasta que el intérprete alcance el último objeto de la cadena en el que __proto__ será null.

Pero seguramente te habrás dado cuenta de que no hemos tocado a __proto__ en nuestro código. Esto es porque realmente no tiene sentido alguno y JS permite usar el objeto prototypo en la función que usa como constructor (Problema en nuestro caso) para especificar el comportamiento de los prototipos de los objetos construidos. Automáticamente establece la propiedad __proto__ de los objetos construídos a Problema.prototype por ti.

Ahora te resultará más fácil ver que llamar a problema1.calcular() primero busca en problema1. Dado que no encuentra el método, busca en su __proto__, que es el mismo que Problema.prototype, y encuentra el método. Luego ejecuta el método calcular en el ámbito de problema1. Así es cómo se puede definir el método tan solo en el prototipo y hacer que funcione correctamente en todos los objetos que tienen ese prototipo.

Tened en cuenta que la resolución prototipal, como la mayoría de las cosas en este lenguaje, ocurre en tiempo de ejecución. Añadimos métodos a Problema.prototype después de que tanto problema1 como problema2, sean creados y no tenemos problemas al usar estos métodos en los objetos creados. El intérprete resuelve las llamadas a la función en el momento en que las llamas.

Vamos a añadir un método final a nuestro Problema:

Problem.prototype.nuevoMensaje = function(){  
    var self = this;
    var formateador = function(){
        return 'Valores: ' + self.x + ' y ' + self.y;
    };

    return function(inicio, fin){
        return '' + inicio + formateador() + fin;
    };
}
var nuevoMensaje = problema1.nuevoMensaje();  
// Hacer cálculos supercomplejos
var mensaje = nuevoMensaje('Este era el problema: ', 'Gracias por todo');  
alert(mensaje);  

Este es un ejemplo peculiar, pero nos sirve para ilustrar algo muy importante en JavaScript – el concepto de closure. Aquí, problema1.nuevoMensaje() devuelve otra función, la cual puedes llamar a placer.

La parte importante es la función formateador. Sabemos que está siendo definida en la función nuevoMensaje, pero esta se ejecuta y acaba mucho antes de que se use la función. ¿Qué le ocurre entonces a formateador? ¿No se supone que se habrá esfumado?

La respuesta es, que la función devuelta por nuevoMensaje se cierra sobre la variable formateador. Mantiene el valor de formateador mucho después de que todo haya ocurrido, incluyendo la función que originalmente la definió.

El truco de la variable self lo usamos porque no podemos usar this en este caso ya que no tenemos ni idea del alcance en que nuevoMensaje se va a ejecutar. Por ello, asignamos this a alguna variable (usamos self) y lo usamos en la función que vamos a devolver. De esta forma, nos asguramos de que se van a usar los datos correctos siempre, independientemente de qué alcance tenga la función.

Este método conocido como closure es especialmente útil en Ajax. Lo creas o no, descubrirás que a menudo pasas una función para que se ejecute después de que se haga una llamada concreta al servidor. Si necesitas que la función tenga acceso a algún tipo de dato especial que no quieres facilitar directamente, tendrás que usar closures.

Este artículo es un pequeño inicio hacia el JavaScript avanzado y, aunque aun queda mucho camino por recorrer, probar nuevas cosas y leer buen código nos lleva por el buen camino.

Fuente

Este artículo bebe casi al completo, de este otro artículo en inglés de @runway_7, aunque creo haberlo completado con algunas cosas que investigué por mi propia cuenta. El día en que leí este artículo que me abrió los ojos en JavaScript.

Programador Front-end en First + Third y Potato. Trabajando con JavaScript y HTML5 desde el corazón de Sevilla.

Comentarios ¡Únete a la conversación!