4. JavaScript avanzado
4.1. Closures
Ya vimos en la primera sesión que JavaScript permite definir funciones dentro de otras funciones.
Una función interna tiene acceso a sus parámetros y variables, y también puede acceder a los parámetros y variables de la función a la que está anidada (la externa). La función interna contiene un enlace al contexto exterior, el cual se conoce como closure, y es la fuente de un enorme poder expresivo.
Los closures son funciones que manejan variables independientes. En otras palabras, la función definida en el closure "recuerda" el entorno en el que se ha creado.
Veamos con un ejemplo un closure en acción:
function inicia() {
var nombre = "Batman"; (1)
function muestraNombre() { (2)
console.log(nombre); (3)
}
muestraNombre();
}
inicia();
1 | nombre es una variable local |
2 | muestraNombre() es una función interna (closure) |
3 | dentro del closure usamos una variable de la función externa |
La función interna muestraNombre()
sólo está disponible en el cuerpo de la función inicia()
, y en vez de tener una variable propia, lo que hace es reutilizar la variable nombre
declarada en la función externa. Tal como vimos en [_alcance], el ámbito de una variable se define por su ubicación dentro del código fuente y las funciones anidadas tienen acceso a las variables declaradas en su ámbito externo.
El siguiente paso es refactorizar el ejemplo para darle una vuelta de tuerca:
function creaFunc() {
var nombre = "Batman";
function muestraNombre() {
console.log(nombre);
}
return muestraNombre;
}
var miFunc = creaFunc();
miFunc();
El ejemplo sigue haciendo lo mismo, aunque ahora la función externa nos ha devuelto la función interna muestraNombre()
antes de ejecutarla.
En principio, podemos dudar de que este código funcionase. Normalmente, las variables locales dentro de una función sólo existen mientras dura la ejecución de dicha función. Una vez que creaFunc()
haya terminado de ejecutarse, es razonable suponer que no se pueda ya acceder a la variable nombre
. La solución a este rompecabezas es que miFunc
se ha convertido en un closure que incorpora tanto la función muestraNombre
como la cadena "Batman"
que existían cuando se creó el closure. Así pues, hemos creado un closure haciendo que la función padre devuelva una función interna.
Un closure es un tipo especial de objeto que combina dos cosas: una función, y el entorno en que se creó esa función. El entorno está formado por las variables locales que estaban dentro del alcance en el momento que se creó el closure.
Veamos otro ejemplo donde la función externa devuelve una función interna que recibe parámetros. Para ello, mediante una factoría de función, crearemos una función que permite sumar un valor específico a su argumento:
function creaSumador(x) { (1)
return function(y) { (2)
return x + y;
};
}
var suma5 = creaSumador(5);
var suma10 = creaSumador(10);
console.log(suma5(2)); // 7
console.log(suma10(2)); // 12
1 | función creaSumador(x) que toma un argumento único x y devuelve una nueva función. |
2 | La función devuelta recibe un único argumento y devuelve la suma del argumento y y el valor de x de la función externa. |
Tanto suma5
como suma10
son closures: comparten la misma definición de cuerpo de función, pero almacenan diferentes entornos. En el entorno de suma5
, x
vale 5
, mientras que para suma10
, x
vale 10
.
4.1.1. Alcance en closures
Para entender mejor los closures, veamos como funciona el alcance. Ya hemos visto que las funciones internas puede tener sus propias variables, cuyo alcance se restringe a la propia función:
function funcExterna() {
function funcInterna() {
var varInterna = 0;
varInterna++;
console.log('varInterna = ' + varInterna);
}
return funcInterna;
}
var ref = funcExterna();
ref();
ref();
var ref2 = funcExterna();
ref2();
ref2();
Cada vez que se llama a la función interna, ya sea mediante una referencia o de cualquier otro modo, se crea una nueva variable varInterna
, la cual se incremente y se muestra:
varInterna = 1
varInterna = 1
varInterna = 1
varInterna = 1
Las funciones internas pueden referenciar a las variables globales del mismo modo que cualquier otro tipo de función:
var varGlobal = 0;
function funcExterna() {
function funcInterna() {
varGlobal++;
console.log('varGlobal = ' + varGlobal);
}
return funcInterna;
}
var ref = funcExterna();
ref();
ref();
var ref2 = funcExterna();
ref2();
ref2();
Ahora nuestra función incrementará de manera consistente la variable global con cada llamada:
varGlobal = 1
varGlobal = 2
varGlobal = 3
varGlobal = 4
Pero ¿qué ocurre si la variable es local a la función externa? Como la función interna hereda el alcance del padre, también podemos referenciar a dicha variable:
function funcExterna() {
var varExterna = 0;
function funcInterna() {
varExterna++;
console.log('varExterna = ' + varExterna);
}
return funcInterna;
}
var ref = funcExterna();
ref();
ref();
var ref2 = funcExterna();
ref2();
ref2();
Ahora nuestra función tiene un comportamiento más interesante:
varExterna = 1
varExterna = 2
varExterna = 1
varExterna = 2
Ahora hemos mezclado los dos efectos anteriores. Las llamadas a funcInterna()
mediante referencias distintas incrementan varExterna
de manera independiente. Podemos observar como la segunda llamada a funcExterna()
no limpia el valor de varExterna
, sino que ha creado una nueva instancia de varExterna
asociada al alcance de la segunda llamada. Si volviesemos a llamar a ref()
imprimiría el valor 3
, y si posteriormente llamamos a ref2()
también imprimiría 3
, ya que los dos contadores están separados.
En resumen, cuando una referencia a una función interna utiliza un elemento que se encuentra fuera del alcance en el que se definió la función, se crea un closure en dicha función. Estos elementos externos que no son parámetros ni variables locales a la función interna las encierra el entorno de la función externa, lo que hace que esta variable externa permanezca atada a la función interna. Cuando la función interna finaliza, la memoria no se libera, ya que todavía la necesita el closure.
4.1.2. Interacciones entre closures
Cuando tenemos más de una función interna, los closures pueden tener comportamientos inesperados. Supongamos que tenemos dos funciones para incrementar el contador:
function funcExterna() {
var varExterna = 0;
function funcInterna1() {
varExterna++;
console.log('(1) varExterna = ' + varExterna);
}
function funcInterna2() {
varExterna += 2;
console.log('(2) varExterna = ' + varExterna);
}
return {'func1': funcInterna1, 'func2': funcInterna2};
}
var ref = funcExterna();
ref.func1();
ref.func2();
ref.func1();
var ref2 = funcExterna();
ref2.func1();
ref2.func2();
ref2.func1();
Devolvemos referencias a ambas funciones mediante un objeto (de manera similar al ejemplo del Sumador) para poder llamar a dichas funciones:
(1) varExterna = 1
(2) varExterna = 3
(1) varExterna = 4
(1) varExterna = 1
(2) varExterna = 3
(1) varExterna = 4
Las dos funciones internas referencian a la misma variable local, con lo que comparten el mismo entorno de closure. Cuando funcInterna1()
incrementa varExterna
en 1, la llamada posterior a funcInterna2()
parte de dicho valor con lo que el resultado de incrementar en dos unidades es 3. De manera similar a antes, al crear una nueva instancia de funcExterna()
se crea un nuevo closure con su respectivo nuevo entorno.
4.1.3. Uso de closures
¿Son los closures realmente útiles? Vamos a considerar sus implicaciones prácticas. Un closure permite asociar algunos datos (el entorno) con una función que opera sobre esos datos. Esto tiene evidentes paralelismos con la programación orientada a objetos, en la que los objetos nos permiten asociar algunos datos (las propiedades del objeto) con uno o más métodos.
Por lo tanto, podemos utilizar un closure en cualquier lugar en el que normalmente usaríamos un objeto con sólo un método.
En la web hay situaciones habituales en las que aplicarlos. Gran parte del código JavaScript para web está basado en eventos: definimos un comportamiento y lo conectamos a un evento que se activa con una acción del usuario (como un click o pulsación de una tecla). Nuestro código generalmente se adjunta como un callback.
Cuando veamos como podemos trabajar con las hojas de estilo mediante JavaScript haremos uso de un closure.
4.1.4. Métodos privados mediante closures
Java permite declarar métodos privados (solamente accesibles por otros métodos en la misma clase). En cambio, JavaScript no proporciona una forma nativa de hacer esto, pero es posible emularlos utilizando closures.
Los métodos privados no sólo son útiles para restringir el acceso al código, también proporcionan una poderosa manera de administrar el espacio de nombres global, evitando que los métodos auxiliares ensucien la interfaz pública del código.
A continuación vamos a mostrar un ejemplo de cómo definir algunas funciones públicas que pueden acceder a variables y funciones privadas utilizando closures, también conocido como patrón Módulo:
var Contador = (function() {
var contadorPriv = 0;
function cambiar(val) {
contadorPriv += val;
}
return {
incrementar: function() {
cambiar(1);
},
decrementar: function() {
cambiar(-1);
},
valor: function() {
return contadorPriv;
}
}
})();
console.log(Contador.valor()); // 0
Contador.incrementar();
Contador.incrementar();
console.log(Contador.valor()); // 2
Contador.decrementar();
console.log(Contador.valor()); // 1
Hasta ahora cada closure ha tenido su propio entorno. En cambio, aquí creamos un único entorno compartido por tres funciones: Contador.incrementar
, Contador.decrementar
y Contador.valor
.
El entorno compartido se crea en el cuerpo de una función anónima, que se ejecuta en el momento que se define. El entorno contiene dos elementos privados: una variable llamada contadorPriv
y una función llamada cambiar
. No se puede acceder a ninguno de estos elementos privados directamente desde fuera de la función anónima. Se accede a ellos por las tres funciones públicas que se devuelven desde el contenedor anónimo.
Esas tres funciones públicas son closures que comparten el mismo entorno. Gracias al ámbito léxico de JavaScript, cada uno de ellas tienen acceso a la variable contadorPriv
y a la función cambiar
.
En este caso hemos definido una función anónima que crea un contador, y luego la llamamos inmediatamente y asignamos el resultado a la variable Contador
. Pero podríamos almacenar esta función en una variable independiente y utilizarlo para crear varios contadores:
var crearContador = function() {
var contadorPriv = 0;
function cambiar(val) {
contadorPriv += val;
}
return {
incrementar: function() {
cambiar(1);
},
decrementar: function() {
cambiar(-1);
},
valor: function() {
return contadorPriv;
}
}
};
var Contador1 = crearContador();
var Contador2 = crearContador();
alert(Contador1.valor()); // 0
Contador1.incrementar();
Contador1.incrementar();
console.log(Contador1.valor()); // 2
Contador1.decrementar();
console.log(Contador1.valor()); // 1
console.log(Contador2.valor()); // 0
Ten en cuenta que cada uno de los dos contadores mantiene su independencia respecto al otro. Su entorno durante la llamada de la función crearContador()
es diferente cada vez. La variable del closure contadorPriv
contiene una instancia diferente cada vez.
Utilizar closures de este modo proporciona una serie de beneficios que se asocian normalmente con la programación orientada a objectos, en particular la encapsulación y la ocultación de datos que vimos en la unidad anterior.
4.1.5. Closures dentro de bucles
Antes de la introducción de la palabra clave let
en JavaScript 1.7, un problema común con closures ocurría cuando se creaban dentro de un bucle loop
. Veamos el siguiente ejemplo:
<p id="ayuda">La ayuda aparecerá aquí</p>
<p>Correo electrónico: <input type="email" id="email" name="email"></p>
<p>Nombre: <input type="text" id="nombre" name="nombre"></p>
<p>Edad: <input type="number" id="edad" name="edad"></p>
function muestraAyuda(textoAyuda) {
document.getElementById('ayuda').innerHTML = textoAyuda;
}
function setupAyuda() {
var textosAyuda = [
{'id': 'email', 'ayuda': 'Dirección de correo electrónico'},
{'id': 'nombre', 'ayuda': 'Nombre completo'},
{'id': 'edad', 'ayuda': 'Edad (debes tener más de 16 años)'}
];
for (var i = 0; i < textosAyuda.length; i++) {
var elem = textosAyuda[i];
document.getElementById(elem.id).onfocus = function() {
muestraAyuda(elem.ayuda);
}
}
}
setupAyuda();
El array textosAyuda
define tres avisos de ayuda, cada uno asociado con el id
de un campo de entrada en el documento. El bucle recorre estas definiciones, enlazando un evento onfocus
a cada uno que muestra el método de ayuda asociada.
Si probamos el código, no funciona como esperamos, ya que independientemente del campo en el que se haga foco, siempre se mostrará el mensaje de ayuda de la edad.
Esto se debe a que las funciones asignadas a onfocus
son closures: constan de la definición de la función y del entorno abarcado desde el ámbito de la función setupAyuda
. Pese a haber creado tres closures, todos comparten el mismo entorno. En el momento en que se ejecutan las funciones callback de onfocus
, el bucle ya ha finalizado y la variable elem
(compartida por los tres closures) referencia a la última entrada del array textosAyuda
.
Para solucionarlo, podemos utilizar más closures, añadiendo una factoría de función como se ha descrito anteriormente:
function muestraAyuda(textoAyuda) {
document.getElementById('ayuda').innerHTML = textoAyuda;
}
function crearCallbackAyuda(ayuda) {
return function() {
muestraAyuda(ayuda);
};
}
function setupAyuda() {
var textosAyuda = [
{'id': 'email', 'ayuda': 'Dirección de correo electrónico'},
{'id': 'nombre', 'ayuda': 'Nombre completo'},
{'id': 'edad', 'ayuda': 'Edad (debes tener más de 16 años)'}
];
for (var i = 0; i < textosAyuda.length; i++) {
var elem = textosAyuda[i];
document.getElementById(elem.id).onfocus = crearCallbackAyuda(elem.ayuda);
}
}
setupAyuda();
Ahora ya funciona correctamente, ya que en lugar de los tres callbacks compartiendo el mismo entorno, la función crearCallbackAyuda
crea un nuevo entorno para cada uno en el que ayuda
se refiere a la cadena correspondiente del array textosAyuda
.
Autoevaluación
A partir del siguiente código:
¿Qué saldrá por consola si el usuario clicka en el primer y cuarto botón? ¿Por qué? [1] |
4.1.6. Consideraciones de rendimiento
No es aconsejable crear innecesariamente funciones dentro de otras funciones si no se necesitan los closures para una tarea particular ya que afectará negativamente el rendimiento del script tanto en consumo de memoria como en velocidad de procesamiento.
Por ejemplo, cuando se crea un nuevo objeto/clase, los métodos normalmente deberían asociarse al prototipo del objeto en vez de definirse en el constructor del objeto. La razón es que con este último sistema, cada vez que se llama al constructor (cada vez que se crea un objeto) se tienen que reasignar los métodos.
Veamos el siguiente caso, que no es práctico pero sí demostrativo:
function MiObjeto(nombre, mensaje) {
this.nombre = nombre.toString();
this.mensaje = mensaje.toString();
this.getNombre = function() {
return this.nombre;
};
this.getMensaje = function() {
return this.mensaje;
};
}
El código anterior no aprovecha los beneficios de los closures. Podríamos modificarlo de la siguiente manera:
function MiObjeto(nombre, mensaje) {
this.nombre = nombre.toString();
this.mensaje = mensaje.toString();
}
MiObjeto.prototype = {
getNombre: function() {
return this.nombre;
},
getMensaje: function() {
return this.mensaje;
}
};
Sin embargo, no se recomienda redefinir el prototipo. Para ello, es mejor añadir funcionalidad al prototipo existente en vez de sustituirlo:
function MiObjeto(nombre, mensaje) {
this.nombre = nombre.toString();
this.mensaje = mensaje.toString();
}
MiObjeto.prototype.getNombre = function() {
return this.nombre;
};
MiObjeto.prototype.getMensaje = function() {
return this.mensaje;
};
En los dos ejemplos anteriores, todos los objetos comparten el prototipo heredado y no se van a definir los métodos cada vez que se crean objetos.
Mas información en https://developer.mozilla.org/es/docs/Web/JavaScript/Guide/Closures
4.2. Módulos
Los módulos permiten reutilizar código entre diferentes aplicaciones. Antes de entrar en detalle con el uso de módulos para organizar el código, es conveniente crear un espacio de nombres (namespace).
Todos sabemos que hay que reservar las variables globales para los objetos que tienen relevancia a nivel de sistema y que tienen que nombrarse de tal manera que no sean ambiguos y que minimicen el riesgo de colisión con otros objetos. En resumen, hay que evitar la creación de objetos globales, a no ser que sea estrictamente necesarios.
Aun así, vamos a hacer uso de las variables globales para crear un pequeño conjunto de objetos globales que harán de espacios de nombre para los módulos y subsistemas existentes.
4.2.1. Espacio de nombres estático
Este tipo fija el nombre del espacio de nombre de manera hard coded.
Una posibilidad de hacerlo mediante una asignación directa. Es el enfoque más sencillo, pero también el que conlleva más código y si queremos renombre el namespace, tenemos muchas referencias. Sin embargo, es seguro y nada ambiguo.
var miApp = {};
miApp.id = 0;
miApp.siguiente = function() {
miApp.id++;
console.log(miApp.id);
return miApp.id;
};
miApp.reset = function() {
miApp.id = 0;
};
miApp.siguiente(); // 1
miApp.siguiente(); // 2
miApp.reset();
miApp.siguiente(); // 1
Pese a que pensemos que haciendo uso de this
, podemos evitar repetir tanto el nombre del espacio de nombre, hay que tener cuidado ya no podemos evitar que se asignen las funciones a variables y por tanto, cambie el comportamiento de this
:
this
- https://jsbin.com/kafokol/edit?js,consolevar miApp = {};
miApp.id = 0;
miApp.siguiente = function() {
this.id++;
console.log(this.id);
return this.id;
};
miApp.reset = function() {
this.id = 0;
};
miApp.siguiente(); // 1
miApp.siguiente(); // 2
var getNextId = miApp.siguiente;
getNextId(); // NaN
La segunda opción es utilizar la notación de objetos literales, de manera que solo se referencia al namespace una sola vez, con lo que cambiar su nombre es trivial.
Sigue existiendo el riesgo de obtener un valor inesperado si se asigna un método a una variable, pero es asumible que los objetos definidos dentro de un objeto literal no se van a reasignar:
var miApp = {
id: 0,
siguiente: function() {
this.id++;
console.log(this.id);
return this.id;
},
reset: function() {
this.id = 0;
}
};
miApp.siguiente(); // 1
miApp.siguiente(); // 2
miApp.reset();
miApp.siguiente(); // 1
4.2.2. El patrón módulo
La lógica se protege del alcance global mediante un función envoltorio, normalmente una IIFE, la cual devuelve un objeto que representa el interfaz público del módulo. Al invocar inmediatamente la función y asignar el resultado a una variable que define el espacio de nombre, el API del módulo se restringe a dicho namespace.
var miModulo = (function() {
var privado; (1)
return {
// interfaz público
}
})();
1 | Las variables que no estén incluidas dentro del objeto devuelto, permacenerán privadas, solamente visitblaes por las funciones incluidas dentro del interfaz público |
Así pues, si reescribimos el ejemplo anterior mediante un módulo, tendremos:
var miApp = (function() {
var id = 0;
return { (1)
siguiente: function() {
id++;
console.log(id);
return id;
},
reset: function() {
id = 0;
}
};
})();
miApp.siguiente(); // 1
miApp.siguiente(); // 2
miApp.reset();
miApp.siguiente(); // 1
1 | Para que el módulo se comunique con el exterior, mediante return devolvemos un objeto cuyas propiedades son los métodos del módulo. |
Paso de parámetros
Si necesitamos pasarle parámetros a un método de un módulo es mejor pasar un objeto literal:
miApp.siguiente({incremento: 5});
Y en el módulo podemos comprobar si viene algun parámetro como método mediante el operador ||
:
siguiente: function() {
var misArgs = arguments[0] || ''; (1)
var miIncremento = misArgs.incremento || 1; (2)
id = id + miIncremento;
console.log(id);
return id;
}
1 | Comprobamos si recibimos un parámetro |
2 | Comprobamos si el parámetro recibido contiene la propiedad incremento . Si no, le asignamos 1 como valor por defecto |
Valores de configuración
Si nuestro módulo va a tener muchas variables para almacenar valores por defecto, es mejor centralizarlas y agruparlas dentro de un objeto privado del módulo:
var miApp = (function() {
var id = 0;
var CONF = { (1)
incremento: 1,
decremento: 1
};
return {
siguiente: function() {
var misArgs = arguments[0] || '';
var miIncremento = misArgs.incremento || CONF.incremento; (2)
id = id + miIncremento;
console.log(id);
return id;
},
reset: function() {
id = 0;
}
};
})();
miApp.siguiente(); // 1
miApp.siguiente({incremento: 5}); // 6
miApp.reset();
miApp.siguiente(); // 1
1 | Objeto de configuración con los valores por defecto para configurar el módulo |
2 | Si el parámetro no contiene la propiedad incremento , le asignamos el valor que tenemos en nuestro objeto de configuración. |
Encadenando llamadas
Si queremos encadenar la salida de un método como la entrada de otro, acción que realiza mucho jQuery, sólo tenemos que devolver this
como resultado de cada método, y así devolver como resultado del método el propio módulo:
var miApp = (function() {
var id = 0;
var CONF = {
incremento: 1,
decremento: 1
};
return {
siguiente: function() {
var misArgs = arguments[0] || '';
var miIncremento = misArgs.incremento || CONF.incremento;
id = id + miIncremento;
console.log(id);
return this; (1)
},
anterior: function() { (2)
var misArgs = arguments[0] || '';
var miDecremento = misArgs.decremento || CONF.decremento;
id = id - miDecremento;
console.log(id);
return this;
},
reset: function() {
id = 0;
},
};
})();
miApp.siguiente(); // 1
miApp.siguiente({incremento: 5}); // 6
miApp.anterior(); // 5
miApp.reset();
miApp.siguiente().siguiente().anterior({decremento: 3}); // 1 2 -1 (3)
1 | Devolvemos el módulo para poder encadenar el método |
2 | Creamos un segundo método que también soporta method chaining |
3 | Encadenamos una llamada con otra |
4.2.3. Espacio de nombres dinámico
También conocido como inyección de espacio de nombres, al definir el namespace de esta manera el código es más flexible y facilita tener múltiples instancias independientes de un mismo módulo en namespaces separador.
Para ello, se emplea un proxy referenciado directamente dentro de la función envoltorio, con lo cual no necesitamos devolver un valor para asignarlo al espacio de nombres. Para ello, simplemente le pasamos el objeto namespace como argumento a la IIFE:
var miApp = {};
(function(contexto) {
var id = 0;
contexto.siguiente = function() {
id++;
console.log(id);
return id;
};
contexto.reset = function() {
id = 0;
};
})(miApp);
miApp.siguiente(); // 1
miApp.siguiente(); // 2
miApp.reset();
miApp.siguiente(); // 1
En el caso de querer que el módulo se asocie al objeto global, tendremos que pasarle como argumento a la IIFE this .
|
Otra manera es utilizar como proxy del namespace el objeto this
, haciendo uso del método apply
:
this
- https://jsbin.com/fuyowi/edit?js,consolevar miApp = {};
(function() {
var id = 0;
this.siguiente = function() {
id++;
console.log(id);
return id;
};
this.reset = function() {
id = 0;
};
}).apply(miApp);
miApp.siguiente(); // 1
miApp.siguiente(); // 2
miApp.reset();
miApp.siguiente(); // 1
Consejos
|
Más información y ejemplos en https://www.kenneth-truyers.net/2013/04/27/javascript-namespaces-and-modules/
4.3. Expresiones regulares
JavaScript permite trabajar con expresiones regulares, las cuales son un modo de describir un patrón en una cadena de datos, y nos servirán para comprobar si un campo contiene un cierto número de dígitos, o si un email está bien formado.
Para crear una expresión regular, primero hemos de describir la expresión a crear, ya sea mediante un objeto RegExp
o incluyéndola entre barras /
. A continuación la hemos de asociar al elemento que queremos aplicarla.
Para trabar con expresiones regulares usaremos los métodos test()
y search()
:
var exReBatman = /Batman/; (1)
var exReBatman2 = new RegExp("Batman"); (2)
var cadena = "¿Sabías que Batman es mejor que Joker, y el mejor amigo de Batman es Robin?";
if (exReBatman.test(cadena)) { (3)
console.log("la cadena contiene a Batman");
var pos = cadena.search(exReBatman); (4)
console.log("en la posición " + pos);
}
1 | Creamos una expresión regular para la cadena Batman . Se crean de forma similar a las cadenas pero en vez de " se usa / |
2 | Otra manera de hacer lo mismo que en la linea anterior |
3 | Mediante el método test(cadena) comprueba si la expresión regular se encuentra en la cadena |
4 | Si nos interesa la posición donde se encuentra la expresión, mediante cadena.search(regexp) obtendremos -1 si no la encuentra, o la posición comenzando por 0 . |
Si en alguna expresión queremos incluir la barra /
como parte de la misma, la tendremos que escapar mediante la barra invertida \
, es decir, si quisiéramos crear una expresión que buscase barras haríamos esto: /\//
La sintaxis de las expresiones regulares en JavaScript es muy similar a la original del Bell Labs con alguna reinterpretación y extensión de Perl. Las reglas de interpretación pueden ser muy complejas ya que interpretan los caracteres de algunas posiciones como operadores y en otras como literales. Y aunque crear patrones mediante expresiones regular es complicado, todavía lo es más leerlos y modificarlos. Por lo tanto, antes de modificar una expresión regular existente, debes estar seguro de tener el conocimiento y destreza adecuada. |
4.3.1. Creando un patrón
Para crear un patrón tenemos diferentes caracteres para fijar el número de ocurrencias y/o colocación del patrón.
Si nos centramos en los patrones para indicar un conjunto de caracteres tenemos:
Elemento | Uso | RegExp | Ejemplo |
---|---|---|---|
|
Comienza por |
|
Batman es el mejor |
|
Acaba por |
|
El mejor es Batman |
|
dentro del rango (a, b o c) |
|
Batman, Betman, Bitman |
|
fuera del rango |
|
Botman, Bbtman, … |
|
entre un rango (de a a z) |
|
Batman, Bbtman, Bctman, … Betman |
|
cualquier caracter |
|
Batman, Bbtman, B-tman, B(tman, … |
|
dígito |
|
B1tman, B2tman, … |
|
alfanumérico o |
|
Batman, B_tman, B1tman, … |
|
espacio (tab, nueva línea) |
|
Batman , |
|
límite de palabra |
|
Batman con espacio delante o tras un salto de línea |
Con estos conjuntos podemos crear las siguientes expresiones regulares que serán verdaderas:
/[0123456789]/.test("en 2015");
/[0-9]/.test("en 2015");
/\d\d-\d\d-\d\d\d\d/.test("31-01-2015");
/[01]/.test("111101111");
/[^01]/.test("2015");
Los patrones se suelen repetir y en ocasiones necesitamos indicar un patron en base a un número de ocurrencias de un determinado patrón. Para ello, tenemos los siguientes patrones:
Elemento | Uso | RegExp | Ejemplo |
---|---|---|---|
|
cero o uno de a |
|
Btman, Batman |
|
cero o más de a |
|
Btman, Batman, Baatman, … |
|
uno o más de a |
|
Batman, Baatman, Baaatman, … |
|
exactamente num unidades de a |
|
Baaatman |
|
num o más unidades de a |
|
Baaatman, Baaaaatman, … |
|
hasta num unidades de a |
|
Batman, Baatman, Baaatman |
|
de num1 a num2 unidades de a |
|
Baatman, Baaatman, Baaaatman |
Con estos conjuntos podemos crear las siguientes expresiones regulares que serán verdaderas:
/\d+/.test("2015"); // falso para ""
/\d*/.test("2015");
/\d*/.test("");
/selfie?/.test("selfie");
/selfie?/.test("selfi");
/\d{1,2}-\d{1,2}-\d{4}/.test("31-01-2015");
Para agrupar expresiones se utilizan los paréntesis ()
. De este modo podemos utilizar un patrón de repetición sobre una expresión:
var bebeLlorando = /buu+(juu+)+/; (1)
bebeLlorando.test("buujuuuujuujuuu");
1 | El primer y el segundo + se aplican a la segunda u de buu y juu respectivamente. El tercero se aplica al grupo juu+ , permitiendo una o más secuencias. |
Finalmente, si lo que queremos es saber si un fragmento de texto contiene un patrón entre un conjunto de posibles patrones, hemos de utilizar el caracter |
, el cual denota una elección entre el patrón de su izquierda y el de su derecha.
var periodoTemporal = /\b\d+ ((dia|semana|año)s?|mes(es)?)\b/;
console.log("periodoTemporal".test("3 semanas"));
console.log("periodoTemporal".test("43 meses"));
A partir de estos elementos se pueden crear patrones muy complejos:
var patronDNI = /[0-9]{8}([-]?)[A-Za-z]/;
var patronFecha = /(0[1-9]|[12][0-9]|3[01])[- /.](0[1-9]|1[012])[- /.](19|20)\d\d/;
var patronEmail = /[-0-9a-zA-Z.+_]+@[-0-9a-zA-Z.+_]+\.[a-zA-Z]{2,4}/;
var patronURL = /^(?:([A-Za-z]+):)?(\/{0,3})([0-9.\-A-Za-z]+)(?::(\d+))?(?:\/([^?#]*))?(?:\?([^#]*))?(?:#(.*))?$/;
var url = "http://server.jtech.com:8080/apuntes-js?regexp#ejemplos";
Las expresiones regulares en JavaScript son difíciles de leer en gran parte porque no permiten ni comentarios ni espacios en blanco. Lo bueno es que la mayoría de expresiones regulares que necesitaremos en el día a día ya están creadas, sólo hay que buscar en Internet.
Una herramienta muy útil es http://www.regexper.com/, que nos permite obtener una representación visual de cualquier expresión regular |
4.3.2. Flags
Las expresiones regulares permiten las siguientes opciones que se incluyen tras cerrar la expresión y que modifican las restricciones que se aplican para encontrar las ocurrencias:
-
g
: emparejamiento global, con lo que realiza la búsqueda en la cadena completa, en vez de pararse en la primera ocurrencia -
i
: ignora las mayúsculas, con lo cual la expresión deja de ser sensible las mayúsculas -
m
: múltiples líneas, de modo que aplica los caracteres de inicio y fin de linea (^
y$
respectivamente) a cada línea de una cadena que contiene varias lineas.
Por ejemplo:
var casaCASA = /casa/i;
4.3.3. Ocurrencias
Además del método test()
, podemos utilizar el método exec()
el cual devolverá un objeto con información de las ocurrencias encontradas o null
en caso contrario.
var ocurrencia = /\d+/.exec("uno dos 100");
console.log(ocurrencia); // ["100"]
console.log(ocurrencia.index); // 8
El objeto devuelto por exec
tiene una propiedad index
con la posiciones dentro de la cadena donde se encuentra la ocurrencia. Además, el objeto en sí es un array de cadenas, cuyo primer elemento es la ocurrencia encontrada.
String.match(regexp) Las cadenas también tienen un método
|
Cuando una expresión regular contiene subexpresiones agrupadas mediante paréntesis, la ocurrencia que cumple esos grupos aparecerá en el array. La ocurrencia completa siempre es el primer elemento. El siguiente elemento es la parte que cumple el primer grupo (aquel cuyo paréntesis de apertura venga primero), después el segundo grupo, etc..
var patronDNI = /([0-9]{8})([-]?)([A-Za-z])/;
var ocurrencia = patronDNI.exec("12345678A"); // ["12345678A", "12345678", "", "A"]
var numero = ocurrencia[1];
var letra = ocurrencia[3];
var txtEntreComillasSimples = /'([^']*)'/;
console.log(txtEntreComillasSimples.exec("Yo soy 'Batman'")); // ["'Batman'","Batman"]
Cuando un grupo se cumple en múltiples ocasiones, sólo la última ocurrencia se añade al array:
console.log(/(\d)+/.exec("2015")); // ["2015","5"];
Trabajando con Fechas
Mediante expresiones regulares podemos parsear una cadena que contiene una fecha y construir un objeto
|
4.3.4. Sustituyendo
Ya vimos en la primera sesión que podemos utilizar el método replace
con una cadena para sustituir una parte por otra:
console.log("papa".replace("p","m")); // "mapa"
El primer parámetro también puede ser una expresión regular, de modo que la primera ocurrencia de la expresión regular se reemplazará. Si a la expresión le añadimos la opción g
(de *g*lobal), se sustituirán todas las ocurrencias, no sólo la primera:
console.log("papa".replace(/p/g,"m")); // "mama"
La gran ventaja de utilizar expresiones regulares para sustituir texto es que podemos volver a las ocurrencias y trabajar con ellas. Supongamos que tenemos un listado de personas ordenado mediante "Apellido, Nombre", y queremos cambiarlo para poner delante el nombre y quitar la coma:
var personas = "Medrano, Aitor\nGallardo, Domingo\nSuch, Alejandro";
console.log(personas.replace(/([\w]+), ([\w]+)/g, "$2 $1")); (1)
1 | Los elementos $1 y $2 referencian a los grupos de paréntesis del patrón. |
Hemos visto que en la cadena de sustitución hemos utilizado $n
para referenciar al bloque n de la expresión regular. De este modo, $1
referencia al primer patrón, $2
para el segundo, … hasta $9
. La ocurrencia completa se referencia mediante $&
.
También podemos pasar una función en vez de una cadena de sustitución, la cual se llamará para todas las ocurrencias (y para la ocurrencia completa también).
Por ejemplo:
var cadena = "Los mejores lenguajes son Java y JavaScript";
console.log(cadena.replace(/\b(java|javascript)\b/ig, function(str) {
return str.toUpperCase();
}))
4.4. Librerías en JavaScript
Existen multitud de librerías y frameworks en JavaScript. Algunas realizan tareas específicas mientras que otras son más genéricas. Respecto a las de propósito general las más conocidas son:
-
jQuery (http://jquery.com): Estándar de facto dentro del mundo JS. La estudiaremos en detalle en posteriores sesiones.
-
Prototype (http://www.prototypejs.org/) fue una de las primeras librerías JavaScript. Normalmente se usaba de manera conjunta a la librería de efectos visuales scriptaculous (http://script.aculo.us/), la cual añade animaciones y controles de interfaz de usuario.
-
Mootools (http://mootools.net/): Framework completo que requiere un nivel medio-avanzado de JavaScript con buena documentación, y menos orientada al DOM que otras librerías, pero con soporte de herencia.
-
Yahoo User Interface Library (YUI) (http://developer.yahoo.com/yui/): librería que nace como un proyecto de Yahoo y es la que usa en los proyectos internos. En Agosto de 2014, el equipo de ingenieros han anunciado que van a reducir el apoyo a la librería.
-
Dojo Toolkit (http://dojotoolkit.org/): Muy completa con una gran colección de ficheros JS que resuelven cualquier tarea. Se trata de una librería con un núcleo muy ligero (4KB)
-
Closure Library (http://developers.google.com/closure/library) es la librería que usa Google para sus aplicaciones web, es decir, GMail, Google Maps, Calendar, etc…
-
Underscore (http://underscorejs.org) librería que agrupa múltiples funciones de manipulación de objetos, funciones y colecciones, que completan a aquellos navegadores que no implementan HTML5.
El siguiente gráfico muestra el grado de implantación de las librerías, tanto a nivel global como entre las páginas que utilizan JavaScript:
Si cruzamos esos datos con el tráfico de los sites que utilizan las librerías tenemos la posición de mercado:
Podemos consultar más datos estadísticos con actualización diaria en http://w3techs.com/technologies/overview/javascript_library/all |
Otras librerías más específicas son:
-
Bootstrap (http://getbootstrap.com): generador de websites adaptables (responsive)
-
Handlebars (http://handlebarsjs.com/) o Mustache.js (https://github.com/janl/mustache.js): uso de plantillas
-
YepNope (http://yepnopejs.com): carga condicional de scripts
-
Lightbox (http://lokeshdhakar.com/projects/lightbox2/) : carrusel de fotos
-
ShadowBox (http://www.shadowbox-js.com): permite mostrar contenido multimedia en ventanas emergentes, ya sean fotos, vídeos o archivos flash.
-
Parsley (http://parsleyjs.org): para validaciones de formularios
-
Datejs (http://www.datejs.com) o Moment.js: gestión de fechas con lenguaje natural
4.4.1. Inclusión de librerías
Para incluir una librería, tal como vimos en [_uso_en_el_navegador], usaremos la etiqueta <script>
para referenciar una archivo externo con el código de la librería.
Cuando incluimos varias librerías, el orden es importante. Las librerías que dependen de otras deben incluirse después. Por ejemplo, si tenemos una librería que hace uso de jQuery, las librerías dependientes deben colocarse a continuación:
...
<script src="jquery.js" />
<script src="libQueUsaJQuery.js" />
</body>
4.4.2. CDN
Cuando trabajamos con librerías de terceros es muy útil y eficiente trabajan con librerías hospedas en Internet mediante CDNs (Content Delivery Network / Red de entrega de contenidos). Estos servidores guardan copias de las librerías de manera transparente al desarrollador y redirigen la petición al servidor más cercano.
Por ejemplo, Google ofrece enlaces a la gran mayoría de librerías existentes en https://developers.google.com/speed/libraries/ o http://code.google.com/apis/libraries o buscar Google CDN
Así pues, para incluir una librería mediante CDN haríamos:
...
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.6.1/jquery.min.js"></script>
</body>
</html>
La gran ventaja es que al usarse por múltiples desarrolladores, lo más seguro que el navegador ya la tenga cacheada, y si no, que se encuentre hospedado en un servidor más cercano que el hosting de nuestra aplicación.
Si falla el CDN, y queremos hacer una carga de otro servidor CDN o de la copia de nuestro servidor, podemos hacerlo de la siguiente manera: http://www.etnassoft.com/2011/06/29/cargar-jquery-desde-un-cdn-o-desde-servidor/ )
4.5. Testing
Todos sabemos que las pruebas son importantes, pero no le damos la importancia que merecen.
Al hacer una aplicación testable nos aseguramos que la cantidad de errores que haya en nuestra aplicación se reduzca, lo que también incrementa la mantenibilidad de la aplicación y promueve que el código esté bien estructurado.
Las pruebas de unidad en el lado del cliente presentan una problemática distinta a las del servidor. Al trabajar con código de cliente nos pelearemos muchas veces con la separación de la lógica de la aplicación respecto a la lógica DOM, así como la propia estructura del código JavaScript.
Ya vimos en la primera sesión que el Console API ofrece el método console.assert(bool, mensaje)
para comprobar si se cumple una determinada condición booleana. Una aserción es una sentencia que predice el resultado del código con el valor esperado. Si la predicción no se cumple, la aserción falla, y de este modo sabemos que algo ha ido mal. Por ejemplo:
console.assert(1 == "1", "Conversión de tipos");
console.assert(1 === "1", "Sin conversión de tipos"); // Assertion failed: Sin conversión de tipos
Mediante este tipo de aserciones podemos hacer pruebas sencillas pero sin automatizar, ya que requieren que el desarrollador comprueba la consola para analizar los mensajes visualizadas.
Por suerte, existen muchas librerías de pruebas que nos ayudan a probar nuestro código, crear métricas sobre la cobertura y analizar la complejidad del mismo.
4.5.1. QUnit
QUnit (http://qunitjs.com) es un framework de pruebas unitarias que facilita depurar el código. Desarrollado por miembros del equipo de jQuery, es el framework utilizado para probar jQuery. Esto no quiere decir que sea exclusivo de jQuery, sino que permite probar cualquier código JavaScript.
Antes de empezar a codificar las pruebas, necesitamos enlazar mediante un CDN a la librería o descargarla desde http://qunitjs.com. También necesitamos enlazar la hoja de estilo para formatear el resultado de las pruebas.
Lanzador QUnit
Para escribir nuestra primera prueba necesitamos preparar el entorno con un documento que hace de lanzador:
<!DOCTYPE html>
<html lang="es">
<head>
<title>QUnit Test Suite</title>
<meta charset="utf-8" />
<link rel="stylesheet" href="//code.jquery.com/qunit/qunit-1.22.0.css" type="text/css" media="screen">
</head>
<body>
<div id="qunit"></div> (1)
<div id="qunit-fixture"></div> (2)
<script type="text/javascript" src="miProyecto.js"></script> (3)
<script src="//code.jquery.com/qunit/qunit-1.22.0.js"></script>
<script src="misPruebas.js"></script> (4)
</body>
</html>
1 | En esta capa se mostrarán los resultados de la prueba |
2 | Aquí podemos colocar código que añade, modifica o elimina elementos del DOM. El código aquí colocado será reseteado con cada prueba. |
3 | El código a probar lo colocaremos de manera similar a miProyecto.js |
4 | En misPruebas.js colocaremos el código de las pruebas. De este modo tendremos las pruebas desacopladas del código de aplicación. |
En este ejemplo hemos usado el CDN para referenciar a la librería y hoja de estilo necesarias. Para un entorno de desarrollo e integración continua es mejor descargarla y tener ambos archivos en local.
Si lanzamos el archivo sin código ni pruebas, al abrir el documento HTML veremos el interfaz que ofrece:
Checkboxes de QUnit
La opción "Hide passed tests" permite ocultar las pruebas exitosas y mostrar sólo los fallos. Esto es realmente útil cuando tenemos un montón de pruebas y sólo unos pocos fallos. Si marcamos "Check for Globals" QUnit crea una lista de todas las propiedades del objeto El checkbox "No try-catch" provoca que QUnit ejecute la prueba fuera de un bloque |
Vamos a probar la siguiente función, que colocamos en miProyecto.js
, la cual obtiene si un número es par:
function esPar(num) {
return num % 2 === 0;
}
Caso de prueba
Como hemos comentado antes, vamos a colocar las pruebas de manera desacoplada del código, en el archivo misPruebas.js
.
Todas las pruebas comienzan con la función QUnit.test()
(o QUnit.asyncTest()
si son asíncronas, las cuales veremos en la siguiente sesión).
Esta función recibe un parámetro con una cadena que sirve para identificar el caso de prueba, y una función que contiene las aserciones que el framework ejecutará. QUnit le pasa a esta función un argumento que expone todos los métodos de aserción de QUnit.
De modo, que nuestro código quedará así:
QUnit.test('Hola QUnit', function(assert) {
// Aserciones con las pruebas
});
Aserciones
El bloque principal de las pruebas son las aserciones. Las aserciones que ofrece QUnit son ok
, equal
, notEqual
, strictEqual
, notStrictEqual
, deepEqual
, notDeepEqual
y throws
.
Para ejecutar aserciones, hay que colocarlas dentro de un caso de prueba (el cual hemos colocado en misPruebas.js
). El siguiente ejemplo demuestra el uso de la aserción booleana ok
:
QUnit.test('esPar()', function(assert) { (1)
assert.ok(esPar(0), 'Cero es par'); (2)
assert.ok(esPar(2), 'Y dos');
assert.ok(esPar(-4), 'Los negativos pares');
assert.ok(!esPar(1), 'Uno no es par');
assert.ok(!esPar(-7), 'Ni un 7 negativo');
});
1 | Al llamar a QUnit.test() se construye un caso de prueba. El primer parámetro es la cadena que se mostrará en el resultado de la prueba, y el segundo es la función callback que contiene nuestras aserciones y se invocará una vez que ejecutemos QUnit. |
2 | Escribimos cinco aserciones, todas siendo booleanas mediante ok(expresionVerdadera, mensaje) . Una aserción booleana espera que el primer parámetro sea verdadero. El segundo parámetro, el cual es opcional, es el mensaje que se muestra en el resultado si la aserción se cumple. |
Una vez lanzada la prueba, obtendremos que ha cumplido todas las aserciones:
Como todas las aserciones han pasado, podemos asegurar que la función esPar()
funciona correctamente, o al menos, de la manera esperada.
Veamos que sucede si la aserción falla:
QUnit.test('esPar()', function(assert) {
assert.ok(esPar(0), 'Cero es par');
assert.ok(esPar(2), 'Y dos');
assert.ok(esPar(-4), 'Los negativos pares');
assert.ok(!esPar(1), 'Uno no es par');
assert.ok(!esPar(-7), 'Ni un 7 negativo');
assert.ok(esPar(3), '3 es par'); (1)
});
1 | Provocamos el error con una aserción errónea |
Como podemos esperar, el resultado muestra que la aserción falla:
Ya sabemos que ok()
no es la única aserción que ofrece QUnit. A continuación vamos a ver otras aserciones que permiten una granularidad más fina en las aserciones.
Comparación
Si vamos a comparar valores es mejor utilizar la aserción equal(valorReal, valorEsperado, mensaje)
, la cual requiere dos parámetros con los valores reales y esperados y el mensaje opcional.
Al mostrar el resultado de la prueba, aparecerá tanto el valor real como el esperado, lo cual facilita la depuración del código.
Por lo tanto, en vez de hacer la prueba mediante una aserciones booleana:
QUnit.test('comparaciones', function(assert) {
assert.ok( 1 == 1, 'uno es igual que uno');
});
es mejor hacerla mediante un aserción de comparación:
QUnit.test('comparaciones', function(assert) {
assert.equal( 1, 1, 'uno es igual que uno');
assert.equal( 1, true, 'pasa porque 1 == true');
});
Si queremos probar una aserción que falla podemos hacer:
QUnit.test('comparaciones', function(assert) {
assert.equal( 2, 1, 'falla porque 2 != 1');
});
Ahora podemos observar como el mensaje de error es más completo que con la aserción booleana:
Si queremos negar el comportamiento, QUnit ofrece la aserción notEqual
para comprobar un valor que no esperamos.
Si queremos que se realice una comparación estricta, hemos de usar la aserción strictEqual()
. Como podemos suponer, la aserción strictEqual()
utiliza el operador ===
para realizar las comparaciones, lo que conviene tener en cuenta al comparar ciertos valores:
QUnit.test('comparacionesEstrictas', function(assert) {
assert.equal( 0, false, 'pasa la prueba');
assert.strictEqual( 0, false, 'falla');
assert.equal( null, undefined, 'pasa la prueba');
assert.strictEqual( null, undefined, 'falla');
});
Ambas aserciones, estrictas o no, al utilizar los operadores ==
o ===
para comparar sus parámetros, no se puede utilizar con arrays u objetos:
QUnit.test('comparacionesArrays', function(assert) {
assert.equal( {}, {}, 'falla, estos objetos son diferentes');
assert.equal( {a: 1}, {a: 1} , 'falla');
assert.equal( [], [], 'falla, son diferentes arrays');
assert.equal( [1], [1], 'falla');
});
Para que estas pruebas de igualdad se cumplan, QUnit ofrece otro tipo de aserción, la aserción identidad.
Identidad
La aserción identidad deepEqual(valorReal, valorEsperado, mensaje)
espera los mismos parámetros que equal
, pero realiza una comparación recursiva que funciona tanto con tipos primitivos como con arrays, objetos y expresiones regulares. Si reescribimos el ejemplo anterior ahora si que pasará las pruebas:
QUnit.test('comparacionesArraysRecursiva', function(assert) {
assert.deepEqual( {}, {}, 'correcto, los objetos tienen el mismo contenido');
assert.deepEqual( {a: 1}, {a: 1} , 'correcto');
assert.deepEqual( [], [], 'correcto, los arrays tienen el mismo contenido');
assert.deepEqual( [1], [1], 'correcto');
})
Si queremos negar la identidad podemos hacer uso de su antónimo notDeepEqual()
.
Además, si estamos interesados en comparar las propiedades y valores de un objeto podemos usar la aserción propEqual()
, la cual será existosa si las propiedades son idénticas, con lo cual podemos reescribir las mismas pruebas:
QUnit.test('comparacionesPropiedades', function(assert) {
assert.propEqual( {}, {}, 'correcto, los objetos tienen el mismo contenido');
assert.propEqual( {a: 1}, {a: 1} , 'correcto');
assert.propEqual( [], [], 'correcto, los arrays tienen el mismo contenido');
assert.propEqual( [1], [1], 'correcto');
});
Excepciones
Para comprobar si nuestro código lanza Error
, haremos uso de la aserción throws(función [, excepcionEsperada] [,mensaje])
, la cual también puede comprobar que función lanza la excepcionEsperada.
throws
- http://jsbin.com/bineto/7/edit?html,js,outputQUnit.test('excepciones', function(assert) {
assert.throws( function() { throw Error("Hola, soy un Error"); },
'pasa al lanzar el Error');
assert.throws( function() { x; }, // ReferenceError
'pasa al no definir x, ');
assert.throws( function() { esPar(2); },
'falla porque no se lanza ningun Error');
});
Podemos observar como la tercera aserción falla:
Módulos
Colocar todas las aserciones en un único caso de prueba no es buena idea, ya que dificulta su mantenibilidad y no devuelve un resultado claro.
Es mucho mejor colocarlos en diferentes casos de pruebas donde cada caso se centra en una única funcionalidad.
Incluso, podemos organizar los casos de pruebas en diferentes módulos, lo cual nos ofrece un nivel mayor de abstracción. Para ello, hemos de llamar a la función module()
:
QUnit.module('Módulo A');
QUnit.test('una prueba', function(assert) { assert.ok(true, "ok"); });
QUnit.test('otra prueba', function(assert) { assert.ok(true, "ok"); });
QUnit.module('Módulo B');
QUnit.test('una prueba', function(assert) { assert.ok(true, "ok"); });
QUnit.test('otra prueba', function(assert) { assert.ok(true, "ok"); });
Podemos observar como las pruebas se agrupan en los dos módulos definidos, y en la cabecera nos aparece un desplegable donde podemos filtrar las pruebas realizadas por los módulos disponibles:
Además de agrupar pruebas, podemos usar QUnit.module()
para refactorizar las pruebas y extraer código común dentro del módulo. Para ello, le añadirmos un segundo parámetro (el cual es opcional) que define las funciones que se ejecutan antes (setup
) o después (teardown
) de cada pruebas:
QUnit.module("Módulo C", {
setup: function( assert ) {
assert.ok( true, "una aserción extra antes de cada test" );
}, teardown: function( assert ) {
assert.ok( true, "y otra más después de cada prueba" );
}
});
QUnit.test("Prueba con setup y teardown", function(assert) {
assert.ok( esPar(2), "dos es par");
});
QUnit.test("Prueba con setup y teardown", function(assert) {
assert.ok( esPar(4), "cuatro es par");
});
Podemos especificar las dos propiedades setup
como teardown
a la vez, o definir únicamente la que nos interese.
Al llamar a QUnit.module()
de nuevo sin ningún argumento adicional, las funciones de setup
y teardown
que haya definido algún otro modulo serán reseteadas.
Expectativas
Al crear una prueba, es una buena práctica indicar el número de aserciones que esperamos que se ejecuten. Al hacerlo, la prueba fallará si una o más aserciones no se ejecutan.
QUnit ofrece el método expect(numeroAserciones)
para este propósito, sobre todo cuando tratamos con código asíncrono, aunque también es útil en funciones síncronas.
Por ejemplo, si queremos probar las comparaciones de nuevo, podemos añadir:
QUnit.test('comparacionesPropiedades', function(assert) {
expect(4);
// aserciones
}
Pruebas asíncronas
Ya veremos en la siguiente sesión como realizar llamadas asíncronas a servicios REST mediante AJAX. Casi cualquier proyecto real que contiene JavaScript accede o utiliza funciones asíncronas.
Para realizar un caso de prueba asíncrona usaremos QUnit.asyncTest(nombre, funcionPrueba)
de manera similar a las pruebas síncronas. Pero aunque la firma sea semejante, su funcionamiento varía.
Dentro de la prueba usaremos los métodos QUnit.start()
y QUnit.stop()
. Cuando QUnit ejecuta un caso de prueba asíncrona, automáticamente detiene el testrunner. Este permanecerá parado hasta que el caso de prueba que contiene las aserciones invoque a QUnit.start()
, el cual inicia o continúa la ejecución de la prueba. Este método acepta un entero como argumento opcional para fusionar múltiples llamadas QUnit.start()
en una sola.
Para paralizar una prueba usaremos QUnit.stop()
. Al hacerlo se incrementa el número de llamadas necesarias a QUnit.start()
para que el testrunner vuelva a funcionar. QUnit.stop()
también acepta un argumento opcional con un entero que especifica el número de llamadas adicionales a QUnit.start()
que el framework tiene que esperar. Por defecto, su valor es 1.
Para probar estos métodos vamos a centrarnos en la siguiente función que recorre todos los parámetros y obtiene el mayor de ellos, la cual queremos probar que funciona correctamente:
function mayor() {
var max=-Infinity;
for (var i=0, len=arguments.length; i<len; i++) {
if (arguments[i] > max) {
max = arguments[i];
}
}
return max;
}
Si imaginamos que esta función trabaja con un conjunto de parámetros muy grande, podemos deducir que queremos evitar que el navegador del usuario se quede bloqueado mientras se calcula el resultado. Para ello, llamaremos a nuestra función max()
dentro de un callback que le pasamos a window.setTimeout()
con un retraso de 0 milisegundos.
Así, el caso de prueba asíncrona quedará así:
QUnit.asyncTest('max', function (assert) { (1)
expect(1); (2)
window.setTimeout(function() {
assert.strictEqual(mayor(3, 1, 2), 3, 'Todo números positivos');
QUnit.start(); (3)
}, 0);
});
1 | Al iniciar la prueba asíncrona, el testrunner se paraliza |
2 | Indicamos que esperamos una aserción |
3 | Primero calculamos el valor, y una vez calculado, reiniciamos el testrunner. Si no hubiesemos llamado a QUnit.start() , el testrunner continuará parado y la prueba habría fallado. |
Lo normal es realizar más de una aserción en un caso de prueba. Vamos a completar la prueba con diferentes aserciones. De este modo, entra en juego QUnit.stop()
:
QUnit.asyncTest('max', function (assert) {
expect(4); (1)
QUnit.stop(3); (2)
window.setTimeout(function() {
assert.strictEqual(mayor(), -Infinity, 'Sin parámetros');
QUnit.start();
}, 0);
window.setTimeout(function() {
assert.strictEqual(mayor(3, 1, 2), 3, 'Todo números positivos');
QUnit.start();
}, 0);
window.setTimeout(function() {
assert.strictEqual(mayor(-10, 5, 3, 99), 99, 'Números positivos y negativos');
QUnit.start();
}, 0);
window.setTimeout(function() {
assert.strictEqual(mayor(-14, -22, -5), -5, 'Todo números negativos');
QUnit.start();
}, 0);
});
1 | Esperamos realizar cuatro aserciones |
2 | Llamamos a QUnit.stop() porque realizamos cuatro llamadas asíncronas, con lo que tenemos que añadirle 3 unidades. Al emplear QUnit.asyncTest() , el framework solo espera una llamada de QUnit.start() . Si hubiesemos omitido la llamada a QUnit.stop() indicando las tres llamadas adicionales a QUnit.start() , la prueba habría fallado ya que el número de aserciones no habría coincidido con la expectativa del caso de prueba. |
Al ejecutar la prueba tendremos el siguiente resultado:
En esta sección hemos visto como probar código asíncrono que no realiza operaciones AJAX. Sin embargo, lo más normal es cargar o enviar datos al servidor. En estos casos, es mejor no confiar en los datos que devuelve el servidor y utilizar mocks para las peticiones AJAX.
4.5.2. Otras librerías
Aunque no hemos centrado en QUnit, no es la única librería de pruebas que deberíamos conocer. QUnit se centra en las pruebas unitarias siguiendo un enfoque TDD (Test Driven Development).
Dependiendo del enfoque necesitaremos emplear las siguientes librerías:
-
Jasmine (http://jasmine.github.io): Sigue un enfoque BDD (Behaviour Driven Development) para centrarse más en los requisitos de negocio que en el código. En los módulos de BackBone y AngularJS trabajaremos con esta librería.
El código de las pruebas será similar a:
describe("Una suite", function() { it("contiene un requisito con una expectativa", function() { expect(true).toBe(true); }); });
-
Mocha (http://mochajs.org): Framework de pruebas muy flexible que también sigue un enfoque BDD, con posibilidad de añadir nuevas aserciones, normalmente mediante Chai.js (http://chaijs.com/) y emplear promesas.
-
Sinon.js (http://sinonjs.org/) o Mockjax (https://github.com/jakerella/jquery-mockjax): frameworks para la creación de objetos mocks y crear objetos que espían y/o suplantan servicios REST/peticiones AJAX
-
Blanket.js (http://blanketjs.org/): añade informes de cobertura de pruebas sobre QUnit, Mocha o Jasmine. Otra alternativa es CoverJS (https://github.com/arian/CoverJS)
-
Plato (https://github.com/es-analysis/plato): permite analizar la complejidad del código JavaScript
4.6. Ejercicios
4.6.1. (0.8 ptos) Ejercicio 41. Canificador
Realizar un módulo denominado Canificador
que ofrezca la funcionalidad del método toCani
, permitiendo configurar el final, el cual por defecto es "HHH"
.
Para ello, podremos realizar una llamada al módulo así:
Canificador.toCani({"texto": "Texto a canificar", "final":"ARGH"});
Haciendo uso de expresiones regulares, vamos a mejorar la funcionalidad para que sustituya las "ca", "co", "cu" por "ka", "ko", "ku", pero no así las "ce", "ci". Además, todas las ocurrencias de "qu" también se sustiuirán por "k", y las "ch" por "x".
También queremos almacenar el número de ocasiones que se ha invocado el método toCani
, el cual se podrá consultar mediante Canificador.getTotal()
.
El módulo a su vez debe permitir descanificar una cadena mediante el método unCani
, la cual pasará toda la cadena a minúsculas, sustituirá las letras k
por c
y eliminirá el final que haya introducido el Canificador. Esta operación reducirá en una unidad el número total de invocaciones del módulo.
Además, debéis utilizar la siguiente plantilla para visualizar el funcionamiento:
<textarea id="texto">Cecilia me ha dicho que este es el texto a canificar</textarea>
<button id="canificar">Canificar</button>
<button id="descanificar">Descanificar</button>
<br />
Resultado: <div id="resultado"> </div>
<br />
Total: <div id="total"></div>
El código del Canificador junto a la gestión de eventos se almacenarán en una archivo denominado ej41.js
.
4.6.2. (0.4 ptos) Ejercicio 42. Pruebas
Para comprobar que el módulo anterior funciona correctamente, hay que desarrollar un suite de pruebas mediante QUnit que valide todas las operaciones, usando las aserciones adecuadas así como indicando mediante expectativas el número de aserciones a probar, y creando un módulo tanto para toCani
como para unCani
.
Todo el código se almacenará en una archivo denominado ej42QUnit.js
.