Guía Completa de JavaScript y DOM
Guía Completa de JavaScript y DOM
Tabla de contenidos
1. JavaScript. El lenguaje ..................................................................................................... 7
1.1. De JavaScript a ECMAScript ................................................................................. 7
Strict mode ............................................................................................................. 8
1.2. Uso en el navegador .............................................................................................. 8
Hola ExpertoJavaUA .............................................................................................. 9
1.3. Herramientas ........................................................................................................ 10
Dev Tools ............................................................................................................. 11
Console API ......................................................................................................... 12
1.4. Datos y variables .................................................................................................. 13
Variables .............................................................................................................. 13
Texto .................................................................................................................... 15
Números ............................................................................................................... 16
Booleanos ............................................................................................................ 17
Coerción de tipos ................................................................................................. 18
Fechas y horas .................................................................................................... 18
typeof .............................................................................................................. 19
1.5. Instrucciones ......................................................................................................... 20
1.6. Funciones ............................................................................................................. 20
Función declaración ............................................................................................. 21
Función expresión ................................................................................................ 21
Funciones declaración vs expresión .................................................................... 22
Callbacks .............................................................................................................. 23
arguments ........................................................................................................ 24
1.7. Alcance ................................................................................................................. 24
Hoisting ................................................................................................................ 26
1.8. Timers ................................................................................................................... 27
1.9. Gestión de errores ............................................................................................... 28
Capturando excepciones ..................................................................................... 29
Lanzando excepciones ........................................................................................ 30
Debug ................................................................................................................... 30
Errores comunes .................................................................................................. 30
1.10. Ejercicios ............................................................................................................ 31
(0.4 ptos) Ejercicio 11. toCani ......................................................................... 31
(0.6 ptos) Ejercicio 12. Temporizador .................................................................. 31
2. JavaScript orientado a objetos ........................................................................................ 33
2.1. Trabajando con objetos ........................................................................................ 33
Propiedades ......................................................................................................... 33
Métodos ............................................................................................................... 34
2.2. Objetos literales .................................................................................................... 34
Objetos anidados ................................................................................................. 35
2.3. Creando un tipo de datos .................................................................................... 36
Función factoría ................................................................................................... 36
Función constructor ............................................................................................. 37
2.4. Invocación indirecta .............................................................................................. 39
2.5. Descriptores de propiedades ................................................................................ 40
1
JavaScript
2
JavaScript
3
JavaScript
4
JavaScript
5
JavaScript
6
JavaScript
1. JavaScript. El lenguaje
Antes de comenzar con JavaScript, es necesario fijar una serie de conocimientos previos. A
lo largo del módulo se van a utilizar páginas HTML, por lo que deberemos saber las etiquetas
básicas, la estructura de una página web, uso de listas y capas. Junto a HTML, definiremos
su estilo mediante CSS, por lo que es necesario conocer su uso básico, y cómo se aplica un
estilo a una etiqueta, una clase o un identificador.
Dentro del mundo de la programación, JavaScript tiene mala fama. Gran parte se debe a
que se trata de un lenguaje débilmente tipado, que permite usar variables sin declarar y al
tratarse de un lenguaje interpretado, no hay compilador que te diga que hay algo erróneo en
tu programa. Realmente JavaScript ofrece mucha flexibilidad y las malas críticas vienen más
por el desconocimiento del lenguaje que por defectos del mismo.
Contrario a lo que el nombre sugiere, JavaScript tiene poco que ver con Java. La similitud fue
una decisión de marketing, allá por el año 1995 cuando Netscape introdujo el lenguaje en el
navegador.
7
JavaScript
Strict mode
El modo estricto elimina características del lenguaje, lo que simplifica los programas y reduce
la cantidad de errores que pueden contener. Por ejemplo, ES5 desaconseja el uso de la
instrucción with , lo que provoca que se lance un error al encontrar dicha instrucción, aunque
si el navegador no soporta ES5 funcionará correctamente. Otros ejemplos de errores con
ES5 es usar variables que no hemos declarado previamente, declarar funciones donde algún
parametro está repetido, un objeto donde dos propiedades tengan el mismo nombre, etc…
Para activar el modo estricto hay que introducir la cadena "use strict" , lo que las
implementaciones antiguas del lenguaje simplemente pasarán por alto, con lo que este modo
es retrocompatible con los navegadores que no lo soporten.
Dependiendo del alcance, ya sea a nivel de función o global usaremos la cadena "use
strict" al inicio del fichero o en la primera línea de la función. Por ejemplo, si sólo queremos
activar el modo dentro de una función haremos:
function modoEstricto() {
"use strict";
// resto de la función
}
Esto significa que el código interno de la función se ejecutará con el subconjunto estricto del
lenguaje, mientras que otras funciones puede que hagan uso del conjunto completo.
El objetivo a medio plazo del lenguaje es que en el futuro sólo se soportará el modo estricto,
con lo que ES5 es una versión transicional en la que se anima (pero no obliga) a escribir código
en modo estricto.
<script>
// Instrucciones JavaScript
</script>
• en un archivo externo (con extensión .js ), de modo que se pueda reutilizar entre varios
documentos:
<script src="ficheroJavaScript.js"></script>
async y defer
Desde HTML5, la etiqueta script admite los siguiente atributos
que provocan que el script comience su descarga inmediatamente sin
pausar el parser:
8
JavaScript
<a href="javascript:nombreFuncionJavaScript()">Validar</a>
Una de las consideraciones más importantes es que las páginas web que
escribamos deben funcionar incluso si el navegador no soporta JavaScript.
Hola ExpertoJavaUA
Por lo tanto, sabiendo que podemos incluir el código JavaScript dentro del código HTML, ya
estamos listos para nuestro saludo:
<!DOCTYPE html>
<html lang="es">
<head>
<title>Hola ExpertoJavaUA</title>
<meta charset="utf-8" />
<script>
console.log("Hola ExpertoJavaUA desde la consola");
alert("Hola ExpertoJavaUA desde alert");
</script>
</head>
<body></body>
</html>
Podéis observar que hemos utilizado la instrucción console.log para mostrar el saludo,
pero que realmente, al probarlo en el navegador sólo aparece el mensaje con alert . Para
9
JavaScript
visualizar los mensajes que pasamos por la consola necesitamos utilizar las DevTools que
veremos a continuación.
1.3. Herramientas
En el curso vamos a utilizar Intellij IDEA (http://www.jetbrains.com/idea/) para editar nuestros
archivos JavaScript, aunque cualquier IDE como Netbeans cumplen de sobra con su propósito.
Para hacer pequeñas pruebas, el hecho de tener que crear un documento HTML que enlace
a un documento JavaScript se puede hacer tedioso. Para ello, existen diferentes "parques"
donde jugar con nuestro código. Dos de los más conocidos son JSBin (http://jsbin.com) y
JSFiddle (http://jsfiddle.net).
Ambos nos permiten probar código en caliente, e interactuar con código HTML, CSS y usar
librerías de terceros como jQuery.
10
JavaScript
Dev Tools
Y por último, aunque cada vez menos necesarias como herramientas de terceros, tenemos las
herramientas de depuración de código. Los navegadores actuales incluyen las herramientas
para el desarrollador que permiten interactuar con la página que se ha cargado permitiendo
tanto la edición del código JavaScript como el estilo de la página, visualizar la consola con los
mensajes y errores mostrados, evaluar y auditar su rendimiento, así como depurar el código
que se ejecuta. Dentro de este apartado tenemos Firebug (http://getfirebug.com) como una
extensión de Firefox, y las herramientas que integran los navegadores:
A continuación vamos a centrarnos en las Chrome Developer Tools. Nada más abrirlas, ya sea
con el botón derecho e Inspeccionar Elemento, mediante el menú de Ver → Opciones para
desarrolladores → Herramientas para Desarrolladores o mediante F12, Ctrl+Shift+I o en Mac
Command+Opt+I, aparece activa la pestaña de Elements.
Figura 3. DevTools
Se puede observar que la pestaña Elements divide la pantalla en varios bloques:
11
JavaScript
• CSS, a la derecha, con los estilos (styles) estáticos y los calculados, así como los eventos
del elemento seleccionado en el bloque HTML. Además de poder ver qué reglas están
activas, podemos habilitar o deshabilitar propiedades, editar las reglas de las pseudo-
clases ( active , hover , etc…) y acceder al fuente de un determinado estilo.
• Consola, en la parte inferior, donde veremos los mensajes y errores de la página.
Otra pestaña con la que vamos a trabajar es la de fuentes (Sources), desde la cual podemos
editar el código fuente de nuestros archivos JS, CSS o HTML (desde la caché local de
Chrome). Las DevTools almacenan un histórico de nuestros cambios (Local Modifications)
desde donde podemos revertir un cambio a un punto anterior. Una vez tenemos la versión
definitiva, podemos guardar los cambios en el fuente de nuestro proyecto mediante Save As.
Console API
En los apuntes vamos a usar el objeto console para mostrar resultados (también podríamos
usar alert pero es más molesto). Este objeto no es parte del lenguaje pero sí del entorno y
está presente en la mayoría de los navegadores, ya sea en las herramientas del desarrollador
o en el inspector web.
Así pues, dentro de la consola de las Dev-Tools podremos visualizar los mensajes que
enviemos a la consola así como ejecutar comandos JavaScript sobre el código ejecutado.
12
JavaScript
• log() : muestra por la consola todos los parámetros recibidos, ya sean cadenas u objetos.
Obtendremos:
Variables
Para declarar una variable, se utiliza la palabra clave var delante del nombre de variable.
13
JavaScript
Los nombres de las variables pueden empezar por minúsculas, mayúsculas, subrayado e
incluso con $ , pero no pueden empezar con números. Además el nombrado es sensible al
uso de las mayúsculas y minúsculas, por lo que no es lo mismo contador que Contador .
Otra característica de JavaScript es que podemos asignar valores a variables que no hemos
declarado:
var a = 3;
b = 5;
c = a + b; // 8
Al hacer esto, la variable pasa a tener un alcance global, y aunque sea posible, es mejor evitar
su uso. Por ello, esta característica queda en desuso si usamos el modo estricto de ES5:
"use strict";
var a = 3;
b = 5; // Uncaught ReferenceError: b is not defined
Un característica especial de JavaScript es la gestión que hace de las variables conocida como
hoisting (elevación). Este concepto permite tener múltiples declaraciones con var a lo largo
de un bloque, y todas ellas actuarán como si estuviesen declaradas al inicio del mismo:
var a = 3;
console.log(b); // undefined
var b = 5;
Por ello, una buena práctica que se considera como un patrón de diseño (Single Var) es
declarar las variables con una única instrucción var en la primera línea de cada bloque
(normalmente al inicio de cada función), lo que facilita su lectura:
var a = 3,
b = 5,
14
JavaScript
suma = a + b,
z;
Texto
JavaScript almacena las cadenas mediante UTF-16. Podemos crear cadenas tanto con
comillas dobles como simples. Podemos incluir comillas dobles dentro de una cadena creada
con comillas simples, así como comillas simples dentro de una cadena creada con comillas
dobles.
Para concatenar cadenas se utiliza el operador + . Otra propiedad muy utilizada es length
para averiguar el tamaño de una cadena.
console.log(nombre);
console.log(typeof(nombre)); // "string"
console.log(nombre.toUpperCase());
console.log(nombre.toLowerCase());
console.log(nombre.charAt(0)); // "B"
console.log(nombre.charAt(-1)); // ""
console.log(nombre.indexOf("u")); // 2
console.log(nombre.lastIndexOf("ce")); // 3
console.log(nombre.lastIndexOf("Super")); // -1
console.log(nombre.substring(6)); // "Wayne"
console.log(nombre.substring(6,9)); // "Way"
15
JavaScript
Desde ECMAScript 5, las cadenas se tratan como arrays de sólo lectura, por lo cual podemos
acceder a los caracteres individuales mediante la notación de array:
s = "Hola Mundo";
s[0] // H
s[s.length-1] // o
Números
Todos los números en JavaScript se almacenan como números de punto flotante de 64 bits.
Para crear variables numéricas, le asignaremos el valor a la variable.
var pi = 3.14159265;
console.log(pi.toFixed(0)); // 3
console.log(pi.toFixed(2)); // 3.14
console.log(pi.toFixed(4)); // 3.1416
16
JavaScript
JavaScript emplea el valor NaN (que significa NotANumber) para indicar un valor numérico
no definido, por ejemplo, la división 0/0 o al parsear un texto que no coincide con ningún
número.
var numero1 = 0;
var numero2 = 0;
console.log(numero1/numero2); // NaN
console.log(parseInt("tres")); // NaN
Booleanos
Podemos guardar valores de true y false en las variables de tipo boolean .
Los operadores que trabajan con booleanos son la conjunción && , la disyunción || y la
negación ! . Además, tenemos el operador ternario (cond) ? valorVerdadero :
valorFalso .
Es decir, tanto los números distintos de cero, como los objetos y arrays devuelven verdadero.
En cambio, el cero, null y el valor undefined son falsos.
Operador ===
Aparte de los operadores básicos de comparación ( < , <= , > , >= ,
== , != ), JavaScript ofrece el operador identidad ( o igualdad estricta),
representado con tres iguales ( === )
17
JavaScript
Coerción de tipos
Cuando a un operador se le aplica un valor con un tipo de datos incorrecto, JavaScript
convertirá el valor al tipo que necesita, mediante un conjunto de reglas que puede que no sean
las que nosotros esperamos. A este comportamiento se le conoce como coerción de tipos.
console.log(8 * null) // 0
console.log("5" - 1) // 4
console.log("5" + 1) // 51
console.log("five" * 2) // NaN
console.log(false == 0) // true
Por ejemplo, el operador suma siempre intenta concatenar, con lo cual convierte de manera
automáticas los números a textos. En cambio, la resta realiza la operación matemática, con
lo que parsea el texto a número.
Fechas y horas
Para crear una fecha usaremos el objeto Date , ya sea con el constructor vacío de modo que
obtengamos la fecha actual, pasándole un valor que representa el timestamp Epoch (desde
el 1/1/70), o pasándole al constructor del día (1-31), mes (0-11) y año.
Si además queremos indicar la hora, lo podemos hacer mediante tres parámetros más:
Una vez tenemos un objeto Date , tenemos muchos métodos para realizar operaciones.
Algunos de los métodos más importantes son:
18
JavaScript
Autoevaluación
1
A partir del siguiente código ¿Qué saldrá por la consola del navegador?
console.log(incognita);
Para comparar fechas podemos usar los operadores < o > con el objeto Date , o
la comparación de igualdad/identidad con el método getTime() que nos devuelve el
timestamp.
Trabajar con fechas siempre es problemático, dado que el propio lenguaje no ofrece métodos
para realizar cálculos sobre fechas, o realizar consultas utilizando el lenguaje natural. Una
librería muy completa es Datejs (http://www.datejs.com/).
typeof
El operador typeof devuelve una cadena que identifica el tipo del operando. Así pues,
19
JavaScript
y en vez de recibir 'null' nos dice que es un objeto. Es decir, si le pasamos un array o null
el resultado es 'object' , lo cual es incorrecto.
1.5. Instrucciones
De manera similar a Java, tenemos los siguientes tipos:
A lo largo del curso usaremos las instrucciones tanto en los ejemplos como en los ejercicios
que realizaremos.
Punto y coma
En ocasiones, JavaScript permite omitir el punto y coma al final de una
sentencia, pero en otras no. Las reglas que definen cuando se puede omitir
el punto y coma son complejas, por lo que se recomienda poner siempre
el punto y coma.
1.6. Funciones
Las funciones en JavaScript son objetos, y como tales, se pueden usar como cualquier otro
valor. Las funciones pueden almacenarse en variables, objetos y arrays. Se pueden pasar
como argumentos a funciones, y una función a su vez puede devolver una función (ella misma
u otra). Además, como objetos que son, pueden tener métodos.
Pero lo que hace especial a una función respecto a otros tipos de objetos es que las funciones
pueden invocarse.
Las funciones se crean con la palabra clave function junto a los parámetros sin tipo
rodeados por una pareja de paréntesis. El nombre de la función es opcional.
20
JavaScript
Además, dentro de una función podemos invocar a otra función que definimos en el código
a posteriori, con lo que no tenemos ninguna restricción de declarar las funciones antes de
usarlas. Pese a no tener restricción, es una buena práctica de código que las funciones que
dependen de otras se coloquen tras ellas.
Por último, para devolver cualquier valor dentro de una función usaremos la instrucción
return , la cual es opcional. Si una función no hace return , el valor devuelto será
undefined .
Función declaración
Si al crear una función le asignamos un nombre se conoce como una función declaración.
Las variables declaradas dentro de la función no serán visibles desde fuera de la función.
Además, los parámetros se pasan por copia, y aquí vienen lo bueno, se pueden pasar
funciones como parámetro de una función, y una función puede devolver otra función.
Autoevaluación
A partir del código anterior, ¿Qué valor obtendríamos en la variables
2
epsilon ?
Función expresión
Otra característica de las funciones de JavaScript es que una función se considera un valor.
De este modo, podemos declarar una función anónima la cual no tiene nombre, y asignarla a
una variable (conocida como función expresión).
2
7
21
JavaScript
Y por último, también podemos crear funciones anónimas al llamar a una función, lo que añade
a JavaScript una gran flexibilidad:
Como veremos en posteriores sesiones, gran parte de las librerías hacen uso de las funciones
anónimas a la hora de invocar a las diferentes funciones que ofrecen.
Las funciones expresión se pueden invocar inmediatamente, lo que hace que sean muy útiles
cuando tenemos un bloque que vamos a utilizar una única vez.
(function() {
// instrucciones
})(); // invoca la función inmediatamente
cantar();
estribillo(); // TypeError: undefined
function cantar() {
console.log("¿Qué puedo hacer?");
}
22
JavaScript
Callbacks
Se conoce como callback a una función que se le pasa a otra función para ofrecerle a esta
segunda función un modo de volver a llamarnos más tarde.
Dicho de otro modo, al llamar a una función, le envío por parámetro otra función (un callback)
esperando que la función llamada se encargue de ejecutar esa función callback. Resumiendo,
los callbacks son funciones que contienen instrucciones que se invocarán cuando se complete
un determinado proceso.
function haceAlgo(miCallback) {
// hago algo y llamo al callback avisando que terminé
miCallback();
}
function ejemploCallback() {
console.log('he realizado algo');
}
Pero los callbacks no sólo se utilizan para llamar a algo cuando termina una acción.
Simplemente podemos tener distintos callbacks que se van llamando en determinados casos,
es decir, como puntos de control sobre una función para facilitar el seguimiento de un workflow.
La idea es disparar eventos en las funciones que llamaron “avisando” sobre lo que esta
sucendiendo:
function paso1(quePaso) {
console.log(quePaso);
}
function paso2(quePaso) {
console.log(quePaso);
}
23
JavaScript
function termino(queHizo) {
console.log(queHizo);
}
De esta forma creamos funciones nombradas fuera de la llamada y éstas a su vez podrían
disparar otros eventos (con callbacks) también.
Por último y no menos importante, los callbacks no son asíncronos, es decir, tras dispararse el
callback, se ejecuta todo el código contenido y el control vuelve a la línea que lo disparó. En el
ejemplo anterior dispara el callbackPaso1() y cuando este termina, continúa la ejecución
disparando el callbackPaso2() .
Esta duda se debe a que al tratar con elementos asíncronos, los callbacks se emplean para
gestionar el orden de ejecución de dichas tareas asíncronas.
arguments
Además de los parámetros declarados, cada función recibe dos parámetros adiciones: this
y arguments .
El parámetro adicional arguments nos da acceso a todos los argumentos recibidos mediante
la invocación de la función, incluso los argumentos que sobraron y no se asignaron a
parámetros. Esto nos permite escribir funciones que tratan un número indeterminado de
parámetros.
Estos datos se almacenan en una estructura similar a un array, aunque realmente no lo sea.
Pese a ello, si que tiene una propiedad length para obtener el número de parámetros y
podemos acceder a cada elemento mediante la notación arguments[x] , pero carece del
resto de métodos que si ofrecen los arrays (que veremos en la siguiente sesión).
Por ejemplo, podemos crear una función que sume un número indeterminado de parámetros:
arguments - http://jsbin.com/wikoha/2/edit?js,console
console.log(suma(1, 2, 3, 4, 5)); // 15
1.7. Alcance
Recordemos que el alcance determina desde donde se puede acceder a una variable, es
decir, donde nace y donde muere. Por un lado tenemos el alcance global, con el que cualquier
variable o función global pueden ser invocada o accedida desde cualquier parte del código de
la aplicación. En JavaScript, por defecto, todas las variables y funciones que definimos tienen
alcance global.
24
JavaScript
Si definimos una variable dentro de una función, el alcance se conoce como de función, de
modo que la variable vive mientras lo hace la función.
Aquella variable/función que definimos dentro de una función (padre) es local a la función pero
global para las funciones anidadas (hijas) a la que hemos definido la función (padre). Por esto,
más que alcance de función, se le conoce como alcance anidado.
Y así sucesivamente, podemos definir funciones dentro de funciones con alcance anidado en
el hijo que serán accesibles por el nieto, pero no por el padre.
funcionLocal();
console.log(varLocal);
};
// console.log(varLocal)
funcionGlobal(2);
Autoevaluación
3
A partir del código anterior, ¿Qué mensajes saldrán por pantalla? ¿Y si
4
descomentamos la linea de console.log(varLocal) ?
Si queremos evitar tener colisiones con las variables locales, podemos usar una función
expresión con invocación inmediata (IIFE). Para ello, tenemos que englobar el código a
proteger dentro de una función, la cual se rodea con paréntesis y se invoca, es decir, del
siguiente código:
(function() {
// código
})();
De este modo, las variables que declaremos dentro serán locales a ésta función, y permitirá
el uso de librería de terceros sin efectos colaterales con nuestras variables. Por lo tanto,
si intentamos acceder a una variable local de una IIFE (también conocidos como closures
anónimos) desde fuera obtendremos una excepción:
(function() {
var a = 1;
3
"¡Hola Mundo!", "2" y "Esta es una variable local con alcance anidado"
4
No saldría nada porque tendríamos un error de JavaScript al no ser visible varLocal
25
JavaScript
console.log(a); // 1
})();
console.log(a); // Uncaught ReferenceError: a is not defined
Autoevaluación
A partir del siguiente fragmento:
IIFE - http://jsbin.com/giziqo/1/edit?js,console
(function() {
var a = b = 5;
})();
console.log(b);
5
¿Qué valor aparecerá por la consola?
Hoisting
Al estudiar la declaración de variables, vimos el concepto de hoisting, el cual eleva las
declaraciones encontradas en un bloque a la primera línea. Ahora que ya conocemos como
funciona el alcance y las funciones lo estudiaremos en mayor profundidad.
"use strict";
var a = "global";
console.log(b); // undefined
var b = 5;
La variable b era una variable que no se había declarado y pese a ello, aún usando el modo
estricto, el intérprete no lanza ninguna excepción, porque la declaración se eleva al principio
del bloque (no así la asignación, que permanece en su lugar).
Ahora veremos que dentro de una función, al referenciar a una variable nombrada de manera
similar a una variable global, al hacer hoisting y declararla más tarde, la referencia apunta a
la variable local.
"use strict";
var a = "global";
console.log(b); // undefined
var b = 5;
console.log(b); // 5
function hoisting() {
console.log(b); // undefined
var b = 7;
console.log(b); // 7
5
5, porque a si que es local a la función, pero b es global. Si la ejecutásemos mediante el modo estricto,
obtendríamos un error de referencia
26
JavaScript
}
hoisting();
"use strict";
var a = "global";
var b;
function hoisting() {
var b;
console.log(b); // undefined
b = 7;
console.log(b); // 7
}
console.log(b); // undefined
b = 5;
console.log(b); // 5
hoisting();
Autoevaluación
6
¿Cual es el resultado de ejecutar el siguiente fragmento y por qué?:
function prueba() {
console.log(a);
console.log(hola());
var a = 1;
function hola() {
return 2;
}
}
prueba();
1.8. Timers
JavaScript permite la invocación de funciones tras un lapso de tiempo y con repeticiones
infinitas.
Para ejecutar una acción tras una duración determinada indicada en milisegundos tenemos
la función setTimeout(funcion, tiempoMS) . La llamada a setTimeout no bloquea
la ejecución del resto de código.
(function() {
6
undefined y 2 , ya que tanto la variable como la función hacen hoisting. La variable se declara pero no toma
valor hasta la asignación, en cambio, la función si que se declara y se puede invocar.
27
JavaScript
(function() {
var velocidad = 2000,
miFuncion = function() {
console.log("Batman vuelve");
setTimeout(miFuncion, velocidad);
};
var timer = setTimeout(miFuncion, velocidad);
// cancelTimer(timer);
}());
Al ejecutar el código, tras 2 segundos, saldrá el mensaje por consola, y con cada iteración que
se repetirá cada 2 segundos, se incrementará el contador mostrado por las Developer Tools.
Aunque si lo que queremos es realizar una llamada a una función de manera repetida
con un intervalo determinado, podemos hacer uso de la función setInterval(funcion,
intervalo) , la cual repetirá la ejecución de la función indicada.
(function() {
miFuncion = function() {
console.log("Batman vuelve");
};
var timer = setInterval(miFuncion, 2000);
}());
28
JavaScript
Capturando excepciones
¿Qué sucede cuando utilizamos a una variable que no esta declarada?
var a = 4;
a = b + 2;
console.log("Después del error"); // No se ejecuta nunca
Ya hemos visto que cuando se lanza un error, aparece por consola un mensaje informativo. Si
queremos ver el código que ha provocado la excepción, en la consola podemos desplegar el
mensaje de la excepción, y a su derecha podremos pulsar sobre el archivo:linea y nos
mostrará la línea en cuestión que ha provocado el fallo en el panel de Sources:
Para evitar estos errores, JavaScript ofrece un mecanismo try / catch / finally similar
al que ofrece Java. Al capturar una excepción, podemos acceder a la propiedad message
de la excepción para averiguar el error.
try-catch-finally - http://jsbin.com/kenufe/2/edit?js,console
try {
var a = 4;
a = b + 2;
console.log("Después del error");
} catch(err) {
console.error("Error en try/catch " + err.message);
} finally {
console.log("Pase lo que pase, llegamos al finally");
}
29
JavaScript
Lanzando excepciones
Si queremos que nuestro código lance una excepción, usaremos la instrucción throw ,
pudiendo bien lanzar una cadena de texto que será el mensaje de error, o bien crear un objeto
Error .
function ecuacion2grado(a,b,c) {
var aux = b*b-4*a*c;
if (aux < 0) {
throw "Raíz Negativa";
// throw new Error("Raíz Negativa");
}
// resto del código
}
Debug
Ya vimos en el apartado de las ??? como podemos introducir breakpoints para depurar el
código o parar la ejecución del código cuando se produce una excepción.
function funcionQueDaProblemas() {
debugger;
// código que funciona de manera aleatoria
}
Errores comunes
A la hora de escribir código JavaScript, los errores más comunes son:
30
JavaScript
Muchos de estos errores se pueden evitar haciendo uso del modo estricto de ECMAScript 5.
1.10. Ejercicios
Todos los ejercicios del módulo deben estar en modo estricto ( "use strict" ) y cumplir la
nomeclatura de nombrado de archivos.
function toCani(cadena) {}
La función recibirá dos parámetros, con los minutos y los segundos, pero en el caso que sólo le
pasemos un parámetro, considerará que son los segundos desde donde comenzará la cuenta
atrás.
Por ejemplo:
Si alguno de los valores que recibe como parámetros son negativos o de un tipo inesperado,
la función debe lanzar una excepción informando del problema.
31
JavaScript
32
JavaScript
Los tipos de datos primitivos son tipos de datos Number , String o Boolean . Realmente
no son objetos, y aunque tengan métodos y propiedades, son inmutables.
En cambio, los objetos en JavaScript son colecciones de claves mutables. En JavaScript, los
arrays son objetos, las funciones son objetos ( Function ), las fechas son objetos ( Date ),
las expresiones regulares son objetos ( RegExp ) y los objetos, objetos son ( Object ).
Propiedades
Un objeto es un contenedor de propiedades (cada propiedad tiene un nombre y un valor), y
por tanto, son útiles para coleccionar y organizar datos. Los objetos pueden contener otros
objetos, lo que permite estructuras de grafo o árbol.
Para añadir propiedades a un objeto no tenemos más que asignarle un valor utilizando el
operador . para indicar que la propiedad forma parte del objeto.
33
JavaScript
Métodos
Para crear un método, podemos asignar una función anónima a una propiedad, y al formar
parte del objeto, la variable this referencia al objetoen cuestión (y no a la variable global
como sucede con las funciones declaración/expresión).
persona.getNombreCompleto = function() {
return this.nombre + " " + this.apellido1;
}
console.log( persona.getNombreCompleto() );
La propiedad y el valor se separan con dos puntos, y cada una de las propiedades con
una coma, de forma similar a JSON.
un método es una propiedad de tipo function
La notación de corchetes es muy útil para acceder a una propiedades cuya clave está
almacenada en una variable que contiene una cadena con el nombre de la misma.
34
JavaScript
Objetos anidados
Los objetos anidados son muy útiles para organizar la información y representar relaciones
contiene con cardinalidades 1 a 1, o 1 a muchos:
var cliente = {
nombre: "Bruce Wayne",
email: "bruce@wayne.com",
direccion: {
calle: "Mountain Drive",
num: 1007,
ciudad: "Gotham"
}
};
Un fallo muy común es acceder a un campo de una propiedad que no existe. El siguiente
fragmento fallará, ya que el objeto direccion no está definido.
En resumen, un objeto puede contener otros objetos como propiedades. Por ello,
podemos ver código del tipo variable.objeto.objeto.objeto.propiedad o
35
JavaScript
Función factoría
Para evitarlo, y que los objetos compartan un interfaz común, podemos crear una función
factoría que devuelva el objeto.
36
JavaScript
Función constructor
Otra manera más elegante y eficiente es utilizar es una función constructor haciendo uso de la
instrucción new , del mismo modo que creamos una fecha con var fecha = new Date() .
Para ello, dentro de la función constructor que nombraremos con la primera letra en mayúscula
(convención de código), crearemos las propiedades y los métodos mediante funciones y los
asignaremos (ya no usamos la notación JSON) a propiedades de la función, tal que así:
this.getNombreCompleto = function() {
return this.nombre + " " + this.apellido1;
};
this.saluda = function(persona) {
if (persona instanceof Persona) {
return "Hola " + persona.getNombreCompleto();
} else {
return "Hola colega";
}
};
};
instanceof
Mediante el operador instanceof podemos determinar ( true o
false ) si un objeto es una instancia de una determinada función
constructor.
Hay que tener en cuenta que devolverá true cada vez que le
preguntemos si un objeto es una instancia de Object , ya que todos los
objetos heredan del constructor Object() .
37
JavaScript
Una vez creada la función, la invocaremos mediante la instrucción new , la cual crea una
nueva instancia del objeto:
También podíamos haber comenzado con una sintaxis similar a Java, es decir, en vez de
una función expresión, mediante una función declaración. Con lo que sustituiríamos la primera
línea por:
No olvides new
Mucho cuidado con olvidar la palabra clave new , porque de ese modo no
estaríamos creando un objeto, sino asignando las propiedades y métodos
del objeto al objeto window , lo que no nos daría error, pero no haría lo
que esperamos.
this.nombre = nombre;
this.apellido1 = apellido1;
// continua con el resto del código
Los desarrolladores que vienen (¿venimos?) del mundo de Java preferimos el uso de funciones
constructor, aunque realmente al usar una función constructor estamos consiguiendo lo mismo
que una función factoría, pero con la variable this siempre referenciando al objeto y
no con un comportamiento dinámico como veremos más adelante. Más información: http://
ericleads.com/2013/01/javascript-constructor-functions-vs-factory-functions/
38
JavaScript
En ambos casos, cada vez que creamos un objeto, los métodos vuelven a crearse y ocupan
memoria. Así al crear dos personas, los métodos de getNombreCompleto y saluda se
crean dos veces cada uno. Para solucionar esto, tenemos que usar la propiedad prototype
que veremos más adelante.
var heroe = {
nombre: "Superheroe",
saludar: function() {
return "Hola " + this.nombre;
}
};
Los métodos apply y call son similares, con la diferencia de que mientras apply además
admite un segundo parámetro con un array de argumentos que pasar a la función invocada,
call admite un número ilimitado de parámetros que se pasarán a la función.
var heroe = {
nombre: "Superheroe",
saludar: function() {
return "Hola " + this.nombre;
},
despedirse: function(enemigo1, enemigo2) {
var malos = enemigo2 ? (enemigo1 + " y " + enemigo2) : enemigo1;
return "Adios " + malos + ", firmado:" + this.nombre;
}
};
Una tercera aproximación es usar bind , que funciona de manera similar a las anteriores, pero
en vez de realizar la llamada a la función, devuelve una función con el contexto modificado.
39
JavaScript
Antes de enamorarse de bind , conviene destacar que forma parte de ECMAScript 5, por
lo que los navegadores antiguos no lo soportan. Se emplea sobre todo cuando usamos un
callback y en vez de guardar una referencia a this en una variable auxiliar (normalmente
nombrada como that ), hacemos uso de bind para pasarle this al callback.
function callback(datos){
this.procesar(datos);
}
ajax(callback.bind(this));
Si queremos que nuestro objeto contenga propiedades privadas, sólo hay que declararlas
como variables dentro del objeto:
• un descriptor de datos para las propiedades que tienen un valor, el cual puede ser de
sólo lectura, mediante Object.defineProperties
• o haciendo uso de los descriptores de acceso que definen dos funciones, para los
métodos get y set .
40
JavaScript
Definiendo propiedades
Vamos a recuperar el ejemplo de la función factoría que creaba una persona:
Object.defineProperty(persona, "nombre", {
value: nom,
writable: true
});
Object.defineProperty(persona, "apellido1", {
value: ape1,
writable: false
});
return persona;
}
De este modo, podemos crear personas, en las cuales podremos modificar el nombre pero
no el apellido:
41
JavaScript
Object.defineProperties(persona, {
nombre: {
value: nom,
writable: true
},
apellido1: {
value: ape1,
writable: false
}
});
return persona;
}
Si en vez de utilizar una función factoría, queremos hacerlo mediante una función
constructor, el funcionamiento es el mismo, sólo que el objeto que le pasaremos a
Object.defineProperty() será this :
Object.defineProperties(this, {
apellido1: {
value: ape1,
writable: false
}
});
}
Get y Set
Los descriptores de acceso sustituyen a los métodos que modifican las propiedades. Para ello,
vamos a crear una propiedad nombreCompleto con sus respectivos métodos de acceso y
modificación:
42
JavaScript
Object.defineProperties(persona, {
nombre: {
value: nom,
writable: true
},
apellido1: {
value: ape1,
writable: false
},
nombreCompleto: {
get: function() {
return this.nombre + " " + this.apellido1;
},
set: function(valor) {
this.nombre = valor;
this.apellido1 = valor;
}
}
});
return persona;
}
Método de acceso
Método de modificación. Destacar que como hemos definido la propiedad 'apellido1' como
de sólo lectura, no va a cambiar su valor
De este modo podemos obtener la propiedad del descriptor de acceso como propiedad en
vez de como método:
43
JavaScript
O hacer uso del método Object.keys(objeto) , el cual nos devuelve las propiedades del
objeto en forma de array:
Object.defineProperties(persona, {
nombre: {
value: nom,
enumerable: true
},
apellido1: {
value: ape1,
enumerable: true
},
nombreCompleto: {
get: function() {
return this.nombre + " " + this.apellido1;
},
enumerable: false
}
});
return persona;
}
44
JavaScript
Object.defineProperties(persona, {
nombre: {
value: nom,
},
apellido1: {
value: ape1,
},
nombreCompleto: {
get: function() {
return this.nombre + " " + this.apellido1;
},
configurable: true
}
});
return persona;
}
Object.defineProperty(persona, "nombreCompleto", {
get: function() {
return this.apellido1 + ", " + this.nombre;
}
});
2.6. Prototipos
Los prototipos son una forma adecuada de definir tipos de objetos que permiten definir
propiedades y funcionalidades que se aplicarán a todas las instancias del objeto. Es decir, es
un objeto que se usa como fuente secundaria de las propiedades. Así pues, cuando un objeto
recibe un petición de una propiedad que no contiene, buscará la propiedad en su prototipo. Si
no lo encuentra, en el prototipo del prototipo, y así sucesivamente.
A nivel de código, todos los objetos contienen una propiedad prototype que inicialmente
referencia a un objeto vacío. Esta propiedad no sirve de mucho hasta que la función se usa
como un constructor.
Por defecto todos los objetos tienen como prototipo raíz Object.prototype , el cual ofrece
algunos métodos que comparten todos los métodos, como toString . Si queremos averiguar
el prototipo de un objeto podemos usar la función Object.getPrototypeOf(objeto) .
45
JavaScript
Constructores y prototype
Al llamar a una función mediante la instrucción new provoca que se invoque como un
constructor. El constructor asocia la variable this al objeto creado, y a menos que se indique,
la llamada devolverá este objeto.
Este objeto se conoce como una instancia de su constructor. Todos los constructores (de
hecho todas las funciones) automáticamente contienen la propiedad prototype que por
defecto referencia a un objeto vacío que deriva de Object.prototype .
Cada instancia creada con este constructor tendrá este objeto como su prototipo. Con lo
que para añadir nuevos métodos al constructor, hemos de añadirlos como propiedades del
prototipo.
Persona.prototype.getNombreCompleto = function() {
return this.nombre + " " + this.apellido1;
};
Persona.prototype.saluda = function(persona) {
if (persona instanceof Persona) {
return "Hola " + persona.getNombreCompleto();
} else {
return "Hola colega";
}
};
Una vez definido el prototipo de un objeto, las propiedades del prototipo se convierten en
propiedades de los objetos instanciados. Su propósito es similar al uso de clases dentro de un
lenguaje clásico orientado a objeto. De hecho, el uso de prototipos en JavaScript se plantea
para poder compartir código de manera similar al paradigma orientado a objetos.
46
JavaScript
prototype y __proto__
Supongamos el siguiente objeto vacío:
Los prototipos en JavaScript son especiales por lo siguiente: cuando le pedimos a JavaScript
que queremos invocar el método push de un objeto, o leer la propiedad x de otro objeto, el
motor primero buscará dentro de las propiedades del propio objeto. Si el motor JS no encuentra
lo que nosotros queremos, seguirá la referencia __proto__ y buscará el miembro en el
prototipo del objeto.
function Heroe(){
this.malvado = false;
this.getTipo = function() {
return this.malvado ? "Malo" : "Bueno";
};
}
Heroe.prototype.atacar = function() {
return this.malvado ? "Ataque con Joker" : "Ataque con Batman";
47
JavaScript
Ya hemos visto que cada objeto en JavaScript tiene una propiedad prototype . No hay
que confundir esta propiedad prototype con la propiedad __proto__ , ya que ni tienen
el mismo propósito ni apuntan al mismo objeto:
Para finalizar, relacionado con estos conceptos tenemos la instrucción new en JavaScript,
la cual realiza tres pasos:
2.7. Herencia
JavaScript es un lenguaje de herencia prototipada, lo que significa que un objeto puede heredar
directamente propiedades de otro objeto a partir de su prototipo, sin necesidad de crear clases.
Ya hemos visto que mediante la propiedad prototype podemos asociar atributos y métodos
al prototipo de nuestras funciones constructor.
this.nombreCompleto = function() {
48
JavaScript
this.saluda = function(persona) {
if (persona instanceof Persona) {
return "Hola " + persona.getNombreCompleto();
} else {
return "Hola colega";
}
};
};
Para mejorar el uso de la memoria y reducir la duplicidad de los métodos, hemos de llevar
los métodos al prototipo. Para ello, crearemos descriptores de acceso para las propiedades y
llevaremos los métodos al prototipo de la función constructor:
Refactor OO - http://jsbin.com/racexa/1/edit?js
Object.defineProperties(Persona.prototype, {
nombreCompleto: {
get: function() {
return this.nombre + " " + this.apellido1;
},
enumerable: true
}
});
Persona.prototype.saluda = function(persona) {
if (persona instanceof Persona) {
return "Hola " + persona.nombreCompleto;
} else {
return "Hola colega";
}
};
Object.defineProperties(Persona.prototype, {
49
JavaScript
nombreCompleto: {
get: function() {
return this.nombre + " " + this.apellido1;
},
enumerable: true
},
saluda: {
value: function(persona) {
if (persona instanceof Persona) {
return "Hola " + persona.nombreCompleto;
} else {
return "Hola colega";
}
},
enumerable: true
}
});
1. Heredar el constructor
2. Heredar el prototipo
Herencia de constructor
Si usamos funciones constructor, podemos realizar herencia de constructor para que el hijo
comparta las mismas propiedades que el padre. Para ello, el hijo debe realizar una llamada
al padre y definir sus propios atributos.
Por ejemplo, supongamos que queremos crear un objeto Empleado que se base en
Persona , pero añadiendo el campo cargo con el puesto laboral del empleado:
Llamamos al constructor del padre mediante call para que this tome el valor del hijo.
50
JavaScript
Herencia de prototipo
Una vez heredado el constructor, necesitamos heredar el prototipo para compartir los métodos
y si fuese el caso, sobrescribirlos.
Herencia - http://jsbin.com/ledavu/1/edit?js
Empleado.prototype = Object.create(Persona.prototype, {
saluda: { // sobreescribimos los métodos que queremos
value: function(persona) {
if (persona instanceof Persona) {
return Persona.prototype.saluda.call(this) + " (desde un
empleado)";
} else {
return "Hola trabajador";
}
},
writable: false,
enumerable: true
},
nombreCompleto: {
get: function() {
var desc
= Object.getOwnPropertyDescriptor(Persona.prototype, "nombreCompleto");
51
JavaScript
A la hora de invocar una función lo podemos hacer de cuatro maneras, las cuales se conocen
como el patrón invocación:
var obj = {
valor : 0,
incrementar: function(inc){
this.valor += inc;
}
};
obj.incrementar(3);
console.log(obj.valor); // 3
Los métodos que hacen uso de this para obtener el contexto del objeto se conocen como
métodos públicos.
function suma(a,b) {
console.log(a+b);
console.log(this);
}
suma(3,5);
8
Window {top: Window, window: Window, location: Location, ... }
Esto puede ser un problema, ya que cuando llamamos a una función dentro de otra, this
sigue referenciando al objeto global y si queremos acceder al this de la función padre
tenemos que almacenarlo previamente en una variable:
52
JavaScript
var obj = {
valor: 0,
incrementar: function(inc) {
var that = this;
function otraFuncion(unValor) {
that.valor += unValor;
}
otraFuncion(inc);
}
};
obj.incrementar(3);
console.log(obj.valor); // 3
var objBind = {
valor: 0,
incrementar: function(inc) {
function otraFuncion(unValor) {
this.valor += unValor;
}
otraFuncion.call(this, inc);
}
}
Al invocar a una función, le indicamos que toma la referencia this del objeto en vez
del global
Persona.prototype.getNombreCompleto = function(){
return this.nombre + " " + this.apellido1;
}
53
JavaScript
El método apply nos permite, además de construir un array de argumentos que usaremos
al invocar una función, elegir el valor que tendrá this , lo cual permite reescribir el valor de
this en tiempo de ejecución.
Para ello, apply recibe 2 parámetros, el primero es el valor para this y el segundo es un
array de parámetros.
Persona.prototype.getNombreCompleto = function(){
return this.nombre + " " + this.apellido1;
}
var otraPersona = {
nombre: "Rubén",
apellido1: "Inoto"
}
Así pues, el método apply realiza una llamada a una función pasándole tanto el objeto que
va a tomar el papel de this como un array con los parámetros que va a utilizar la función.
Autoevaluación - this
7
¿Cual es el resultado del siguiente fragmento de código?
http://jsbin.com/xuzuhu/1/edit?js
54
JavaScript
}
}
};
console.log(obj.prop.getNombre());
var test = obj.prop.getNombre;
console.log(test());
2.9. Arrays
Se trata de un tipo predefinido que, a diferencia de otros lenguajes, es un objeto. Del mismo
modo que los tipos básicos, lo podemos crear de la siguiente manera:
Podemos observar que en JavaScript los arrays pueden contener tipos diferentes, que el
primer elemento es el 0 y que podemos obtener su longitud mediante la propiedad length .
Igual que antes, aunque se pueden crear los arrays de este modo, realmente se crean e
inicializan con la notación de corchetes de JSON:
tresTipos[3] = 15;
tresTipos[tresTipos.length] = "Bruce";
var longitud2 = tresTipos.length; // 5
tresTipos[8] = "Wayne";
var longitud3 = tresTipos.length; // 9
var nada = tresTipos[7]; // undefined
Cabe destacar que si accedemos a un elemento que no contiene ningún dato obtendremos
undefined .
Autoevaluación
¿Sabes cual es el contenido del array tiposTres tras ejecutar todas las
8
instrucciones?
Por lo tanto, si añadimos elementos en posiciones mayores al tamaño del array, éste crecerá
con valores undefined hasta el elemento que añadamos.
Si en algún momento quisiéramos eliminar un elemento del array, hemos de usar el operador
delete sobre el elemento en cuestión.
8
[11, "hola", true, 15, "Bruce", undefined, undefined, undefined, "Wayne"]
55
JavaScript
delete tresTipos[1];
El problema es que delete deja el hueco, y por tanto, la longitud del array no se ve reducida,
asignándole undefined al elemento en cuestión.
Array de palabras
Al trabajar con una cadena de texto, una operación que se utiliza mucho es, dividirla en
trozos a partir de un separador. Al utilizar el método String.split(separador) ,
éste devolverá un array de cadenas con cada uno de los trozos.
Manipulación
Los arrays soportan los siguientes métodos para trabajar con elementos individuales:
Métodos Propósito
pop() Extrae y devuelve el último elemento del array
push(elemento) Añade el elemento en la última posición
shift() Extrae y devuelve el primer elemento del array
unshift(elemento) Añade el elemento en la primera posición
notas.push('Matrícula de Honor');
var matricula = notas.pop(); // "Matrícula de Honor"
var suspenso = notas.shift(); // "Suspenso"
notas.unshift('Suspendido');
console.log(notas);
Además, podemos usar los siguiente métodos que modifican los arrays en su conjunto:
Métodos Propósito
concat(array2[, Une dos o más arrays
…, arrayN])
Concatena las partes de un array en una cadena, indicándole como
join(separador)
parámetro el separador a utilizar
reverse() Invierte el orden de los elementos del array, mutando el array
sort() Ordena los elementos del array alfabéticamente, mutando el array
56
JavaScript
Métodos Propósito
sort(fcomparacion) Ordena los elementos del array mediante la función fcomparacion
Devuelve un nuevo array con una copia con los elementos
slice(inicio, fin)
comprendidos entre inicio y fin (con índice 0 , y sin incluir fin).
Modifica el contenido del array, añadiendo nuevos elementos
splice(índice,
mientras elimina los antiguos seleccionando a partir de índice la
cantidad, elem1[,
cantidad de elementos indicados. Si cantidad es 0, sólo inserta los
…, elemN])
nuevos elementos.
Hay que tener en cuenta que los métodos mutables modifican el array sobre el que se realiza
la operación:
notas.reverse();
console.log(notas); // ["Sobresaliente", "Notable", "Bien", "Aprobado",
"Suspenso"]
notas.sort();
console.log(notas); // ["Aprobado", "Bien", "Notable", "Sobresaliente",
"Suspenso"]
notas.splice(0, 4, "Apto");
console.log(notas); // ["Apto", "Suspenso"]
Autoevaluación
9
¿Cual es el valor de la variables iguales ?
Una operación muy usual es querer ordenar un array de objetos por un determinado campo
del objeto. Supongamos que tenemos los siguientes datos:
var personas = [
9
false , porque tras darle la vuelta los valores son ["Laura, Ana, Pedro, Antonio, Juan"] y al
ordenarlos quedan como ["Ana, Antonio, Juan, Laura, Pedro"] , por lo que a alfa se asigna el valor
Antonio , mientras que nombres[2] queda como Juan
57
JavaScript
{nombre:"Aitor", apellido1:"Medrano"},
{nombre:"Domingo", apellido1:"Gallardo"},
{nombre:"Alejandro", apellido1:"Such"}
];
personas.sort(function(a,b) {
if (a.nombre < b.nombre)
return -1;
if (a.nombre > b.nombre)
return 1;
return 0;
});
function compare(a, b) {
if (a es menor que b según criterio de ordenamiento) {
return -1;
}
if (a es mayor que b según criterio de ordenamiento) {
return 1;
}
// a debe ser igual b
return 0;
}
Finalmente, vamos a estudiar los métodos slice y splice que son menos comunes.
Crea un nuevo array con los elementos comprendidos entre el tercero y el quinto,
quedando ["uva", "fresa"]
Tras borrar dos elementos a partir de la posición tres, añade piña . En uvaFresa
se almacena un array con los elementos eliminados ( ["uva", "fresa"] ), mientras
que frutas se queda con ["naranja", "pera", "manzana", "piña",
"naranja"] .
Si lo que queremos es buscar un determinado elemento dentro de un array:
Método Propósito
indexOf(elem[, Devuelve la primera posición (0..n-1) del elemento comenzando
inicio]) desde el principio o desde inicio
lastIndexOf(elem[, Igual que indexOf pero buscando desde el final hasta el principio
inicio])
Autoevaluación
¿Cual es el valor de la variable ultima ?
58
JavaScript
var frutas =
["naranja", "pera", "manzana", "uva", "fresa", "naranja"];
var primera = frutas.indexOf("naranja");
var ultima = frutas.lastIndexOf("naranja");
Iteración
Los siguiente métodos aceptan una función callback como primer argumento e invocan dicha
función para cada elemento del array. La función que le pasamos a los métodos reciben tres
parámetros:
Método Propósito
forEach(función) Ejecuta la función para cada elemento del array
Ejecuta la función para cada elemento del array, y el nuevo valor se
map(función)
inserta como un elemento del nuevo array que devuelve.
Verdadero si la función se cumple para todos los valores. Falso en
every(función)
caso contrario (Similar a una conjunción → Y)
Verdadero si la función se cumple para al menos un valor. Falso
some(función) si no se cumple para ninguno de los elementos (Similar a un
disyunción → O)
filter(función) Devuelve un nuevo array con los elementos que cumplen la función
Ejecuta la función para un acumulador y cada valor del array (de
reduce(función)
inicio a fin) se reduce a un único valor
Si queremos pasar a mayúsculas todos los elementos del array, podemos usar la función
map() , la cual se ejecuta para cada elemento del array:
O si queremos mostrar todos los elementos del array, podemos hacer uso del método
forEach :
59
JavaScript
Si queremos comprobar si todos los elementos de un array son cadenas, podemos utilizar el
método every() . Para ello, crearemos una función esCadena :
console.log(frutas.every(esCadena)); // true
Si tenemos un array con datos mezclados con textos y números, podemos quedarnos con los
elementos que son cadenas mediante la función filter() .
Finalmente, mediante la función reduce podemos realizar un cálculo sobre los elementos
del array, por ejemplo, podemos contar cuantas veces aparece una ocurrencia dentro de un
array o sumar sus elementos. Para ello, la función recibe dos parámetros con el valor anterior
y el actual:
En el primer paso, como no hay valor anterior, se pasan el primer y el segundo elemento del
array (los valores 1 y 3). En siguientes iteraciones, el valor anterior es lo que devuelve el
código, y el actual es el siguiente elemento del array. De este modo, estamos cogiendo el
valor actual y sumándoselo al valor anterior (el total acumulado)
arguments a array
Si queremos convertir el pseudo-array arguments de una función a un
array verdadero para poder utilizar sus métodos, podemos hacer uso del
prototipo del array, para obtener una copia de los los argumentos:
60
JavaScript
2.10. Destructurar
Una novedad que ofrece ES6 es la posibilidad de destructurar tanto arrays como objetos para
transformar una estructura de datos compuesta, tales como objetos y arrays, en diferentes
datos individuales. Así pues, en vez de asignar una a una las asignaciones para cada
variable mediante múltiples sentencias, al destructurar podemos asignar los valores a múltiples
variables en una única sentencia.
En el caso de los arrays, hemos de asignar un array a las variables entre corchetes ( [] ):
De la misma manera, si queremos extraer los datos de un objeto, lo haremos mediante llaves
( {} ):
La sintaxis de destructurar también permite emplear su uso como parámetros de las funciones
declaración, ya sea un objeto o un array.
persona.saluda(persona2);
persona.saluda({nombre, apellido1});
Recibe un objeto como parámetro, y por tanto dentro de la función saluda accederá a los
elementos mediante persona2.nombre y persona2.apellido1
Recibe un objeto destructurado como parámetro, y por tanto dentro de la función saluda
accederá a los elementos mediante nombre y apellido1 directamente
Más información en https://developer.mozilla.org/es/docs/Web/JavaScript/Referencia/
Operadores/Destructuring_assignment
2.11. Ejercicios
(0.4 ptos) Ejercicio 21. Objeto Usuario
A partir del siguiente objeto el cual se crea mediante una función factoría:
61
JavaScript
Refactoriza el código utilizando una función constructor que haga uso de descriptores de datos
y acceso, de manera que no permita consultar el password una vez creado el objeto Usuario .
El método autenticar quedará como un método el objeto, es decir, no como una propiedad
get/set.
Al recuperar el nombre de una persona, si no tiene ninguno, debe devolver el login del usuario.
62
JavaScript
1. Función reverseCopia(array) que a partir de un array, devuelva una copia del mismo
pero en orden inverso (no se puede utilizar el método reverse )
2. Función union(array1, array2 [,…arrayN]) que a partir de un número variable
de arrays, devuelva un array con la unión de sus elementos.. Cada elemento sólo debe
aparecer una única vez.
Por ejemplo:
63
JavaScript
3. JavaScript y DOM
JavaScript funciona de la mano de las páginas web, y éstas se basan en el DOM y trabajan
sobre un navegador que interactúa con el BOM.
3.1. BOM
El BOM (Browser Object Model) define una serie de objetos que nos permiten interactuar con
el navegador, como son window , navigator y location .
Figura 8. BOM
Objeto window
El objeto window es el objeto raíz (global) del navegador.
Tras abrir una ventana mediante window.open() el método devolverá un nuevo objeto
window , el cual será el objeto global para el script que corre sobre dicha ventana,
conteniendo todas las propiedades comunes a los objetos como el constructor Object o
el objeto Math . Pero si intentamos mirar sus propiedades, la mayoría de los navegadores
no lo permitirán, ya que funciona a modo de sandbox.
Este modo de trabajo implica que el navegador sólo nos mostrará la información relativa al
mismo dominio, y si abrimos una página de un dominio diferente al nuestro no tendremos
control sobre las propiedades privadas del objeto window . Este caso lo estudiaremos en
la siguiente unidad y aprenderemos como solucionarlo.
• cerrar una ventana mediante window.close()
• mostrar mensajes de alerta, confirmación y consulta mediante
window.alert(mensaje) , window.confirm(mensaje) y
window.prompt(mensaje [,valorPorDefecto]) .
alert("uno");
confirm("dos");
64
JavaScript
Objeto navigator
Mediante el objeto navigator podemos acceder a propiedades de información del
navegador, tales como su nombre y versión. Para ello, podemos acceder a las propiedades
navigator.appName o navigator.appVersion .
Objeto document
Cada objeto window contiene la propiedad document , el cual contiene un objeto que
representa el documento mostrado. Este objeto, a su vez, contiene la propiedad location
que nos permite obtener información sobre la URL con las propiedades:
65
JavaScript
Por ejemplo, si queremos generar la hora actual, podemos usar el objeto Date y mediante
document.write() escribirla:
<html>
<head><title>La hora</title></head>
<body>
<p>Son las
<script type="text/javascript">
var time = new Date();
document.write(time.getHours() + ":" + time.getMinutes());
</script>
</p>
</body>
</html>
El API DOM permite interactuar con el documento HTML y cambiar el contenido y la estructura
del mismo, los estilos CSS y gestionar los eventos mediante listeners.
• DOM Level 0 (Legacy DOM): define las colecciones forms , links e images .
• DOM Level 1 (1998): introduce el objeto Node y a partir de él, los nodos Document ,
Element , Attr y Text . Además, las operaciones getElemensByTagName ,
getAttribute , removeAtribute y setAttribute
• DOM Level 2: facilita el trabajo con XHTML y añade los métodos getElementById ,
hasAttributes y hasAttribute
• DOM Level 3: añade atributos al modelo, entre ellos textContent y el método
isEqualNode .
• DOM Level 4 (2004): supone el abandono de HTML por XML e introduce los métodos
getElementsByClassName , prepend , append , before , after , replace y
remove .
El objeto window
Tal como vimos en la primera sesión, al declarar una variable tiene un alcance global.
Realmente, todas las variable globales forman parte del objeto window , ya que éste objeto
es el padre de la jerarquía de objetos del navegador (DOM, Document Object Model).
66
JavaScript
Por lo general, casi nunca vamos a referenciar directamente al objeto window . Una de las
excepciones es cuando desde una función declaramos una variable local a la función (con
alcance anidado) que tiene el mismo nombre que una variable global y queremos referenciar
a la global de manera únivoca, utilizaremos el objeto window .
if (window.superheroe != superheroe) {
superheroe = window.superheroe;
}
}
Queda claro que sería más conveniente utilizar otro nombre para la variable local, pero ahí
queda un ejemplo de su uso.
El objeto document
El objeto document nos da acceso al DOM (Modelo de objetos de documento) de una página
web, y a partir de él, acceder a los elementos que forman la página mediante una estructura
jerárquica.
Al objeto que hace de raíz del árbol, el nodo html , se puede acceder mediante la propiedad
document.documentElement . Sin embargo, la mayoría de ocasiones necesitaremos
acceder al elemento body más que a la raíz, con lo que usaremos document.body .
Cada porción de este fragmento se convierte en un nodo DOM con punteros a otros nodos
que apuntan sus nodos relativos (padres, hijos, hermanos, …), del siguiente modo:
67
JavaScript
Enlaces de nodos
Podemos acceder a los enlaces existentes entre los nodos mediante las propiedades que
poseen los nodos. Cada objeto DOM contiene un conjunto de propiedades para acceder a
los nodos con lo que mantiene alguna relación. Por ejemplo, cada nodo tiene una propiedad
parentNode que referencia a su padre (si tiene). Estos padres, a su vez, contienen enlaces
que devuelven la referencia a sus hijos, pero como puede tener más de un hijo, se almacenan
en un pseudoarray denominado childNodes .
<!DOCTYPE html>
<html lang="es">
<head>
<title>Ejemplo DOM</title>
<meta charset="utf-8" />
</head>
<body>
<h1>Encabezado uno</h1>
<p>Primer párrafo</p>
<p>Segundo párrafo</p>
<div><p id="tres">Tercer párrafo dentro de un div</p></div>
<script src="dom.js" charset="utf-8"></script>
</body>
</html>
68
JavaScript
Por ejemplo:
<div>
69
JavaScript
<p id="tres">
Tercer párrafo dentro de un div
</p>
</div>
Así pues, si quisiéramos crear una función que averiguase si es un nodo de texto:
function esNodoTexto(nodo) {
return nodo.nodeType == document.TEXT_NODE; // 3
}
esNodoTexto(document.body); // false
esNodoTexto(document.body.firstChild.firstChild); // true
Realmente existen 12 tipos, pero estos tres son los más importantes. Por
ejemplo, el objeto document es el tipo 9.
Los elementos contienen la propiedad nodeName que indica el tipo de etiqueta HTML que
representa (siempre en mayúsculas). Los nodos de texto, en cambio, contienen nodeValue
que obtiene el texto contenido.
document.body.firstChild.nodeName; // H1
document.body.firstChild.firstChild.nodeValue; // Encabezado uno
Recorriendo el DOM
Si queremos recorrer todos los nodos de un árbol, lo mejor es realizar un recorrido recursivo.
Por ejemplo, si queremos crear una función que indique si un nodo (o sus hijos) contiene una
determinada cadena:
70
JavaScript
Seleccionando elementos
Mediante el DOM, podemos usar dos métodos para seleccionar un determinado elemento.
(function() {
var pElements = document.getElementsByTagName("p"); // NodeList
console.log(pElements.length); // 3
console.log(pElements[0]); // Primer párrafo
querySelector
El método de getElementsByTagName es antiguo y no se suele utilizar. En el año 2013 se
definió el Selector API, que define los métodos querySelectorAll y querySelector ,
los cuales permiten obtener elementos mediantes consultas CSS, las cuales ofrecen mayor
flexibilidad:
Esta manera de acceder a los elementos es la misma que usa jQuery, por lo que la
estudiaremos en la sesión correspondiente.
Conviene citar que getElementById es casi 5 veces más rápido que querySelector .
Más información en: http://jsperf.com/getelementbyid-vs-queryselector
71
JavaScript
(function() {
var elem = document.createElement("p"),
texto = "<strong>Nuevo párrafo creado dinámicamente</strong>",
contenido = document.createTextNode(texto);
elem.appendChild(contenido);
elem.id = "conAppendChild";
document.body.appendChild(elem);
// lo añade como el último nodo detrás de script
}());
Si probamos el código veremos que las etiquetas <strong> se han parseado y en vez de
mostrar el texto en negrita se muestra el código de la etiqueta. Además, si intentamos ver el
código fuente de la página no veremos el contenido creado dinámicamente, y necesitaremos
utilizar las herramientas de desarrollador que ofrecen los navegadores web.
insertAfter
Aunque parezca mentira, no existe ningún método para añadir después,
pero podríamos crearlo fácilmente de la siguiente manera:
72
JavaScript
e.parentNode.insertBefore(i, e.nextSibling);
} else {
e.parentNode.appendChild(i);
}
}
Así pues, si queremos añadir el párrafo dentro de la capa que tenemos definida en vez de al
final del documento, podemos obtener el nodo que contiene el párrafo de la capa, y añadir el
nuevo nodo a su padre en la posición que deseemos (al final, antes del nodo o sustituirlo).
(function() {
var doc = document,
elem = doc.createElement("p"),
contenido = doc.createTextNode("<strong>Nuevo párrafo creado
dinámicamente</strong>"),
pTres = doc.getElementById("tres");
elem.appendChild(contenido);
elem.id = "conAppendChild";
pTres.parentNode.appendChild(elem);
}());
Guardamos en una variable la referencia a document para evitar tener que salir del
alcance y subir al alcance global con cada referencia. Se trata de una pequeño mejora
que aumenta la eficiencia del código.
Obtenemos una referencia al Node que contiene el párrafo de dentro del div
Insertamos un hijo al padre de #tres , lo que lo convierte en su
hermano. Si hubiesemos querido que se hubiese colocado delante, tendríamos
que haber utilizado pTres.parentNode.insertBefore(elem, pTres) .
En cambio, para sustituir un párrafo por el nuevo necesitaríamos hacer
pTres.parentNode.replaceChild(elem, pTres) ;
Otra forma de añadir contenido es mediante la propiedad innerHTML , el cual sí que va a
parsear el código incluido. Para ello, en vez de crear un elemento y añadirle contenido, el
contenido se lo podemos añadir como una propiedad del elemento.
(function() {
var
doc = document,
elem = doc.createElement("p"),
pTres = doc.getElementById("tres");
73
JavaScript
pTres.parentNode.replaceChild(elem, pTres);
}());
innerHTML vs nodeValue
Mientras que innerHTML interpreta la cadena como HTML,
nodeValue la interpreta como texto plano, con lo que los símbolos de
< y > no aportan significado al contenido
document.write()
Pese a que el método document.write(txt) permite añadir
contenido a un documento, hemos de tener mucho cuidado porque, pese
a funcionar si lo incluimos al cargar una página, al ejecutarlo dentro de
una función una vez el DOM ya ha cargado, el texto que queramos
escribir sustituirá todo el contenido que había previamente. Por lo tanto,
si queremos añadir contenido, es mejor hacerlo añadiendo un nodo o
mediante innerHtml
getElementsBy vs querySelector
Una diferencia importante es que las referencias con getElementsBy* están vivas y
siempre contienen el estado actual del documento, mientras que con querySelector*
obtenemos las referencias existentes en el momento de ejecución, sin que cambios posteriores
en el DOM afecten a las referencias obtenidas.
(function() {
var
getElements = document.getElementsByTagName("p"),
queryElements = document.querySelectorAll("p");
74
JavaScript
Gestionando atributos
Una vez hemos recuperado un nodo, podemos acceder a sus atributos
mediante el método getAttribute(nombreAtributo) y modificarlo mediante
setAttribute(nombreAtributo, valorAtributo) .
(function() {
var pTres = document.getElementById("tres");
pTres.setAttribute("align","right");
}());
También podemos acceder a los atributos como propiedades de los elementos, con lo que
podemos hacer lo mismo del siguiente modo:
(function() {
var pTres = document.getElementById("tres");
pTres.align = "right";
}());
La gestión de los atributos están en desuso en favor de las hojas de estilo para dotar a las
páginas de un comportamiento predefinido y desacoplado en su archivo correspondiente.
75
JavaScript
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="utf-8" />
<title></title>
<style>
#batman { }
.css-class {
color: blue;
border : 1px solid black;
}
</style>
</head>
<body>
<div style="font-size:xx-large" id="batman">Batman siempre gana.</div>
<script src="css.js"></script>
</body>
</html>
La propiedad style de los elementos nos permiten obtener/modificar los estilos. Por
ejemplo, si queremos cambiar mediante JavaScript el color del texto del párrafo a azul y
añadirle un borde negro, haríamos lo siguiente:
style - http://jsbin.com/qitume/1/edit?html,js,output
(function() {
var divBatman = document.getElementById("batman");
divBatman.style.color = "blue";
divBatman.style.border = "1px solid black";
}());
(function() {
var divBatman = document.getElementById("batman");
76
JavaScript
divBatman.className = "css-class";
// divBatman.className = ""; -> elimina la clase CSS
}());
Si queremos añadir más de una clase, podemos separarlas con espacios o utilizar la propiedad
classList (en navegadores actuales - http://caniuse.com/classlist ) que permite añadir
clases mediante el método add .
(function() {
var divBatman = document.getElementById("batman");
divBatman.classList.add("css-class");
divBatman.classList.add("css-class2");
}());
Otros métodos útiles de classList son remove para eliminar una clase, toggle para
cambiar una clase por otra, length para averiguar la longitud de la lista de clases y
contains para averiguar si una clase existe dentro de la lista.
(function() {
var divBatman = document.getElementById("batman");
var color = window.getComputedStyle(divBatman,
null).getPropertyValue("color");
var colorIE = divBatman.currentStyle["color"];
}());
(function() {
var divBatman = document.getElementById("batman");
divBatman.style.display = "none"; // oculta
divBatman.style.display = ""; // visible
}());
3.4. Animaciones
Uno de las acciones más usadas en las web actual es el movimiento y la manipulación de
contenidos, de modo que hay texto que aparece/desaparece o cambia de lugar de manera
dinámica.
77
JavaScript
Por ejemplo, si lo que queremos es crear una animación, haremos llamadas sucesivas a una
función, pero con un límite de ejecuciones mediante el uso de ???.
(function() {
var velocidad = 2000,
i = 0;
miFuncion = function() {
console.log("Batman vuelve " + i);
i = i + 1;
if (i < 10) {
setTimeout(miFuncion, velocidad);
}
};
setTimeout(miFuncion, velocidad);
}());
(function() {
var velocidad = 2000,
i = 0;
miFuncion = function() {
console.log("Batman vuelve " + i);
i = i + 1;
if (i > 9) {
clearInterval(timer);
}
};
var timer = setTimeout(miFuncion, velocidad);
}());
Para ver un ejemplo de animación en movimiento, a partir de una capa con un cuadrado azul,
vamos a moverla horizontalmente de manera ininterrumpida.
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="utf-8" />
<title>Caja</title>
<style>
#caja {
position: absolute;
left: 50px;
top: 50px;
background-color: blue;
height: 100px;
width: 100px;
}
</style>
78
JavaScript
</head>
<body>
<div id="caja"></div>
<script src="caja.js"></script>
</body>
</html>
(function() {
var velocidad = 10,
mueveCaja = function(pasos) {
var el = document.getElementById("caja"),
izq = el.offsetLeft;
if ((pasos > 0 && izq > 399) || (pasos < 0 && izq < 51)) {
clearTimeout(timer);
timer = setInterval(function() {
mueveCaja(pasos * -1);
}, velocidad);
}
3.5. Eventos
Los eventos nos van a permitir asociar funciones a acciones que ocurren en el navegador tras
la interacción con el usuario, ya sea mediante el teclado o el ratón.
Gestionando eventos
Para comenzar con un ejemplo muy sencillo, vamos a basarnos en la página web que dibujaba
un cuadrado. A partir del evento de hacer click sobre el mismo, cambiaremos su color a rojo.
Para ello:
(function() {
var el = document.getElementById("caja");
79
JavaScript
el.onclick = function() {
this.style.backgroundColor = "red";
};
}());
Definimos una función que se ejecutará cada vez que se haga click sobre el elemento
Un error muy común es olvidar el ; tras la definición de la función
Podemos observar como hemos asociado una función a la propiedad onclick de un
elemento, mediante la técnica de elemento.evento. De este modo, cada vez que hagamos
click sobre el elemento caja se ejecutará dicha función anónima. Otros eventos que
podemos usar son onload , onmouseover , onblur , onfocus , etc…
Otra formar de asociar una acción al evento, aunque no se recomienda, es incrustar el código
JavaScript como parte de un atributo de etiqueta HTML, con el nombre del evento:
<button onclick="this.style.backgroundColor='red';">Incrustado</button>
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="utf-8" />
<title>Hola Eventos</title>
<style>
.normal {
background-color: white;
color: black;
}
.contrario {
background-color: black;
color: white;
}
</style>
</head>
<body class="normal">
<h1>Hola Eventos</h1>
<p><a href="http://es.wikipedia.org/wiki/Batman">Batman</a>
Forever</p>
<button>Normal</button>
<button>Contrario</button>
<script src="eventos.js"></script>
</body>
80
JavaScript
</html>
(function() {
var botones = document.getElementsByTagName("button");
Para cada uno de los botones, le añadimos una función anónima al evento onclick para
que le asigne al body la clase que coincide con el nombre del botón (en minúsculas).
Mucho cuidado con añadir una segunda función al mismo evento tal
cual está en la línea 9, ya que realmente estaremos sobreescribiendo (y
borrando) la función anterior.
Ante el mismo evento, un objeto puede tener solo un manejador, pero varios listeners.
Flujo de eventos
A la hora de propagarse un evento, existen dos posibilidades al definir el flujo de eventos para
saber cual es el elemento que va a responder al mismo:
• Con la captura de eventos, al pulsar sobre un elemento, se produce una evento de arriba
a abajo, desde el elemento window , pasando por <body> hasta llegar al elemento que
lo captura.
• En cambio, mediante el burbujeo de eventos (event bubbling), el evento se produce en el
elemento de más abajo y va subiendo hasta llegar al window .
<html onclick="procesaEvento()">
<head><title>Ejemplo de flujo de eventos</title></head>
<body onclick="procesaEvento()">
<div onclick="procesaEvento()">Pincha aqui</div>
</body>
</html>
Cuando se pulsa sobre el texto "Pincha aquí" que se encuentra dentro del <div> , si el
navegador sigue el modelo de event bubbling, se ejecutan los siguientes eventos en el orden
que muestra el siguiente esquema:
81
JavaScript
El modelo estándar
También conocido como eventos de DOM nivel 0, consisten en asignar una función al evento
de un elemento.
Este modelo se puede hacer utilizando el atributo on* de las etiquetas HTML, o de manera
programativa mediante el uso del método addEventListener(evento, función,
flujoEvento) .
El parámetro flujoEvento puede tomar los valores true para aplicar el modelo de captura
de eventos, o false para event bubbling (recomendado).
(function() {
var botonClick = function() {
82
JavaScript
Función que cambia la clase del body al valor del elemento con el que se invoque (en
nuestro caso, el nombre del botón)
A cada botón, se le registra al evento click el manejador de eventos de la función del paso
1, y con flujo de event bubbling.
Por último, sobre el evento podemos llamar al método preventDefault() para cancelar
el comportamiento por defecto que tenía asociado el elemento sobre el cual se ha registrado
el evento. Por ejemplo, si quisiéramos evitar que al pulsar sobre el enlace nos llevase a su
destino:
Delegación de eventos
Se basa en el flujo de eventos ofrecidos por event bubbling para delegar un evento desde un
elemento inferior en el DOM que va a subir como una burbuja hasta el exterior.
(function() {
83
JavaScript
document.addEventListener("click", function(evt) {
var tag = evt.target.tagName;
console.log("Click en " + tag);
if ("A" == tag) {
evt.preventDefault();
}
}, false);
})();
Tipos de eventos
Podemos dividir, a grosso modo, los eventos en los siguientes tipos:
• Evento de carga.
• Eventos de foco.
• Eventos de ratón.
• Eventos de teclado.
Evento de carga
Para poder asignar un listener a un elemento del DOM, éste debe haberse cargado, y de ahí
la conveniencia de incluir el código JavaScript al final de la página HTML, justo antes de cerrar
el body.
function preparandoManejadores() {
var miLogo = document.getElementById("logo");
miLogo.onclick() {
alert("Has venido al sitio adecuado.");
}
}
window.onload = function() {
preparandoManejadores();
}
Un fallo recurrente es incluir más de un manejador para el mismo evento onload . Si incluimos
diferentes archivos .js , únicamente se cargará el último manejador, lo que puede provocar
comportamientos no deseados.
Una característica a destacar es que el evento se lanzará una vez todo el contenido se ha
descargado, lo que incluye las imágenes de la página.
Eventos de foco
Cuando trabajamos con formularios, al clickar o acceder a un campo mediante el uso del
tabulador, éste obtiene el foco, lo que lanza el evento onfocus . Al cambiar a otro campo, el
elemento que tiene el foco lo pierde, y se lanza el evento onblur .
84
JavaScript
<form name="miForm">
Nombre: <input type="text" name="nombre" id="nom" tabindex="10" />
Apellidos: <input type="text" name="apellidos" id="ape" tabindex="20" />
</form>
campoNombre.onfocus = function() {
if ( campoNombre.value == "Escribe tu nombre") {
campoNombre.value = "";
}
};
campoNombre.onblur = function() {
if ( campoNombre.value == "") {
campoNombre.value = "Escribe tu nombre";
}
};
Eventos de ratón
Cuando el usuario hace click con el ratón, se generan tres eventos. Primero se genera
mousedown en el momento que se presiona el botón. Luego, mouseup cuando se suelta
el botón. Finalmente, se genera click para indicar que se ha clickado sobre un elemento.
Cuando esto sucede dos veces de manera consecutiva, se genera un evento dblclick
(doble click).
Cuando asociamos un manejador de eventos a un botón, lo normal es que sólo nos interese
si ha hecho click. En cambio, si asociamos el manejador a un nodo que tiene hijos, al hacer
click sobre los hijos el evento "burbujea" hacia arriba, por lo que nos interesará averiguar que
hijo ha sido el responsable, utilizando la propiedad target del evento.
Si nos interesa las coordenadas exactas del click, el objeto evento contiene la propiedades
clientX y clientY con los valores en pixeles del cursor en la pantalla. Como los
documentos pueden usar el scroll, en ocasiones, estas coordenadas no nos indica en que
parte del documento se encuentra el ratón. Algunos navegadores ofrecen las propiedades
pageX y pageY para este propósito, aunque otros no. Por suerte, la información respecto
a la cantidad de píxeles que el documento ha sido scrollado puede encontrarse en
document.body.scrollLeft y document.body.scrollTop .
Aunque también es posible averiguar que botón del ratón hemos pulsado mediante las
propiedades which y button del evento, es muy dependiente del navegador, por lo que
dejaremos su gestión a jQuery.
Si además de los clicks nos interesa el movimiento del ratón, el evento mousemove salta
cada vez que el ratón se mueve sobre un elemento. Además, los evento mouseover y
85
JavaScript
mouseout se lanzan al entrar o salir del elemento. Para estos dos eventos, la propiedad
target referencia el nodo que ha lanzado el evento, mientras que relatedTarget nos
indica el nodo de donde viene el ratón (para mouseover ) o adonde va (para mouseout )
Si el nodo tiene hijos, los eventos mouseover y mouseout pueden dar lugar a
malinterpretaciones. Los evento lanzado desde los hijos nodos burbujean hasta el elemento
padre, con lo que tendremos un evento mouseover cuando el ratón entre en uno de los hijos.
Para detectar (e ignorar) estos eventos, podemos usar la propiedad target :
miParrafo.addEventListener("mouseover", function(event) {
if (event.target == miParrafo)
console.log("El ratón ha entrado en mi párrafo");
}, false);
Eventos de teclado
Si queremos que nuestra aplicación reaccione a la pulsación de las teclas, tenemos de nuevo
3 eventos:
Normalmente, usaremos keydown y keyup para averiguar que tecla se ha pulsado, por
ejemplo los cursores. Si estamos interesado en el carácter pulsado, entonces usaremos
keypress .
<!DOCTYPE html>
<html lang="es">
<head>
<title>Eventos Teclado</title>
<meta charset="utf-8" />
</head>
<body>
<input type="text" name="cajaTexto" id="cajaTexto" />
<script src="eventos.js"></script>
</body>
</html>
Por ejemplo, si nos basamos en el evento keypress , si queremos que sólo se permitan
escribir letras en mayúsculas, mediante evt.charCode podemos obtener el código ASCII
de la tecla pulsada:
86
JavaScript
(function() {
var caja = document.getElementById("cajaTexto");
document.addEventListener("keypress", function(evt) {
Por ejemplo, si queremos capturar cuando el usuario pulsa CTRL + B dentro de la caja de
texto, haremos:
document.addEventListener("keydown", function(evt) {
var code = evt.keyCode;
var ctrlKey = evt.ctrlKey;
}, false);
Eventos y closures
Una manera de asociar una función a un evento que responda a determinados elementos es
mediante Closures. Vamos a crear un ejemplo muy sencillo para demostrar su uso.
87
JavaScript
Supongamos que queremos añadir algunos botones a una página para ajustar el tamaño del
texto. Una manera de hacer esto es especificar el tamaño de fuente del elemento body en
píxeles y, a continuación, ajustar el tamaño de los demás elementos de la página (como los
encabezados) utilizando la unidad relativa em :
body {
font-family: Helvetica, Arial, sans-serif;
font-size: 12px;
}
h1 {
font-size: 1.5em;
}
h2 {
font-size: 1.2em;
}
function cambiarTamanyo(tamanyo) {
return function() {
document.body.style.fontSize = tamanyo + 'px';
};
}
<button id="tam-12">12</button>
<button id="tam-14">14</button>
<button id="tam-16">16</button>
document.getElementById('tam-12').onclick = tam12;
document.getElementById('tam-14').onclick = tam14;
document.getElementById('tam-16').onclick = tam16;
88
JavaScript
Para poder interactuar con un formulario y poder capturar cuando se envía la información, lo
mejor es crear un id al formulario y acceder al formulario mediante dicho id tal como hemos
visto anteriormente con getElementById .
Del mismo modo, para acceder a un campo del formulario, bien lo haremos a través de su id
o mediante la propiedad document.forms.nombreDelFormulario.nombreDelCampo .
</fieldset>
</form>
function preparandoManejadores() {
document.getElementById("frmClnt").onsubmit = function() {
var ok = false;
// validamos el formulario
if (ok) {
return true; // se realiza el envío
} else {
return false;
}
};
}
window.onload = function() {
preparandoManejadores();
};
89
JavaScript
Campos de texto
Si nos centramos en los campos de texto ( type="text" ) (también válido para los campos de
tipo password o hidden ), para obtener el valor del campo usaremos la propiedad value ,
tal como vimos en el apartado de “Eventos de foco”. Cabe recordar que los eventos que puede
lanzar un campo de texto son: focus , blur , change , keypress , keydown y keyup .
Desplegables
Para un desplegable creado con select , mediante la propiedad type podemos averiguar
si se trata de una lista de selección única ( select-one ) o selección múltiple ( select-
multiple ).
Cada uno de los elementos de la lista es un objeto Option . Si queremos obtener el valor de
una opción usaremos la propiedad value , y para su texto text .
<fielset id="direccion">
<legend>Dirección</legend>
<p><label for="tipoVia">Tipo de Via</label>
<select name="tipoVia" id="tipoViaId">
<option value="calle">Calle</option>
<option value="avda">Avenida</option>
<option value="pza">Plaza</option>
</select>
</p>
<p><label for="domicilio">Domicilio</label>
<input type="text" name="domicilio" id="domi" /></p>
</fieldset>
</form>
tipoViaId.onchange = function() {
var indice = tipoViaId.selectedIndex; // 1
var valor = tipoViaId.options[indice].value; // avda
90
JavaScript
Opciones
Tanto los radio como los checkboxes tienen la propiedad checked que nos dice si hay
algún elemento seleccionado ( true o false ). Para averiguar cual es el elemento marcado,
tenemos que recorrer el array de elementos que lo forman.
color.checked = true;
3.7. Ejercicios
(0.4 ptos) Ejercicio 31. Contenido bloqueado
Crear una función que recorra el DOM desde la etiqueta <body> y si encuentra la palabra
"sexo", elimine el elemento y lo sustituya por "Contenido Bloqueado", con el texto en negrita.
function bloquearContenido() {}
91
JavaScript
<h2>Temporizador</h2>
<form id="formTemporizador">
Min: <input type="number" name="min" id="formMin"> <br />
Seg: <input type="number" name="seg" id="formSeg"> <br />
<input type="submit">
</form>
<br />
<div id="temporizador">
<span id="min">_</span>:<span id="seg">_</span>
</div>
<br />
<table>
<thead>
<tr><th>Contenido</th><th>Operación</th></tr>
</thead>
<tbody id="bodyTabla">
<tr>
<td id="fila1">Ejemplo de contenido</td>
<td><button onclick="toCani('fila1')">Caniar</button></td>
</tr>
</tbody>
</tr>
</table>
Añade el código necesarios para que la pulsar sobre los botones realice las siguientes
acciones:
92
JavaScript
• Añadir fila: Añade el contenido del campo como última fila de la tabla
• Caniar: Transformar el texto de la celda mediante la función toCani de la primera sesión
• Al pasar el ratón por encima de una celda, cambiará el color de fondo de la misma.
Escribir el código necesario para que tras dos segundos, se muestre la siguiente imagen. Una
vez mostrada la última imagen, el carrusel volverá a comenzar por la primera.
93
JavaScript
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.
function inicia() {
var nombre = "Batman";
function muestraNombre() {
console.log(nombre);
}
muestraNombre();
}
inicia();
function creaFunc() {
var nombre = "Batman";
function muestraNombre() {
console.log(nombre);
}
return muestraNombre;
}
El ejemplo sigue haciendo lo mismo, aunque ahora la función externa nos ha devuelto la
función interna muestraNombre() antes de ejecutarla.
94
JavaScript
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) {
return function(y) {
return x + y;
};
}
console.log(suma5(2)); // 7
console.log(suma10(2)); // 12
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();
95
JavaScript
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;
}
96
JavaScript
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.
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();
97
JavaScript
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.
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.
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:
98
JavaScript
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
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:
99
JavaScript
return contadorPriv;
}
}
};
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.
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)'}
];
setupAyuda();
100
JavaScript
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.
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)'}
];
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:
101
JavaScript
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.
this.getMensaje = function() {
return this.mensaje;
};
}
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:
10
Saldrá 'Click en elemento número 4', considerando que hay cuatro nodos en la lista, ya que se crean tantos
closures como nodos haya pero con un único entorno en común, de modo que todos comparten el valor de la
variable i
102
JavaScript
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.
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.
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.
miApp.id = 0;
miApp.siguiente = function() {
miApp.id++;
console.log(miApp.id);
return miApp.id;
};
miApp.reset = function() {
miApp.id = 0;
103
JavaScript
};
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 :
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
var miApp = {
id: 0,
siguiente: function() {
this.id++;
console.log(this.id);
return this.id;
},
reset: function() {
this.id = 0;
}
104
JavaScript
};
miApp.siguiente(); // 1
miApp.siguiente(); // 2
miApp.reset();
miApp.siguiente(); // 1
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.
return {
// interfaz público
}
})();
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:
Ejemplo módulo -
return {
siguiente: function() {
id++;
console.log(id);
return id;
},
reset: function() {
id = 0;
}
};
})();
miApp.siguiente(); // 1
miApp.siguiente(); // 2
miApp.reset();
miApp.siguiente(); // 1
105
JavaScript
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});
siguiente: function() {
var misArgs = arguments[0] || '';
var miIncremento = misArgs.incremento || 1;
id = id + miIncremento;
console.log(id);
return id;
}
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 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 id;
},
reset: function() {
id = 0;
}
106
JavaScript
};
})();
miApp.siguiente(); // 1
miApp.siguiente({incremento: 5}); // 6
miApp.reset();
miApp.siguiente(); // 1
Objeto de configuración con los valores por defecto para configurar el módulo
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 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;
},
anterior: function() {
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
107
JavaScript
miApp.reset();
miApp.siguiente().siguiente().anterior({decremento: 3}); // 1 2 -1
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:
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
Otra manera es utilizar como proxy del namespace el objeto this , haciendo uso del método
apply :
108
JavaScript
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
1. Evitar utilizar namespaces anidados, cuando más sencillos, mejor.
2. Aunque se pueda dividir en varios archivos .js , es conveniente
asociar un único namespace a un único fichero (y si se llaman igual,
mejor)
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 cadena = "¿Sabías que Batman es mejor que Joker, y el mejor amigo de
Batman es Robin?";
if (exReBatman.test(cadena)) {
console.log("la cadena contiene a Batman");
var pos = cadena.search(exReBatman);
console.log("en la posición " + pos);
}
Creamos una expresión regular para la cadena Batman . Se crean de forma similar a las
cadenas pero en vez de " se usa /
109
JavaScript
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.
Con estos conjuntos podemos crear las siguientes expresiones regulares que serán
verdaderas:
/[0123456789]/.test("en 2015");
/[0-9]/.test("en 2015");
110
JavaScript
/\d\d-\d\d-\d\d\d\d/.test("31-01-2015");
/[01]/.test("111101111");
/[^01]/.test("2015");
Con estos conjuntos podemos crear las siguientes expresiones regulares que serán
verdaderas:
Para agrupar expresiones se utilizan los paréntesis () . De este modo podemos utilizar un
patrón de repetición sobre una expresión:
111
JavaScript
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.
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:
Por ejemplo:
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.
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 match que se comporta del
mismo modo:
112
JavaScript
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"];
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
113
JavaScript
ordenado mediante "Apellido, Nombre", y queremos cambiarlo para poner delante el nombre
y quitar la coma:
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:
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:
114
JavaScript
Si cruzamos esos datos con el tráfico de los sites que utilizan las librerías tenemos la posición
de mercado:
115
JavaScript
Inclusión de librerías
Para incluir una librería, tal como vimos en Sección 1.2, “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:
...
116
JavaScript
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.
...
<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.
117
JavaScript
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.
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.
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>
<div id="qunit-fixture"></div>
118
JavaScript
<script src="//code.jquery.com/qunit/qunit-1.22.0.js"></script>
<script src="misPruebas.js"></script>
</body>
</html>
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 window , antes y después de cada caso de prueba
y posteriormente comprueba las diferencias. Si se añaden o modifican
propiedades el test fallará, mostrando las diferencias. De este modo
podemos comprobar que ni nuestro código ni nuestras pruebas exportan
accidentalmente alguna variable global.
function esPar(num) {
return num % 2 === 0;
}
119
JavaScript
Caso de prueba
Como hemos comentado antes, vamos a colocar las pruebas de manera desacoplada del
código, en el archivo misPruebas.js .
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.
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) {
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');
});
120
JavaScript
Como todas las aserciones han pasado, podemos asegurar que la función esPar() funciona
correctamente, o al menos, de la manera esperada.
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');
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.
121
JavaScript
Comparación
Al mostrar el resultado de la prueba, aparecerá tanto el valor real como el esperado, lo cual
facilita la depuración del código.
QUnit.test('comparaciones', function(assert) {
assert.ok( 1 == 1, 'uno es igual que uno');
});
QUnit.test('comparaciones', function(assert) {
assert.equal( 1, 1, 'uno es igual que uno');
assert.equal( 1, true, 'pasa porque 1 == true');
});
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:
122
JavaScript
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
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');
})
QUnit.test('comparacionesPropiedades', function(assert) {
assert.propEqual( {}, {}, 'correcto, los objetos tienen el mismo
contenido');
assert.propEqual( {a: 1}, {a: 1} , 'correcto');
123
JavaScript
Excepciones
QUnit.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');
});
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"); });
124
JavaScript
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:
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.
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.
125
JavaScript
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 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.
126
JavaScript
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);
});
127
JavaScript
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.
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).
128
JavaScript
4.6. Ejercicios
(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" .
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".
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.
129
JavaScript
5. JavaScript y el navegador
5.1. AJAX
Uno de los usos más comunes en JavaScript es AJAX (Asynchronous JavaScript And XML),
técnica (que no lenguaje) que permite realizar peticiones HTTP al servidor desde JavaScript,
y recibir la respuesta sin recargar la página ni cambiar a otra página distinta. La información
que se recibe se inserta mediante el API DOM que vimos en la sesión anterior.
Para usar AJAX, hemos de emplear el objeto XMLHttpRequest para lanzar una petición
HTTP al servidor con el método open(getPost, recurso, esAsync) donde:
• getPost : cadena con el valor GET o POST dependiendo del protocolo deseado
• recurso : URI del recurso que se solicita
• esAsync : booleano donde true indica que la petición en asíncrona
Aunque en sus inicios era original de IE, posteriormente fue adoptado por
todos los navegadores.
AJAX síncrono
Para comenzar vamos a basarnos en un ejemplo con llamadas síncronas (sí, parece extraño,
es como si usáramos SJAX), ya que el código es un poco más sencillo. El siguiente fragmento
realiza una petición a un archivo del servidor fichero.txt , y tras recibirlo, lo muestra
mediante un diálogo de alerta.
Tras crear el objeto, se crea una conexión donde le indicamos el método de acceso ( GET
o POST ), el nombre el recurso al que accedemos ( fichero.txt ) y finalmente si la
llamada es asíncrona ( true ) o síncrona ( false ).
Se envía la petición, en este caso sin parámetros ( null )
130
JavaScript
AJAX asíncrono
Al realizar una petición síncrona, AJAX bloquea el navegador y se queda a la espera de
la respuesta. Para evitar este bloqueo, usaremos el modo asíncrono. De este modo, tras
realizar la petición, el control vuelve al navegador inmediatamente. El problema que tenemos
ahora es averiguar si el recurso solicitado ya esta disponible. Para ello, tenemos la propiedad
readyState , la cual tenemos que consultar para conocer el estado de la petición. Pero si
la consultamos inmediatamente, nos dirá que no ha finalizado. Para evitar tener que crear
un bucle infinito de consulta de la propiedad, siguiente el esquema de eventos, usaremos el
manejador que ofrece la propiedad onreadystatechange .
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
var status = xhr.status;
if ((status >= 200 && status < 300) || (status === 304)) {
131
JavaScript
alert(xhr.responseText);
} else {
alert("Houston, tenemos un problema");
}
}
};
A modo de esquema, el siguiente gráfico representa las llamadas realizadas mediante una
petición asíncrona:
Bucle de Eventos
En la primera sesión comentamos que JavaScript es un lenguaje mono-
hilo. ¿Cómo se gestionan las peticiones HTTP asíncronas?
Enviando datos
Cuando vamos a enviar datos, el primer paso es serializarlos. Para ello, debemos considerar
qué datos vamos a enviar, ya sean parejas de variable/valor o ficheros, si vamos a emplear el
protocolo GET o POST y el formato de los datos a enviar.
HTML5 introduce el objeto FormData para serializar los datos y convertir la información a
multipart/form-data . Se trata de un objeto similar a un mapa, el cual se puede inicializar
con un formulario (pasándole al constructor el elemento DOM del formulario) o crearlo en
blanco y añadirle valores mediante el método append() :
132
JavaScript
formDataObj.append('uno', 'JavaScript');
formDataObj.append('dos', 'jQuery');
formDataObj.append('tres', 'HTML5');
Para poder enviar datos con POST, a la hora de abrir la conexión le indicaremos el método de
envío mediante xhr.setRequestHeader( 'Content-Type', 'application/x-www-
form-urlencoded') o el tipo de contenido deseado ( multipart/form-data , text/
xml , application/json , …).
Tras esto, al enviar la petición los datos como una cadena o mediante un objeto FormData .
Eventos
Toda petición AJAX lanza una serie de eventos conforme se realiza y completa la
comunicación, ya sea al descargar datos del servidor como al enviarlos. Los eventos que
podemos gestionar y el motivo de su lanzamiento son:
133
JavaScript
xhr.open('GET', 'http://www.omdbapi.com/?s=batman');
function onLoadStart(evt) {
console.log('Iniciando la petición');
}
function onProgress(evt) {
var porcentajeActual = (evt.loaded / evt.total) * 100;
console.log(porcentajeActual);
}
function onLoad(evt) {
console.log('Transferencia completada');
}
function onError(evt) {
console.error('Error durante la transferenciaº');
}
function onAbort(evt) {
console.error('El usuario ha cancelado la petición');
}
Mediante las propiedades loaded y total del evento, podemos obtener el porcentaje
del archivo descargado.
Respuesta HTTP
Si el tipo de datos obtenido de una petición no es una cadena, podemos indicarlo
mediante el atributo responseType , el cual puede contener los siguientes valores: text ,
arraybuffer , document (para documentos XML o HTML), blob o json .
134
JavaScript
xhr.responseType = 'blob';
function finDescarga(evt) {
if (this.status == 200) {
var blob = new Blob([this.response], {type: 'img/png'});
document.getElementById("datos").src = blob;
}
}
5.2. JSON
Tal como vimos en el módulo de Servicios REST, JSON es un formato de texto que almacena
datos reconocibles como objetos por JavaScript. Pese a que AJAX nació de la mano de XML
como el formato idóneo de intercambio de datos, JSON se ha convertido en el estándar de
facto para el intercambio de datos entre navegador y servidor, ya que se trata de un formato
más eficiente y con una sintaxis de acceso a las propiedades más sencilla.
{
"nombre": "Batman",
"email": "batman@heroes.com",
"gadgets": ["batmovil","batarang"],
"amigos": [
{ "nombre": "Robin", "email": "robin@heroes.com"},
{ "nombre": "Cat Woman", "email": "catwoman@heroes.com"}
]
}
Si queremos recuperarla mediante AJAX, una vez recuperada la información del servidor,
desde ES5 podemos usar el objeto JSON para interactuar con el texto. Este objeto ofrece
los siguientes métodos:
Así pues, volvamos al código AJAX. Una vez recuperada la información del servidor, la
transformamos a un objeto mediante JSON.parse() .
135
JavaScript
eval
Si el navegador no soporta ES5, podemos usar la función
eval(codigo) que evalúa una cadena como código JavaScript. Así
pues, podríamos hacer:
Filtrando campos
Al serializar un objeto mediante stringify podemos indicar tanto los campos que queremos
incluir como el número de espacios utilizados como sangría del código.
var heroe = {
136
JavaScript
nombre: "Batman",
email: "batman@heroes.com",
gadgets: ["batmovil", "batarang"],
amigos: [
{ nombre: "Robin", email: "robin@heroes.com"},
{ nombre: "Cat Woman", email: "catwoman@heroes.com"}
]
};
console.log(nomEmail); // {"nombre":"Batman","email":"batman@heroes.com"}
console.log(joker); //
{"nombre":"Joker","email":"joker_batman@heroes.com","gadgets":"batmovil y
batarang","amigos":[{"nombre":"Joker","email":"joker_robin@heroes.com"},
{"nombre":"Joker","email":"joker_catwoman@heroes.com"}]}
En el caso de querer indentar el código, con el tercer parámetro le indicamos con un número
(entre 1 y 10) la cantidad de espacios utilizados como sangría, y en el caso de pasarle un
carácter, será el elemento utilizado como separador.
137
JavaScript
Ejemplo OMDB
Una vez que sabemos como parsear una respuesta JSON, vamos a mostrar un ejemplo
completo de una petición AJAX al servidor de OMDB para obtener los datos de una película.
Al realizar una petición de búsqueda con http://www.omdbapi.com/?s=batman, en OMDB,
obtendremos una respuesta tal que así:
{"Search":[
{"Title":"Batman
Begins","Year":"2005","imdbID":"tt0372784","Type":"movie","Poster":"http://
ia.media-imdb.com/images/M/
MV5BNTM3OTc0MzM2OV5BMl5BanBnXkFtZTYwNzUwMTI3._V1_SX300.jpg"},
{"Title":"Batman","Year":"1989","imdbID":"tt0096895","Type":"movie","Poster":"http://
ia.media-imdb.com/images/M/
MV5BMTYwNjAyODIyMF5BMl5BanBnXkFtZTYwNDMwMDk2._V1_SX300.jpg"},
{"Title":"Batman
Returns","Year":"1992","imdbID":"tt0103776","Type":"movie","Poster":"http://
ia.media-imdb.com/images/M/
MV5BODM2OTc0Njg2OF5BMl5BanBnXkFtZTgwMDA4NjQxMTE@._V1_SX300.jpg"},
...
]}
Por lo tanto, para mostrar el título y el año de las peliculas encontradas haremos:
function mostrarPelicula(datos) {
var o = JSON.parse(datos);
var pelis = o.Search;
for (numPeli in pelis) {
console.log(pelis[numPeli].Title + " - " + pelis[numPeli].Year);
}
}
Cross-Domain
Sabemos que el navegador funciona como sandbox y restringe las peticiones AJAX, de modo
que sólo permite realizar peticiones a recursos que se encuentren en el mismo dominio.
Esta restricción de conoce como Same-Origin Policy, y para ello las peticiones tienen que
compartir protocolo, servidor y puerto. Esta política también fija que desde un dominio A se
138
JavaScript
puede realizar una petición a un dominio B, pero no se puede obtener ninguna información de
la petición hacia B, ni la respuesta ni siquiera el código de respuesta.
El uso de cross-domain AJAX permite romper esta poli#tica bajo ciertas circunstancias,
dependiendo de la técnica utilizada:
Access-Control-Allow-Credentials: true
La realidad es que pocos dominios permiten su uso (algunos son http://bit.ly o http://
twitpic.com).
Si queremos evitarnos configurar el servidor con la cabecera, podemos hacer uso de http://
www.corsproxy.com, el cual fusiona tanto la técnica CORS como el uso de un proxy.
139
JavaScript
JSONP
Estudiemos en más detalle esta técnica. Supongamos que realizamos la siguiente petición:
<script src="http://www.omdbapi.com/?s=batman"></script>
Esto no sirve de mucho, ya que sólo inserta en ese punto del documento el objeto en formato
JSON, pero no hace nada con esa informacio#n.
Lo que JSONP permite es definir una función (que es nuestra) y que recibirá como parámetro
el JSON que envía el servidor. Los servicios que admiten JSONP reciben un parámetro en la
petición, (normalmente llamado callback) que especifica el nombre de la funcio#n a llamar.
http://www.omdbapi.com/?s=batman?callback=miFuncion
miFuncion(respuestaEnJSON);
<body>
<script>
function llamarServicio() {
var s = document.createElement("script");
s.src = "http://www.omdbapi.com/?s=batman?callback=miFuncion";
document.body.appendChild(s);
}
function miFuncion(json) {
document.getElementById("resultado").innerHTML = JSON.stringify(json);
}
</script>
<input type="button" onclick="llamarServicio()" value="JSONP">
<div id="resultado"></div>
</body>
Esta técnica se restringe a peticiones GET, con lo que si queremos hacer otro tipo de petición
(POST, PUT o DELETE), no podremos usar JSONP. Además, requiere que el servidor sea de
confianza, ya que el servicio podría devolver código malicioso que se ejecutará en el contexto
de nuestra página (lo que le da acceso a las cookies, almacenamiento local, etc…). Para
140
JavaScript
reducir los riesgos se pueden usar frames y window.postMessage para aislar las peticiones
JSONP.
Trabajaremos más ejemplos de AJAX mediante jQuery en siguientes sesiones, el cual facilita
mucho el acceso a contenido remoto desde el cliente.
Testing
En la unidad anterior ya vimos como hacer pruebas de código asíncrono mediante QUnit.
Veamos un ejemplo que pruebe código AJAX:
5.3. HTML 5
Las principales características que ofrece HTML 5 y que se están incorporando a los
navegadores de manera progresiva son:
141
JavaScript
Cada una de las características vistas anteriormente (vídeo, audio, geolocalización) tienen sus
métodos y eventos propios, que se salen del alcance del módulo.
var c1 = document.getElementsByClassName("clase1")
var c12 = document.getElementsByClassName("clase1 clase2");
if (document.getElementsByClassName) {
// existe, por lo que el navegador lo soporta
} else {
// no existe, con lo que el navegador no lo soporta
}
if (navigator.userAgent.indexOf("Netscape")) { /// }
if (navigator.appName == "Microsoft Internet Explorer")
{ /// }
Para ayudarnos a detectar las características que ofrece nuestro navegador, podemos
usar la famosa librería Modernizr (http://www.modernizr.com). Esta librería ofrece el objeto
Modernizr que contiene propiedades booleanas con las prestaciones soportadas, por
ejemplo, Modernizr.video o Modernizr.localStorage . Así pues, una vez incluida la
librería, para detectar si el navegador soporta la etiqueta video de HTML5 haríamos:
if (Modernizr.video) {
// usamos el vídeo de HTML5
} else {
// usamos el vídeo Flash
}
142
JavaScript
Polyfills
Si nuestro navegador no soporta la característica deseada, podemos usar HTML shims o
polyfills que reproducen la funcionalidad del navegador.
Un shim es una librería que ofrece una nueva API a un entorno antiguo mediante los medios
que ofrece el entorno.
Un polyfill es un fragmento de código (o plugin) que ofrece la tecnología que esperamos que
el navegador ofrezca de manera nativa. Con lo que un polyfill es un shim para el API del
navegador.
Por ejemplo, si nuestra aplicación tiene que soportar navegadores antiguos, podemos usar el
polyfill html5shiv (https://github.com/aFarkas/html5shiv):
<!--[if lt IE 9]>
<script src="html5shiv.js"></script>
<![endif]-->
Cookies
Para almacenar una cookie podemos acceder a la propiedad document.cookie
asignándole un valor o consultándola para recuperar el valor almacenado.
document.cookie = "nombre=Batman";
var info = document.cookie;
document.cookie = "amigo=Robin";
143
JavaScript
Para eliminar una cookie hay que poner el atributo expires con una fecha del pasado y no
pasarle ningún valor:
La principal ventaja de usar cookies es que tanto el servidor con Java (o cualquier
otro lenguaje) como el navegador mediante JavaScript pueden acceder a la información
almacenada.
• sólo pueden almacenar hasta 4KB (normalmente guardan una clave hash que identifica
la vista)
• se envían y vuelven a recibir con cada petición
• caducan
LocalStorage
En cambio, si nos centramos en LocalStorage, sólo podemos usarlo mediante JavaScript,
aunque como veremos a continuación, su uso es muy sencillo y potente:
localStorage.nombre = "Aitor";
localStorage.setItem("apellido1","Medrano");
console.log(localStorage.nombre);
console.log(localStorage.getItem("apellido1"));
MDN recomienda usar los métodos getItem y setItem en vez del uso
de las propiedades
144
JavaScript
console.log(localStorage.length); // 2
localStorage.removeItem(apellido1);
console.log(localStorage.getItem("apellido1")); // undefined
localStorage.clear();
console.log(localStorage.getItem("nombre")); // undefined
Cabe destacar que los datos se almacenan todos como cadenas, ya que antes de almacenar
se ejecuta el método .toString por lo que si insertamos un dato como número, deberemos
volver al parsearlo al recuperarlo del almacenamiento.
localStorage.edad = 35;
console.log(typeof localStorage.getItem("edad")); // "string"
var edad = parseInt(localStorage.getItem("edad"), 10);
Almacenando un objeto
Si lo que queremos almacenar es un objeto, realmente se almacenará una cadena indicando
que se trata de un objeto.
var persona = {
nombre : "Aitor",
apellido1 : "Medrano",
145
JavaScript
edad : 35
};
localStorage.setItem("persona", persona);
console.log(localStorage.getItem("persona")); // [object Object]
Para solucionar esto, lo mejor es usar las librerías JSON que acabamos de estudiar para
crear una representación del objeto mediante JSON.stringify(objeto) y posteriormente
recuperarlo con JSON.parse(objetoLS) .
var persona = {
nombre : "Aitor",
apellido1 : "Medrano",
edad : 18
};
localStorage.setItem("persona", JSON.stringify(persona));
console.log(localStorage.getItem("persona")); // "{\"nombre\":\"Aitor\",
\"apellido1\":\"Medrano\", \"edad\":18}"
var personaRecuperada = JSON.parse(localStorage.getItem("persona"));
console.log(personaRecuperada); /* [object Object] {
apellido1: "Medrano",
edad: 18,
nombre: "Aitor"
} */
El uso del almacenamiento local sigue el planteamiento de sandbox, con lo que queda
restringido su uso al dominio activo, con lo que no podremos acceder a propiedades del
almacenamiento local creadas desde un dominio distinto.
SessionStorage
De manera similar a localStorage , podemos usar el objeto sessionStorage para
almacenar la información con un ciclo de vida asociado a la sesión del navegador. Es decir,
al cerrar el navegador, el almacenamiento se vaciará.
Un caso partícular es que se produzca un cierre inesperado por fallo del navegador. En dicho
caso, los datos se restablecen como si no hubiésemos cerrado la sesión.
Ambos objetos heredan del interfaz Storage por lo que el código de uso es similar.
IndexedDB
Además del almacenamiento local, el navegador permite almacenar la información en una
estructura más compleja similar a una BBDD.
Existen 2 posibilidades:
• WebSQL DB: wrapper sobre SQLite que permite interactuar con lo datos mediante un
interfaz SQL, con lo que podemos ejecutar sentencias select, insert, update o delete. El
problema viene de que al tratarse de un estándar, y no haber más de una implementación,
la especificación se ha congelado y ha pasado a un estado de deprecated. En la actualidad
la soportan la mayor parte de los navegadores excepto Firefox e IE (ver http://caniuse.com/
#feat=sql-storage).
146
JavaScript
HTML5 introduce los Web Workers como una solución a la ejecución mono-hilo de JavaScript.
De este modo, vamos a poder crear hilos de ejecución que se ejecutan en background que
corren al mismo tiempo (más o menos) y que pueden llegar a aprovechar las arquitecturas
multi-núcleo que ofrece el hardware.
Al crear un web worker, éste correrá en background mientras que el hilo principal procesa los
eventos del interfaz de usuario, incluso si el hilo del worker esta ocupado procesando gran
cantidad de datos. Por ejemplo, un worker puede procesar una estructura JSON para extraer
la información útil a mostrar en el interfaz de usuario.
Hilo padre
El código que pertenece a un web worker reside en un archivo JavaScript aparte. El hilo padre
crea el nuevo worker indicando la URI del script en el constructor de Worker , el cual lo carga
de manera asíncrona y lo ejecuta.
// archivo JS padre
var worker = new Worker("otroFichero.js");
Creamos un web worker en el hijo padre que referencia al código que reside en
otroFichero.js
Para lanzar el worker, el hilo padre le envía un mensaje al hijo mediante el método
postMessage(mensaje) :
worker.postMessage("contenido");
La página padre puede comunicarse con los workers mediante el API postMessage , el cual
también se envía para que los workers envíen mensajes al padre. Además de poder enviar
datos de tipos primitivos (cadena, número, booleano, null o undefined ), podemos enviar
estructuras JSON mediante arrays y objetos. Lo que no se puede enviar son funciones ya que
pueden contener referencias al DOM.
Los hilos del padre y de los workers tienen su propio espacio en memoria, con lo que los
mensajes enviados desde y hacia se pasan por copia, no por referencia, con lo cual se
147
JavaScript
Además de enviar información, el padre puede registrar un callback que quede a la espera
de recibir un mensaje una vez que el worker haya finalizado su trabajo. Este permite al hilo
del padre utilizar la respuesta del hijo, como por ejemplo, modificar el DOM con la información
procesada por el worker.
worker.onmessage = function(evt) {
console.log("El worker me ha contestado!");
console.log("Y me ha enviado " + evt.data);
};
• target : identifica al worker que envió el mensaje, por lo que se usa cuando tener un
entorno con múltiples workers
• data : contiene el mensaje enviado por el worker de vuelta a su padre
worker.onmessage = function(evt) {
console.log("El worker me ha contestado!");
console.log("Y me ha enviado " + evt.data);
};
worker.postMessage("contenido");
Worker hijo
El worker en sí se coloca en un archivo .js aparte, que en el ejemplo anterior hemos
nombrado como otroFichero.js .
Por ejemplo, en su ejemplo más sencillo si queremos que devuelva el mensaje recibido y le
salude haríamos:
self.addEventListener('message', function(evt) {
var mensaje = evt.data;
El worker también registra un manejador de evento para los mensajes que recibe
de su padre. Este manejador también se podía haber añadido mediante la propiedad
onmessage
148
JavaScript
Aunque es común tener múltiples hilos workers para dividir el trabajo entre
ellos, hay que ir con mucho cuidado, ya que tienen un gran coste de
rendimiento al arrancar y consumen mucha memoria por cada instancia.
Para que un worker termine por sí mismo y deseche cualquier tarea pendiente usaremos el
método close :
self.close();
worker.terminate();
Seguridad y restricciones
Como los web workers trabajan de manera independiente del hilo de interfaz del navegador,
dentro de un worker no tenemos acceso a muchos objetos JavaScript como document ,
window (variables globales), console , parent ni, el más importante, al DOM. El hecho
de no tener acceso al DOM y no poder modificar la página puede parecer muy restrictivo, pero
es una importante decisión sobre la seguridad. ¿Qué podría pasar si múltiples hilos intentaran
modificar el mismo elemento? Por ello los web workers funcionan en un entorno restringido
y de mono-hilo.
Dicho esto, podemos usar los workers para procesar datos y devolver el
resultado al hilo principal, el cual sí que puede modificar el DOM. Aunque
tiene reducido el número de objetos que pueden usar, desde un worker si
que tenemos acceso a algunas funciones como setTimeout()/clearTimeout() ,
setInterval()/clearInterval() , navigator , etc… También podemos usar los
objetos XMLHttpRequest y localStorage dentro de un worker, e incluso importar otros
workers.
Ya hemos comentado que en su contexto, tanto self como this referencian al alcance
global.
Otra restricción de los web workers es que siguen la política de Same-Origin Policy. Por
ejemplo, un script hospedado en http://www.ejemplo.com no puede acceder a otro en https://
www.ejemplo.com. Incluso con nombres de dominio idénticos, la política fuerza que el
protocolo sea el mismo. Esto no suele ser un problema ya que suelen residir en el mismo
dominio, siempre es bueno recordarlo.
Carga de archivos
Dentro de un worker podemos cargar otros archivos JavaScript mediante
importScripts() :
149
JavaScript
importScripts("script1.js", "script2.js");
Gestión de errores
Al no tener acceso a la consola, el proceso de depuración y gestión de errores se complica un
poco. Por suerte, las Dev-Tools nos permiten depurar el código del worker como si se tratase
de cualquier otro código JavaScript
worker.addEventListener('error', function(error){
console.log('Error provocado por el worker: ' + error.filename
+ ' en la linea: ' + error.lineno
+ ' con el mensaje: ' + error.message);
});
Casos de uso
Ya hemos comentado que los web workers no están pensados para usarse en grandes
cantidades debido a su alto coste de inicio y el gran coste de memoria por instancia.
Un caso de uso real puede ser cuando tenemos que trabajar con una librería de terceros
con un API síncrona que provoca que el hilo principal tenga que esperar al resultado antes
de continuar con la siguiente sentencia. En este caso, podemos delegar la tarea a un nuevo
worker para beneficiarnos de la capacidad asíncrona.
Si necesitamos procesar una gran cantidad de información devuelta por el servidor, es mejor
crear varios workers para procesar los datos en porciones que no se solapen.
150
JavaScript
Finalmente, también se emplea para analizar fuentes de video o audio con la ayuda de
múltiples web workers, cada uno trabajando en una parte predefinida del problema
Aunque se trate de una tecnología que no esta completamente implantada, en un futuro abrirá
nuevas posibilidades una vez los navegadores la implementes y ofrezcan herramientas de
depuración.
Shared workers
La especificación de HTML5 define dos tipos de workers, los dedicados y los compartidos
(shared). Los dedicados son los que hemos estudiado, ya que existe una relación directa entre
el creador del worker y el propio worker mediante una relación 1:1.
En cambio, un shared worker puede compartirse entre todas las páginas de un mismo origen,
por ejemplo, todas las páginas o scripts de un mismo dominio pueden comunicarse con un
único shared worker.
Hay que tener en cuenta que a día de hoy ni Internet Explorer ni Safari
los soportan, estando disponibles únicamente en Firefox, Chrome y Opera
(http://caniuse.com/#feat=sharedworkers).
Para crear un worker compartido le pasaremos la URL del script o el nombre del worker al
constructor de SharedWorker .
sharedWorker.port.start();
self.onconnect = function(evento) {
var clientePuerto = event.source;
clientePuerto.onmessage = function(evento) {
var datos = evento.data;
// ... procesamos los datos
clientePuerto.postMessage('datos de respuesta');
}
151
JavaScript
clientePuerto.start();
};
5.6. WebSockets
Una de las características más novedosas de HTML5 son los WebSockets, los cuales permiten
comunicar un cliente con el servidor sin necesidad de peticiones AJAX.
Así pues, son una técnica de comunicación bidireccional sobre un socket TCP, un tipo de
tecnología PUSH.
Su constructor recibe un parámetro con la URL que vamos a conectar y otro opcional con
información sobre los protocolos a emplear:
Una vez creada la conexión, podemos consultar el estado de la misma mediante la propiedad
status , cuyos valores pueden ser:
• 0 : conectando
• 1 : abierto
• 2 : cerrado
Una vez conectados, ya podemos enviar datos al servidor mediante el método send() cada
vez que queramos enviar un mensaje:
152
JavaScript
Los datos que podemos enviar son del tipo String (en UTF8), Blob o ArrayBuffer
ejemploWS.close();
Antes de cerrar la conexión puede ser necesario comprobar el valor del atributo
bufferedAmount para verificar que se ha transmitido toda la información.
Eventos
Al tratarse de una conexión asíncrona, no hay garantía de poder llamar al método send()
inmediatamente después de haber creado el objeto WebSocket .
Por ello, todos los mecanismos de comunicación y gestión de la conexión se realizan mediante
eventos.
Para tratar la respuesta del servidor hay que utilizar el evento onmessage el cual saltará al
recibir un mensaje:
Para extraer la información del mensaje, hay que acceder a la propiedad data del objeto
recibido.
Finalmente, para comprobar si ha sucedido algún error, le asignaremos un manejador al evento
onerror :
153
JavaScript
Ejemplo chat
Si ahora revisamos el código de la sala de chat que vimos en el módulo de Componentes
Web, podemos comprobar como se creaba la conexión y la manera en que gestionábamos
los eventos de los mensajes recibidos:
var websocket;
websocket.onmessage = onMessage;
websocket.onopen = onOpen;
}
function onOpen(event) {
document.getElementById("chat").style.display = 'block';
document.getElementById("login").style.display = 'none';
}
function onMessage(event) {
items = event.data.split(";");
document.getElementById("mensajes").innerHTML = "<p><strong><"
+ items[0] + "></strong> " + items[1] + "</p>" +
document.getElementById("mensajes").innerHTML;
}
function send(texto) {
websocket.send(texto);
}
154
JavaScript
5.7. Rendimiento
Un elemento a tener en cuenta desde el mismo momento que empezar a codificar es el
rendimiento.
• Herramientas de Profiling: Las Dev-Tools incluyen este tipo de herramientas, las cuales
podemos usar desde:
# el botón/pestaña de Profile
# la consola, mediante las funciones profile() y profileEnd()
# el código de aplicación, mediante console.profile() y
console.profileEnd()
Para ello, primero hemos de abrir la pestaña Network de las Dev-Tools y posteriormente
acceder al recurso que queremos analizar. Por ejemplo, si accedemos a la web del experto
http://expertojava.ua.es/ obtendremos el siguiente análisis:
155
JavaScript
Si comprobamos la barra de estado inferior podemos ver como hemos pasado de 227KB a
123B de información transferida, y de 381ms a 246ms. Además, el tiempo necesario para la
carga de la página y del DOM también se ha reducido.
Reglas
Una serie de reglas a tomar dentro de JavaScript son:
156
JavaScript
# JSLint (http://jslint.com)
# JSHint (http://jshint.com)
• Colocar los enlaces a las hojas de estilo antes de los archivos JavaScript, para que la
página se renderice antes.
• Colocar los enlaces a código JavaScript al final de la página, para que primero se cargue
todo el contenido DOM, o realizar la carga de la librería JavaScript de manera asíncrona
incluyendo el atributo async :
• Utilizar funciones con nombre en vez de anónimas, para que aparezcan en el Profiler
• Aislar las llamadas AJAX y HTTP para centrarse en el código JavaScript. Para ello, es
conveniente crear objetos mock que eviten el tráfico de red.
• Dominar la herramienta Profiler que usemos en nuestro entorno de trabajo.
157
JavaScript
5.8. Ejercicios
(0.6 ptos) Ejercicio 51. Star Wars
Vamos a crear una aplicación que muestre información sobre Star Wars. Para ello, vamos
a utilizar el API REST que ofrece 'The Star Wars API' en http://swapi.co, la cual permite su
acceso mediante CORS (sin necesidad de usar JSONP).
Para ello, al cargar la página, vamos a mostrar un listado con las películas de Star Wars.
Al pulsar sobre el título de una película, se mostrará la sinopsis de la misma y un listado con
un máximo de 10 personajes que aparecen en la misma.
<!DOCTYPE html>
<html>
<head>
<title>Ejercicio 51</title>
<meta charset="utf-8" />
</head>
<body>
<h1>Star War API</h1>
<h2>Películas - <span id="total"></span></h2>
<ul id="peliculas"></ul>
<h2>Sinopsis</h2>
<div id="sinopsis"></div>
<h2>Personajes</h2>
<ul id="personajes"></ul>
<script src="ej51.js"></script>
</body>
</html>
158
JavaScript
<!DOCTYPE html>
<html>
<head>
<title>Ejercicio 52</title>
<meta charset="utf-8" />
</head>
<body>
<form id="miForm">
Usuario: <input type="text" name="usuario" id="inputUsuario" /><br/>
<input type="submit" id="btnSubmit" value="Enviar" />
</form>
Visitas usuario: <span id="visitas"></span> <br />
Total visitas: <span id="total"></span>
<script src="ej52.js"></script>
</body>
</html>
Tras introducir los datos del usuario, las capas de visitas y total se actualizarán
automáticamente con el número de visitas del usuario y con el total de visitas realizadas desde
el navegador, independientemente del usuario rellenado.
159
JavaScript
Para ello, mediante http://swapi.co/api/ obtendremos un objeto JSON con las propiedades y
las URLs de los recursos disponibles.
Para cada uno de estos recursos, hemos de crear un Web Worker que realice la petición AJAX
y obtenga el número de elementos que contiene, es decir, cada uno de los hijos realizará
una petición REST a una categoría. Una vez obtenido el resultado, devolverá el número de
elementos de dicha categoría al padre.
<!DOCTYPE html>
<html>
<head lang="es">
<meta charset="UTF-8">
<title>Ejercicio 53</title>
</head>
<body>
<h1>Star War API - Workers</h1>
<h2>Elementos - <span id="total"></span></h2>
<ul id="elementos"></ul>
<script src="ej53.js"></script>
</body>
</html>
• un archivo denominado ej53.js , con el código necesario para obtener las categorías y
sus urls, y la creación y comunicación con el web worker.
• un archivo denominado ej53worker.js , con el código necesario para obtener la
cantidad de elementos de una determinada categoría/url.
160
JavaScript
161
JavaScript
6. jQuery
A día de hoy, podemos asegurar que jQuery (http://jquery.com) es la librería JavaScript más
utilizada ya que facilita mucho el trabajo del desarrollador. Su lema de write less, do more
(escribe menos, haz más) resume su propósito.
6.2. Versiones
Si accedemos a la página de jQuery (http://jquery.com) podemos descargar dos versiones: la
versión 1.12.1 y la 2.2.1. La principal diferencia es que la versión 2.x deja de dar soporte a las
versiones 6, 7 y 8 de Internet Explorer.
162
JavaScript
Además, a la hora de descargar la versión en la que estemos interesados, podemos bajar uno
de los dos siguientes scripts:
• un script que no está comprimido y que nos permite consultar el código, pero que ocupa
más (este es el que usaremos en el módulo)
• el que está comprimido (minificado) y que se usa en producción para reducir la carga de
la página.
Por ejemplo, si nos centramos en la versión 1.12.1 , la versión de desarrollo ocupa 287KB
mientras que la minificada sólo ocupa 95KB, es decir, casi tres veces más.
Si no queremos descargarla, podemos usar cualquiera de los CDNs que más nos interese:
Así pues, una vez elegida la versión que mejor nos convenga, la incluiremos dentro de nuestro
código, teniendo en cuenta que deberíamos cargarla tras los archivos CSS y antes de todas
las librerías que dependan de jQuery:
En todos los ejemplos que veremos en esta sesión y las posteriores damos
por supuesto que se ha incluido correctamente la librería de jQuery.
document.getElementById("miCapa").className = "resaltado";
jQuery("#miCapa").addClass("resaltado");
Además de ser más corto, jQuery nos permitirá acceder a un elemento sin hacerlo por su ID
(por ejemplo, por su clase CSS, por el tipo de etiqueta HTML, …) y filtrar los resultados del
selector.
Para reducir el tamaño del código, en vez de utilizar todo el rato la función jQuery , se utiliza
el alias $ que viene predefinido en la librería:
$("#miCapa").addClass("resaltado");
163
JavaScript
Así pues, tras el $ , rodeado por paréntesis le pasaremos el selector del elemento sobre el
que queremos trabajar, para posteriormente encadenar la acción a realizar sobre la selección.
Si utilizamos otras librerías JavaScript que también utilice el $ , se recomienda realizar una
llamada a $.noConflict() para evitar conflictos de namespace. Tras la llamada, el alias
$ deja de estar disponible, obligándonos a escribir jQuery cada vez que escribiríamos $ .
(function($) {
// código jQuery que usa el $
}) (jQuery);
El resultado de aplicar un selector siempre va a ser un array de objetos (no son objetos DOM,
sino objetos jQuery que envuelven a los objetos DOM añadiéndoles funcionalidad extra) que
cumplen el criterio de la consulta. Posteriormente, podemos aplicar un filtro sobre un selector
para refinar el array de resultados que devuelve.
Selectores
Los selectores y filtros que usa jQuery se basan en la sintaxis de CSS. Para aplicar un selector,
le tenemos que pasar el selector como un parámetro entre comillas a la función jQuery , o
mejor, a su alias $ , del siguiente modo:
$('selector');
Si queremos restringir el contexto de aplicación del selector para evitar realizar la búsqueda
sobre el documento DOM completo, le podemos pasar el contexto como segundo parámetro:
$('selector', contexto);
164
JavaScript
Si el contexto que le pasamos contiene muchos elementos, cada elemento se utiliza como
punto de inicio en la búsqueda del selector.
$('p:even');
$('p:even', $('.importante'));
Básicos
A continuación tenemos los selectores que son exactamente igual que en CSS:
Para poder probar todos estos selectores vamos a basarnos en la siguiente página:
<!DOCTYPE html>
<html>
<head lang="es">
<meta charset="UTF-8">
<title>Selectores</title>
<style>
.a { color: red; }
.b { color: gold; }
</style>
</head>
<body>
165
JavaScript
<ul id="listado">
<li class="a">elemento 0</li>
<li class="a">elemento 1</li>
<li class="b">elemento 2</li>
<li class="b">elemento 3</li>
</ul>
<p id="pa0" class="a" lang="es-AR">Párrafo 0</p>
<p>Párrafo 1</p>
<p id="pa2" class="b">Párrafo 2</p>
<p id="pa3" lang="es-ES">Párrafo 3</p>
</body>
</html>
document.getElementsByTagName("p");
$("p");
document.getElementById("listado");
$("#listado");
166
JavaScript
Mediante DOM tenemos que recuperar todas las etiquetas, recorrerlas como un array y
seleccionar los elementos deseados
Obtener las etiquetas de clase b , pero solo si están dentro de un lista desordenada
Mediante DOM tenemos que recuperar todas las etiquetas, recuperar sus hijos, nietos,
etc… y ver si alguno tiene dicha clase
Jerárquicos
Además de los selectores básicos, podemos hacer consultas dependiendo de las relaciones
jerárquicas o una serie de criterios comunes, mediante la siguiente sintaxis:
$("p, li.b");
Obtener los elementos de lista cuya clase sea a y que sean descendientes de una lista
desordenada:
$("ul li.a");
167
JavaScript
Filtros
Los filtros trabajan de manera conjunta con los selectores para ofrecer un control todavía más
preciso a la hora de seleccionar elementos del documento.
Comienzan con : , y se pueden encadenar para crear filtros complejos, como por ejemplo:
$("#noticias tr:has(td):not(:contains('Java'))");
Básicos
Permiten refinar la selección con los elementos que cumplen condiciones relativas a la posición
que ocupan o un determinado índice:
Filtro Propósito
Selecciona sólo la primera instancia del conjunto devuelto por el
:first
selector
:last Selecciona sólo la última instancia del conjunto devuelto por el selector
Selecciona sólo los elementos pares del conjunto devuelto por el
:even
selector
Selecciona sólo los elementos impares del conjunto devuelto por el
:odd
selector
:eq(n) Selecciona los elementos situados en el índice n
:gt(n) Selecciona los elementos situados detrás del índice n
:lt(n) Selecciona los elementos situados antes del índice n
:header Selecciona todos los elementos cabeceras ( h1 , h2 , h3 ,…)
Selecciona todos los elementos que están actualmente animados de
:animated
alguna manera
:not(selector) Selecciona los elementos que no cumplen el selector
$("p:first");
$(".a:last");
168
JavaScript
Obtener los párrafos pares (el primer párrafo es el 0, con lo cual es par)
$("p:even");
$("p:gt(1)");
$("p:not(p:eq(1))");
Filtro Propósito
[atrib] Incluye los elementos que tienen el atributo atrib
[atrib=valor] Incluye los elementos que tienen el atributo atrib con el valor valor
Incluye los elementos que tienen el atributo atrib y no tiene el valor
[atrib!=valor]
valor
Incluye los elementos que tienen el atributo atrib y su valor
[atrib^=valor]
comienza por valor
Incluye los elementos que tienen el atributo atrib y su valor termina
[atrib$=valor]
con valor
Incluye los elementos que tienen el atributo atrib y su valor contiene
[atrib*=valor]
valor
[filtroAtrib1] Incluye los elementos que cumplen todos los filtros especificados,
[filtroAtrib2] es decir, filtroAtrib1 y filtroAtrib2
$("p[class]");
$("p[id=pa2]");
$("p[id^=pa]");
Obtener los párrafos cuyo id comience por pa y que su atributo lang contenga es
$("p[id^=pa][lang*=es]");
169
JavaScript
Basados en el Contenido
Permiten refinar la selección con los elementos que cumplen condiciones relativas a su
contenido:
Filtro Propósito
:contains(texto) Incluye los elementos que contienen la cadena texto
:empty Incluye los elementos vacíos, es decir, sin contenido
Incluye los elementos que contienen al menos uno que cumple el
:has(selector)
selector
Incluye los elementos que son padres, es decir, que contienen al
:parents
menos otro elemento, incluido texto
$("p:contains(3)");
$(":contains(3)");
$("p:contains(3),li:contains(3)");
Selecciona los que contienen un 3, y sus padres, ya que por ejemplo, el elemento body
contiene un 3 dentro de un párrafo, con lo cual no sería una solución correcta
Restringimos la búsqueda sobre los elementos que conocemos. Para hacerlo
correctamente deberíamos seleccionar todos aquellos que fueran hijos
Obtener los párrafos que son padres
$("p:parent");
Obtener una lista desordenada donde haya algún elemento que sea de la clase b
$("ul:has(li[class=b])");
Basados en la Visibilidad
Permiten refinar la selección con los elementos que cumplen condiciones relativas a la
propiedad visibility :
Filtro Propósito
:visible Incluye los elementos visibles
:hidden Incluye los elementos ocultos
Y un ejemplo:
170
JavaScript
$("p:hidden");
Filtro Propósito
:nth-child(índice) Incluye los hijos número índice, numerados de 1 a n.
:nth-child(even) Incluye los hijos pares
:nth-child(odd) Incluye los hijos impares
Incluye los hijos cuya posición cumple la ecuación, por ejemplo,
:nth-child(ecuación)
2n o 3n+1
:first-child Incluye los elementos que son el primer hijo
:last-child Incluye los elementos que son el último hijo
Incluye los elementos que son hijos únicos, es decir, no tienen
:only-child
hermanos
$("ul li:nth-child(3)");
$("ul li:last-child");
Selector Propósito
:input Encuentra todos los input, select, textarea y elementos button
:text
:password
:radio
:checkbox
:submit Encuentra todos los elementos de dicho tipo
:reset
:image
:button
:file
Y para realizar filtrados adicionales sobre los elementos, podemos usar los siguientes filtros
de manera conjunta a los anteriores:
171
JavaScript
Filtro Propósito
:enabled Incluye los elementos que están habilitados
:disabled Incluye los elementos que están deshabilitados
:checked Incluye los elementos que están marcados (radio y checkbox)
:selected Incluye los elementos que están seleccionados (select)
$("form :input");
$("form :text:enabled");
$("form :checkbox:checked");
El objeto jQuery
Tras seleccionar la información mediante selectores y filtros obtendremos un objeto jQuery ,
el cual envuelve a uno o más elementos HTML.
A partir de este objeto vamos a poder acceder al resultado, iterar sobre él o encadenar nuevas
sentencias.
172
JavaScript
Destacar que los métodos index() son los complementarios a get() , de modo que en
get() se indica un índice y se obtiene un HTMLElement , y el index() a partir del
elemento se obtiene el índice.
$("body *").index(document.getElementById("contenido"));
$("body *").index($("#contenido"));
$("#contenido").index("body *");
$("ul").find("li.b");
$("ul li.b");
$("p").each(function() {
// Por ejemplo, ponerles un borde
$(this).css("border","3px solid red");
});
De HTMLElement a jQuery
Si tenemos un objeto DOM y queremos crear un objeto jQuery , podemos hacerlo pasándolo
como argumento a la función $ . De este modo podemos incluir código que no usa jQuery e
integrarlo de un modo sencillo.
173
JavaScript
Modificando la selección
Una vez seleccionada la información mediante selectores y filtros, vamos a poder modificarla
añadiendo nuevos elementos al conjunto de resultados o restringiendo los resultados a un
subconjunto menor.
Encadenando sentencias
Para ello, tras usar el selector, mediante el operador . podemos añadir métodos que se irán
encadenando unos detrás de otros:
$(selector).funcion1().funcion2().funcion3();
$(this).siblings('button').removeAttr('disabled').end().attr('disabled','disabled');
Todos los métodos que veremos a continuación devuelven un objeto jQuery, para permitir el
encadenamiento de sentencias.
Expandiendo la selección
Método Propósito
add(selector) Añade todos los elementos que cumplen el selector
add(selector, Añade todos los elementos que cumplen el selector dentro del
contexto) contexto
add(HTMLElement)
Añade un elemento o un array de HTMLElements
add(HTMLElement[])
add(jQuery) Añade los contenidos del objeto jQuery
Por ejemplo, podemos obtener un selector para posteriormente reutilizarlo sobre otra
selección:
174
JavaScript
Reduciendo la selección
Si lo que queremos es reducir el conjunto de resultados para refinar la selección, podemos
utilizar los siguientes métodos similar a los empleados en los filtros:
Método Propósito
Elimina todos los elementos a excepción al que ocupa la posición
eq(índice)
índice . Si el indice es negativo, cuenta desde el final.
first() Elimina todos los elementos excepto el primero
has(selector)
has(jQuery) Elimina los elementos que no tengan un descendiente que cumpla
el selector o el objeto jQuery o aquellos descendientes que no
has(HTMLElement) incluyan los objetos HTMLElement especificados
has(HTMLElement[])
last() Elimina todos los elementos excepto el último
Elimina los elementos fuera del rango indicado por los valores de
slice(inicio, fin)
inicio y fin
Por ejemplo, mediante los operadores sencillos podemos restringir el conjunto de resultados
(http://jsbin.com/hufoci/1/edit?html,css,js,output):
$("li").first();
$("li").last();
$("li").eq(1); // segundo elemento
$("li").eq(-1); // último elemento
$("li").slice(0,2); // primeros dos elementos
Otros métodos que permiten restringir el resultado son filter() y not() , los cuales son
complementarios:
Método Propósito
filter(selector) Mantiene todos los elementos que cumplen el selector
filter(HTMLElement) Elimina todos los elementos menos el HTMLElement
Elimina todos los elementos que no están contenidos en el objeto
filter(jQuery)
jQuery
La función se invoca por cada elemento; aquellos en los que la
filter(función(índice))
función devuelva false se eliminarán
not(selector) Elimina los elementos que cumplen el selector
not(HTMLElement[]) Elimina los elementos del array
not(HTMLElement) Elimina el elemento especificado
not(jQuery) Elimina los elementos contenidos en el objeto jQuery
La función se invoca por cada elemento; aquellos en los que la
not(función(índice))
función devuelva true se eliminarán
$("p").filter("[class]")
175
JavaScript
$("p").not("[class]");
Obtener los párrafos cuyo id comience por pa o los que no comiencen por pa
var pa = $("[id^=pa]");
$("p").filter(pa);
$("p").not(pa);
Obtener los párrafos cuya clase es a o el párrafo que ocupa la tercera posición
$("p").filter(function(indice) {
return this.getAttribute("class") == "a" || indice == 2;
});
Navegación descendiente
Para navegar descendentemente podemos emplear los siguientes métodos:
Método Propósito
Selecciona los hijos (descendientes inmediatos) de todos los
children()
elementos del conjunto de resultados
Selecciona todos los elementos que cumplen el selector y que son
children(selector)
hijos del conjunto de resultados
Devuelve los hijos, incluyendo los contenidos de texto u nodos de
contents()
comentarios de todos los elementos
El uso de las relaciones DOM suele emplearse con el método find() (ver “El objeto
jQuery )”para buscar un selector entre los descendientes de la selección. Además, en ambos
casos no se devuelven elementos duplicados.
Por ejemplo, podemos recorrer todos los hijos de un selector de este modo:
Navegación ascendente
Si en cambio estamos interesados en los elementos que están por encima de nuestro conjunto
de resultados usaremos los siguientes métodos:
Método Propósito
parent() Selecciona el padre de cada elemento del objeto jQuery,
parent(selector) pudiéndose filtrar por el selector
176
JavaScript
Método Propósito
parents() Selecciona los ascendentes de cada elemento del objeto jQuery,
parents(selector) pudiéndose filtrar por el selector
parentsUntil(selector1) Selecciona los ascendentes de cada elemento del objeto jQuery
parentsUntil(selector1, hasta que se encuentre una ocurrencia para el selector1. Los
selector2) resultados se pueden filtrar mediante selector2.
parentsUntil(HTMLElement)
parentsUntil(HTMLElement,
selector) Selecciona los ascendentes de cada elemento del objeto jQuery
hasta que se encuentre uno de los elementos especificados. Los
parentsUntil(HTMLElement[])
resultados se pueden filtrar mediante un selector.
parentsUntil(HTMLElement[],
selector)
closest(selector)
Selecciona el ascendente más cercano de cada elemento del
closest(selector, objeto jQuery y realiza la intersección con el selector / contexto.
contexto)
closest(jQuery) Selecciona el ascendente más cercano de cada elemento del
objeto jQuery y realiza la intersección con los elementos contenidos
closest(HTMLElement) en el parámetro.
Encuentra el ascendente posicionado más cercano (tiene valor
offsetParent() fixed , absolute o relative en la propiedad position ).
Útil para trabajar con animaciones
Obtener el padre de todos los enlaces - Obtener aquellos padres de enlaces que tienen
el atributo lang
$("a").parent();
$("a").parent("[lang]");
Obtener los ascendentes (padre, abuelo, etc..) de los elementos cuya clase sea a
$(".a").parents();
Obtener los ascendentes hasta llegar a la etiqueta form de los elementos cuya clase
sea a y que contenga el valor es en el atributo lang
$(".a").parentsUntil('form','[lang*=es]');
Obtiene los ascendentes más cercanos cuya clase sea a , de la imagen cuya fuente
referencie a flickr
$("img[src*=flickr]").closest(".a");
Navegación horizontal
Finalmente, si lo queremos es navegar entre los elementos que están en el mismo nivel,
podemos utilizar los siguientes métodos:
177
JavaScript
Método Propósito
Selecciona todos los hermanos (anteriores y posteriores) para cada
siblings()
uno de los elementos del objeto jQuery .
Selecciona el hermano inmediatamente posterior para cada
next()
elemento del objeto jQuery.
Selecciona todos los hermanos posteriores para cada elemento del
nextAll()
objeto jQuery, pudiéndose filtrar por el selector
nextUntil(selector)Selecciona los hermanos anteriores para cada elemento hasta
nextUntil(jQuery) (sin incluir) un elemento que cumpla el selector o un elemento del
objeto jQuery o del array HTMLElement .
nextUntil(HTMLElement[])
Selecciona el hermano inmediatamente anterior para cada
prev()
elemento del objeto jQuery .
Selecciona todos los hermanos anteriores para cada elemento del
prevAll()
objeto jQuery .
prevUntil(selector)Selecciona los hermanos anteriores para cada elemento hasta
prevUntil(jQuery) (sin incluir) un elemento que cumpla el selector o un elemento del
objeto jQuery o del array HTMLElement .
prevUntil(HTMLElement[])
En ocasiones crearemos contenido de manera dinámica, de modo que jQuery nos ofrece
métodos para crear, copiar, borrar y mover contenido, además de permitir envolver contenido
dentro de otro.
Además, jQuery ofrece soporte Cross-Browser para trabajar con CSS, incluyendo información
sobre posicionamiento y tamaño.
Creando contenido
Para crear nuevo contenido, no tenemos más que pasarle una cadena con el código HTML
a la función $()
Método Propósito
Devuelve el contenido HTML del primer elemento del conjunto de
html()
resultados
Asigna el nuevoContenido HTML a todos los elementos del
html(nuevoContenido)
conjunto de resultados
178
JavaScript
Método Propósito
Devuelve el contenido de todos los elemento del conjunto de
text()
resultados
Asigna el nuevoTexto a todos los elementos del conjunto de
text(nuevoTexto)
resultados
Como podemos observar, al asignar nuevo contenido estamos eliminando el contenido previo
y sustituyéndolo por el nuevo, pero manteniendo los atributos de la etiqueta sobre la que se
añade el contenido.
Método Propósito
val() Devuelve el valor del primer elemento del conjunto de resultados
val(valor) Asigna el valor a todos los elementos del conjunto de resultados
Asigna valores a todos los elementos del conjunto de resultados
val(función)
mediante la función
Por ejemplo:
179
JavaScript
$("input").each(function(indice, elem) {
console.log("Nombre: " + elem.name + " Valor: " + $(elem).val());
});
$("input").val(function(indice, valorActual) {
return (indice + 1) * 100;
});
Método Propósito
Accede a la propiedad del primero elemento del conjunto de
resultados. Éste método facilita obtener el valor de propiedades.
attr(nombre)
Si el elemento no tiene un atributo con dicho nombre, devuelve
undefined
Asigna una serie de atributos en todos los elementos del conjunto
de resultado mediante la sintaxis de notación de objeto, lo que
attr(objetoPropiedades)permite asignar un gran número de propiedades de una sola
vez $("img").attr({ src:"/imagenes/logo.gif",
title:"JavaUA", alt: "Logo JavaUA"});
Asigna valor a la propiedad clave a todos los elementos del
attr(clave, valor)
conjunto de resultados
Asigna una única propiedad a un valor calculado para todos los
attr(clave, función) elementos del conjunto de resultados. En vez de pasar un valor
mediante una cadena, la función devolverá el valor del atributo.
Elimina el atributo nombre de todos los elementos del conjunto de
removeAttr(nombre)
resultados
Vamos a estudiar algunos ejemplos, suponiendo que tenemos un enlace con una imagen del
siguiente modo:
$("a").attr("target","_blank");
$("a").removeAttr("href");
$("img").attr({src:"imagenes/batman.jpg", alt:"Batman"});
180
JavaScript
Insertando contenido
jQuery ofrece varios métodos para insertar nuevo contenido en el documento, tanto antes
como después del contenido de los elementos de la página.
Método Propósito
Anexa contenido (al final) dentro de cada elemento del conjunto de
append(contenido)
resultados
Traslada todos los elementos del conjunto de resultados detrás de
appendTo(selector)
los encontrados por el selector
Añade contenido (al inicio) dentro de cada elemento del conjunto
prepend(contenido)
de resultados
Traslada todos los elementos del conjunto de resultados delante de
prependTo(selector)
los encontrados por el selector
El contenido de los métodos puede ser contenido HTML, un objeto jQuery , un array de
HTMLElement o una función que devuelva el contenido.
<p>Párrafo 1</p>
<p>Párrafo 2</p>
$("p:last").prependTo("p:first");
// <p>Párrafo 2</p>
// <p>Párrafo 1</p>
Autoevaluación
Suponiendo que tenemos una capa que contiene una capa con artículos
11
¿Qué realiza el siguiente código ?
$('#articulo').find('span.co').each(function() {
var $this = $(this);
11
Crea una cita con el contenido que había en la capa span.co
181
JavaScript
$('<blockquote></blockquote>', {
class: 'co',
text: $this.text()
}).prependTo( $this.closest('p') );
});
Método Propósito
Inserta el contenido detrás de cada elemento del conjunto de
after(contenido)
resultados
Inserta el contenido antes de cada elemento del conjunto de
before(contenido)
resultados
Inserta todos los elementos del conjunto de resultados detrás de
insertAfter(selector)
los encontrados por el selector
Inserta todos los elementos del conjunto de resultados delante de
insertBefore(selector)
los encontrados por el selector
$("p").after("<p>Párrafo separado</p>");
$("<p>Párrafo separado</p>").insertAfter("p");
// <p>Párrafo 1</p>
// <p>Párrafo separado</p>
// <p>Párrafo 2</p>
// <p>Párrafo separado</p>
Por ejemplo, si queremos mover un elemento una posición hacia abajo, dentro de after
podemos crear una función anónima que devuelva el contenido a mover (además nos sirve
para hacer un filtro):
$("p:eq(1)").after(function() {
return $(this).prev();
});
Método Propósito
Clona los elementos del conjunto de resultados y selecciona los
clone()
clonados
Clona los elementos del conjunto de resultados y todos sus
clone(bool) manejadores de eventos y selecciona los clonados (pasándole
true )
182
JavaScript
Modificando el contenido
jQuery permite envolver contenido en la página, sustituir contenido, copiar y eliminarlo,
mediante los siguientes métodos.
Si nos centramos en los métodos para envolver contenido, de manera que les añadiremos
un padre, tenemos:
Método Propósito
Envuelve cada elemento del conjunto de resultados con el
wrap(html)
contenido html especificado
Envuelve cada elemento del conjunto de resultados con el
wrap(elemento)
elemento especificado
Envuelve todos los elementos del conjunto de resultados con el
wrapAll(html)
contenido html especificado
Envuelve todos los elementos del conjunto de resultados con un
wrapAll(elemento)
único elemento
Envuelve los contenidos de los hijos internos de cada elemento
wrapInner(html) del conjunto de resultados (incluyendo los nodos de texto) con una
estructura html
Envuelve los contenidos de los hijos internos de cada elemento
wrapInner(elemento) del conjunto de resultados (incluyendo los nodos de texto) con un
elemento DOM
Envuelve cada párrafo con una capa de color rojo (tantas capas como párrafos)
Envuelve todos los párrafos con una capa de color rojo (una capa que envuelve a todos
los párrafos)
Envuelve el contenido de todos los párrafos con una capa de color rojo
183
JavaScript
Método Propósito
Sustituye todos los elementos del conjunto de resultados con el
replaceWith(contenido)
contenido especificado, ya sea HTML o un elemento DOM
Sustituye los elementos encontrados por el selector con los del
replaceAll(selector)
conjunto de resultados, es decir, lo contrario a replaceWith .
empty() Elimina todos los nodos hijos del conjunto de resultados
remove() Elimina todos los elementos del conjunto de resultados del DOM
detach() Igual que remove() pero devuelve los elementos eliminados.
detach(selector) Esto permite volver a insertarlos en otra parte del documento
Elimina el padre de cada uno de los elementos del conjunto de
unwrap() resultados, de modo que los elementos se convierten en hijos de
sus abuelos.
$("p").replaceWith("<div>Capa Nueva</div>");
$("<div>Capa Nueva</div>").replaceAll("p");
$("ul").empty();
$("ul").remove();
var listaBorrada = $("ul").detach();
Método Propósito
Devuelve el valor de la propiedad CSS nombre del primer elemento
css(nombre)
del conjunto de resultados
Asigna las propiedades de cada elemento del conjunto de
resultados mediante la sintaxis de objeto: var cssObj =
css(propiedades)
{'background-color':'blue';'font-weight':'bold'};
$(this).set(cssObj);
Asigna un valor a una única propiedad CSS. Si se pasa un número,
css(propiedad,
se convertirá automáticamente a un valor de píxel, a excepción de:
valor)
z-index , font-weight , opacity , zoom y line-height .
184
JavaScript
var valoresCSS = {
"font-size": "1.5em",
"color": "blue"
};
$("p").css("font-size", valoresCSS);
Además, jQuery también ofrece métodos para trabajar con las clases CSS y poder añadir,
borrar, detectar o intercambiar sus clases:
Método Propósito
addClass(clase1 Añade la clase especificada a cada elemento del conjunto de
[clase2]) resultados
Devueve true si está presenta la clase en al menos uno de los
hasClass(clase)
elementos del conjunto de resultados
removeClass(clase Elimina la clase de todos los elementos del conjunto de resultados
[clase2])
toggleClass(clase Si no está presente, añade la clase. Si está presente, la elimina.
[clase2])
toggleClass(clase, Si cambio es true añade la clase, pero la elimina en el caso de
cambio) que cambio sea false .
$("p").addClass("a");
$("p:even").removeClass("b").addClass("a");
$("p").toggleClass("a");
Respecto al posicionamiento CSS, jQuery ofrece métodos que soportan Cross-Browser para
averiguar la posición de los elementos:
Método Propósito
Obtiene el desplazamiento actual (en píxeles) del primer elemento
offset()
del conjunto de resultados, respecto al documento
Devuelve una colección jQuery con el padre posicionado del primer
offsetParent()
elemento del conjunto de resultados
185
JavaScript
Método Propósito
Obtiene la posiciones superior e izquierda de un elemento relativo
position()
al desplazamiento de su padre
Obtiene el desplazamiento del scroll superior del primer elemento
scrollTop()
del conjunto de resultados
Asigna el valor del desplazamiento del scroll superior a todos los
scrollTop(valor)
elementos del conjunto de resultados
Obtiene el desplazamiento del scroll superior del primer elemento
scrollLeft()
del conjunto de resultados
Asigna el valor del desplazamiento del scroll izquierdo a todos los
scrollLeft(valor)
elementos del conjunto de resultados
Método Propósito
Obtiene la altura calculada en píxeles del primer elemento del
height()
conjunto de resultados
Asigna la altura CSS para cada elemento del conjunto de
height(valor)
resultados
Obtiene la anchura calculada en píxeles del primer elemento del
width()
conjunto de resultados
Asigna la anchura CSS para cada elemento del conjunto de
width(valor)
resultados
Obtiene la altura interna (excluye el borde e incluye el padding)
innerHeight()
para el primer elemento del conjunto de resultados
Obtiene la anchura interna (excluye el borde e incluye el padding)
innerWidth()
para el primer elemento del conjunto de resultados
Obtiene la altura externa (incluye el borde y el padding) para el
outerHeight(margen) primer elemento del conjunto de resultados. Si el margen es true ,
también se incluyen los valores del margen.
Obtiene la anchura externa (incluye el borde y el padding) para el
outerWidth(margen) primer elemento del conjunto de resultados. Si el margen es true ,
también se incluyen los valores del margen.
div#capa {
width: 250px;
height: 180px;
margin: 10px;
padding: 20px;
background: blue;
border: 2px solid black;
cursor: pointer;
186
JavaScript
}
p, span {
font-size: 16pt;
}
Si en el documento HTML tenemos capas con ids para imprimir el valor de las propiedades
mediante este código:
$("#height").html($("#capa").height());
$("#width").html($("#capa").width());
$("#innerH").html($("#capa").innerHeight());
$("#innerW").html($("#capa").innerWidth());
$("#outerH").html($("#capa").outerHeight());
$("#outerW").html($("#capa").outerWidth());
$("#offset").html($("#capa").offset().top + " - " +
$("#capa").offset().left);
$("#position").html($("#capa").position().top + " - " +
$("#capa").position().left);
Método Propósito
Obtiene el atributo data-clave del primer elemento del conjunto
data(clave)
de resultados
Almacena el valor en el atributo data-clave en cada elemento
data(clave, valor)
del conjunto de resultados
Elimina todos los atributos de datos de cada elemento del conjunto
removeData()
de resultados
187
JavaScript
Método Propósito
Elimina el atributo data-clave de cada elemento del conjunto de
removeData(clave)
resultados
Es mejor acceder a los elementos por su atributo data que por el texto
que contienen sus atributos, ya que éste último puede cambiar debido a
la internacionalización (18n) de la aplicación.
$("#listado").data("tipo","tutorial");
$("#listado").data("codigo", 123);
$("#listado").removeData("tipo");
6.7. Eventos
Igual que estudiamos en la sesión de JavaScript vamos a ver como jQuery simplifica, y mucho,
el tratamiento de los eventos.
Evento document.ready
Del mismo modo que con JavaScript básico, si ponemos un selector jQuery en el encabezado
que referencia a elementos DOM que no se han cargado, no va a funcionar. En cambio, si
movemos el selector a una posición previa a cerrar el body irá todo correctamente. Para
asegurarnos, de manera similar a window.onload , mediante jQuery usaremos el siguiente
fragmento:
188
JavaScript
$(document).ready(function() {
// código jQuery
}
La ventaja de usar jQuery es que permite usar varias funciones sobre el mismo evento, ya que
mediante window.onload sólo se ejecutará la última función que le hayamos asignado.
$(document).ready(function() {
// código alfa
}
$(document).ready(function() {
// código beta
}
Además, mientras que con window.onload el evento espera a que haya cargado toda
la página, incluidas las imágenes, con jQuery se lanzará cuando haya cargado el DOM,
sin tener que esperar a descargar todas las imágenes. Esto se conoce como el evento
document.ready .
Si queremos poner el mismo código pero de manera simplificada, aunque menos legible,
podemos hacerlo del siguiente modo:
$(function() {
// código jQuery
});
Es decir, cada vez que le pasemos una función a jQuery() o $() , le diremos al navegador
que espere hasta que el DOM haya cargado completamente para ejecutar el código.
Antes de jQuery 1.7, en vez de on() , se usaba bind() con jQuery 1.0,
live() con la versión 1.3 y delegate() a partir de la versión 1.4.2
$(selector).on("nombreDelEvento", function() {
// código del manejador
189
JavaScript
})
$("p").on("click", function() {
console.log("Click sobre un párrafo");
})
Si queremos capturar más de un evento, hemos de separar el nombre de los eventos con
espacio:
Cuando aplicamos el evento on , en vez de seleccionar todos los elementos de un tipo que
hay en DOM, es mejor elegir el padre del que estamos interesado y luego filtrar dentro del
método. Por ejemplo, en vez de:
$("dt").on("mouseenter", function() {
$("dl").on("mouseenter","dt",function() {
Ejemplo eventos
Vamos a realizar un ejemplo para ver a jQuery en acción. Vamos a cambiar los estilos de una
caja cuando el ratón pase por encima del mismo. Si hacemos click sobre la caja, eliminaremos
el evento.
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<style type="text/css">
.normal {
width:300px; height:200px; background-color:yellow; font-size:18pt;
}
.resaltado {
background-color:red;
}
</style>
</head>
<body>
<h1>Eventos con jQuery</h1>
<div id="destinoEvento" class="normal">Pasar el ratón para ver el efecto.
Click para quitar/añadir el manejador.</div>
190
JavaScript
</body>
</html>
$(document).ready(function() {
var dest = $("#destinoEvento");
dest.on("mouseover mouseleave", function(evt) {
dest.toggleClass("resaltado");
});
dest.on("click", function(evt) {
dest.off("mouseover mouseleave");
$("#destinoEvento").text("Manejadores eliminados");
});
});
Guardamos el destino del enlace en una variable para evitar repetir la búsqueda
Capturamos los eventos de mouseover y mouseleave sobre la caja y le cambiamos
la clase del estilo dentro del manejador
Dentro de la captura del evento click , eliminamos los manejadores de los eventos
mouseover y mouseleave e informamos al usuario del cambio. Si el destino tuviese
más manejadores sobre estos eventos, también se eliminarían.
Métodos auxiliares
jQuery ofrece un conjunto de métodos auxiliares para simplificar su uso con los eventos más
utilizados. Si los ordenamos por su categoría tenemos:
$("p").click(function() {
// Manejador
});
$("p").on("click", function() {
// Manejador
});
191
JavaScript
Un caso particular que conviene estudiar es el método hover() , el cual acepta dos
manejadores, los cuales se ejecutarán cuando el ratón entre y salga del elemento
respectivamente:
$(function() {
$("#destino").hover(resaltar, resaltar);
$("#destino").click(fnClick1);
$("#destino").dblclick(fnClick2);
});
function resaltar(evt) {
$("#destino").toggleClass("resaltado");
}
function fnClick1() {
$("#destino").html("Click!");
}
function fnClick2() {
$("#destino").html("Doble Click!");
}
También podemos usar los siguientes métodos para realizar tareas específicas:
Método Propósito
one(tipo, datos, Permite capturar el evento tipo una sola vez para cada elemento
manejador) del conjunto de resultado
Lanza un evento para cada elemento del conjunto de resultados,
trigger(evento, lo que también provoca que se ejecute la acción predeterminada
datos) por el navegador. Por ejemplo, si le pasamos un evento click , el
navegador actuará como si se hubiese clickado dicho elemento.
Dispara todos los manejadores asociados al evento sin ejecutar la
triggerHandler(evento,
acción predeterminada o burbujero del navegador. Sólo funciona
datos)
para el primer elemento del conjunto de resultados del selector
Por ejemplo, supongamos que tenemos varios cuadrados y queremos que cambien su color
una sola vez:
<!DOCTYPE html>
<html lang="es">
<head>
<title>Usando el objeto jQuery Event</title>
<meta charset="UTF-8">
<style type="text/css">
div {
width: 60px; height: 60px; margin: 10px; float: left;
background: blue; border: 2px solid black; cursor: pointer;
}
p {
font-size: 18pt;
192
JavaScript
}
</style>
</head>
<body>
<p>Click en cualquier cuadrado para cambiar su color</p>
<div></div>
<div></div>
<div></div>
<div></div>
</body>
</html>
$(function() {
$("div").one("click", function(evt) {
$(this).css({
background: "red", cursor: "auto"
});
console.log("Evento " + evt.type + " en div " + $(this).attr("id"));
});
});
Si hacemos click más de una ocasión sobre un mismo cuadrado podemos comprobar como
no sale por consola.
Este objeto se obtiene como parámetro de la función manejadora del evento. Para mostrar
estas propiedades en acción, vamos a basarnos en el siguiente código donde podemos ver
tres capas:
<!DOCTYPE html>
193
JavaScript
<html lang="es">
<head>
<title>Usando el objeto jQuery Event</title>
<meta charset="UTF-8">
<style type="text/css">
.normal {
width:300px; height:200px; background-color: silver;
font-size:18pt; margin:5pt 5pt 5pt 5pt;
}
</style>
</head>
<body>
<h1>Usando el objeto jQuery Event</h1>
<div id="div1" class="normal">Click en esta capa (div1) para ver la
información del evento</div>
<div id="div2" class="normal">Click en esta capa (div2) para ver la
información del evento</div>
</body>
</html>
$(function() {
$("div").click(function(evt) {
$(this).html("pageX: " + evt.pageX + ", pageY: " + evt.pageY + ",
tipo: " + evt.type + ", target: " + evt.target);
});
});
Eventos personalizados
Sabemos que una vez seleccionado un elemento, mediante el método on podemos capturar
el evento y controlar el código del manejador mediante una función anónima.
Ya hemos visto que si queremos lanzar un evento de manera programativa, sin que lo haga
el usuario, podemos usar el método trigger(evento,datos) :
194
JavaScript
$('body').on('batClick', function() {
console.log("Capturado el evento personalizado batClick");
});
$('body').trigger('batClick');
Veamos un ejemplo práctico. Supongamos que accedemos a un servicio REST que devuelve
JSON como el de OMDB visto en la sesión anterior:
var datos;
$.getJSON('http://www.omdbapi.com/?
s=batman&callback=?', function(resultado) {
datos = resultado;
});
console.log(datos); // undefined
Tal como vimos, se trata de una llamada asíncrona de modo que cuando mostramos los datos
recibidos no obtenemos nada porque realmente la llamada AJAX todavía no ha finalizado.
Para poder controlar cuando se han cargado los datos podemos lanzar un evento
personalizado de modo que al capturarlo estaremos seguro que la petición ha finalizado.
$.getJSON('http://www.omdbapi.com/?s=batman&callback=?', function(datos) {
$(document).trigger('obd/datos', datos); // publicador
});
195
JavaScript
6.8. this
Ya sabemos que al cargar una página, si referenciamos a this desde la raíz apunta al objeto
window . En cambio, si lo hacemos desde dentro una función, siempre apuntará a su padre.
Es decir, si la función se llama desde el click de un enlace, this contendrá el enlace.
$("#clickeame").click(function(evt) {
console.log(this); // HTMLButtonElement
console.log(this.type); // "submit"
})
Una acción que se utiliza mucho es convertir el elemento HTML a un objeto jQuery:
Modificando this
Si queremos especificar qué va a ser this al pasarle el control a una función, podemos usar
un proxy mediante:
$.proxy(nombreDeLaFuncion, objetoQueSeraThis);
Supongamos que rescribimos el ejemplo anterior mediante un módulo, del siguiente modo:
var manejador = {
type: "Mi manejador de eventos",
manejadorClick: function(evt) {
console.log(this.type);
}
};
$(function() {
$("#clickeame").click(manejador.manejadorClick);
});
Si volvemos al ejemplo anterior, y en vez de obtener mediante this el botón queremos que
tome el del manejadores haremos:
$(function() {
$("#clickeame").click($.proxy(manejador.manejadorClick, manejador));
196
JavaScript
});
6.9. Ejercicios
(0.2 ptos) Ejercicio 61. Recuperando contenido
A partir del siguiente documento con enlaces, añadir el código jQuery necesario para que al
lado de cada enlace muestre mediante una imagen el formato del archivo (pdf, html) o el tipo
de enlace (sólo para email).
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<title>Ejercicio 61. Enlaces</title>
</head>
<body>
Enlaces
<ul>
<li><a href="algunsitio.html">Enlace 1</a></li>
<li><a name="#ancla1">Ancla de Enlace</a></li>
<li><a href="algunsitio.html">Enlace 2</a></li>
<li><a href="algunsitio.pdf">Enlace 3</a></li>
<li><a href="algunsitio.html">Enlace 4</a></li>
<li><a href="algunsitio.pdf">Enlace 5</a></li>
<li><a href="algunsitio.pdf">Enlace 6</a></li>
<li><a href="mailto:batman@heroes.com">Enlace email</a></li>
</ul>
</body>
</html>
197
JavaScript
• cambia los datos del autor y email con tus datos personales
• sustituye la tabla de contenidos con una lista desordenada que sólo contenga los títulos de
primer nivel, con enlaces al lugar correspondiente
• añade un enlace al final de cada capítulo para volver al inicio del documento.
• añade dos botones (A+, a-) tras el título del documento que modifiquen el tamaño del texto
para hacerlo más grande o más pequeño.
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<title>Ejercicio 63. Tablas</title>
<style type="text/css">
th,td {
font-family: Verdana, Arial, Helvetica; font-size: 18px; color: #000000;
}
tr {
border: 1px solid gray;
}
td {
width:200px; padding:3px;
}
th {
background-color:#D2E0E8; color:#003366
}
table {
border: 1pt solid gray;
}
.clickable {
cursor:pointer;
}
</style>
</head>
<body>
<h1>Usando jQuery para resaltar filas de una tabla</h1>
<table id="laLista">
198
JavaScript
<thead>
<tr><th>Producto</th><th>Precio</th></tr>
</thead>
<tbody>
<tr><td>Leche</td><td>1.99</td></tr>
<tr><td>Huevos</td><td>2.29</td></tr>
<tr><td>Mantequilla</td><td>3.49</td></tr>
<tr><td>Pan</td><td>0.99</td></tr>
<tr><td>Macarrones</td><td>1.19</td></tr>
<tr><td>Miel</td><td>4.39</td></tr>
<tr><td>Galletas</td><td>2.99</td></tr>
</tbody>
</table>
</body>
</html>
.par {
background-color:#0f0;
}
.impar {
background-color:#afa;
}
.resaltado {
background-color: #ffcc00;
font-weight:bold;
}
• Al clickar sobre una fila, mostrará una alerta con el producto y su precio
199
JavaScript
7. jQuery Avanzado
7.1. Efectos
jQuery facilita el uso de animaciones y efectos consiguiendo grandes resultados con apenas
un par de líneas, como por ejemplo:
Mostrar y ocultar
Tanto mostrar como ocultar elementos son acciones comunes y sencillas que podemos
realizar de manera inmediata o durante un período de tiempo.
Método Propósito
Muestra cada elemento del conjunto de resultados, si estaban
show()
ocultos
Muestra todos los elementos del conjunto del resultados mediante
show(velocidad[,callback])
una animación, y opcionalmente lanza un callback tras completar la
animación
Oculta cada elemento del conjunto de resultados, si estaban
hide()
visibles
Oculta todos los elementos del conjunto del resultados mediante
hide(velocidad[,callback])
una animación, y opcionalmente lanza un callback tras completar la
animación
Cambia la visualización (visible u oculto, de manera contraria a su
toggle()
estado) para cada elemento del conjunto de resultados
Cambia la visualización para cada elemento del conjunto de
toggle(switch) resultados dependiendo del switch (verdadero muestra todos los
elementos, falso para ocultarlos)
Cambia la visualización de todos los elementos del conjunto del
toggle(velocidad[,callback])
resultados mediante una animación, y opcionalmente lanza un
callback tras completar la animación
Por ejemplo, supongamos que tenemos un cuadrado con varios botones para cambiar su
visualización:
<!DOCTYPE html>
200
JavaScript
<html lang="es">
<head>
<title>Mostrar y Ocultar</title>
<meta charset="UTF-8">
<style type="text/css">
div#laCapa {
width: 250px; height: 180px; margin: 10px; padding: 20px;
background: blue; border: 2px solid black; cursor: pointer;
}
p, span {
font-size: 16pt;
}
button {
margin: 5px;
}
</style>
</head>
<body>
<p>Mostrando y Ocultando un elemento</p>
<div id="laCapa"></div>
<button id="mostrar">Mostrar</button>
<button id="ocultar">Ocultar</button>
<button id="cambiar">Cambiar (Toggle)</button>
</body>
</html>
$(function() {
$("#mostrar").click(function() {
$("#laCapa").show("normal");
});
$("#ocultar").click(function() {
$("#laCapa").hide(2000); // ms
});
$("#cambiar").click(function() {
$("#laCapa").toggle("slow");
});
});
Aparecer y desvanecer
Los efectos más comunes se basan en la aparición progresiva y desvanecimiento del
contenido de manera completa o hasta una determinada opacidad. jQuery ofrece un conjunto
de métodos para estos efectos:
Método Propósito
El contenido aparece a la velocidad indicada para cada elemento
fadeIn(velocidad[,callback])
del conjunto de resultados, y opcionalmente lanza un callback tras
completar la animación
El contenido se desvanece a la velocidad indicada para cada
fadeOut(velocidad[,callback])
elemento del conjunto de resultados, y opcionalmente lanza un
callback tras completar la animación
201
JavaScript
Método Propósito
El contenido cambia a la opacidad y velocidad indicadas para cada
fadeTo(velocidad,opacidad[,callback])
elemento del conjunto de resultados, y opcionalmente lanza un
callback tras completar la animación
Ya hemos visto que si queremos que se ejecute una función cuando termine la ejecución, la
pasaremos como callback:
$(this).fadeOut(1000, function() {
console.log("He acabado");
});
$(this).fadeOut(500, function() {
$(this).delay(2000).fadeIn(500);
});
Si hubiésemos puesto la función fuera del parámetro del fadeOut , se hubiese ejecutado
justamente después de iniciarse la animación, y no al finalizar la misma.
$(this).fadeOut(500).css("margin","50 px");
De manera similar al ejemplo anterior, vamos a dibujar un cuadrado con 4 botones para mostrar
estos efectos:
<!DOCTYPE html>
<html lang="es">
<head>
<title>Aparecer y Desvanecer</title>
<meta charset="UTF-8">
<style type="text/css">
div#laCapa {
width: 250px; height: 180px; margin: 10px; padding: 20px;
background: blue; border: 2px solid black; cursor: pointer;
}
p, span {
font-size: 16pt;
}
button {
margin: 5px;
}
</style>
</head>
<body>
<p>Aparecer y Desvanecer un elemento</p>
<div id="laCapa"></div>
<button id="aparecer">Aparecer</button>
<button id="desvanecer">Desvanecer</button>
<button id="fade03">Opacidad hasta .3</button>
202
JavaScript
$(function() {
$("#aparecer").click(function() {
$("#laCapa").fadeIn(300);
});
$("#desvanecer").click(function() {
$("#laCapa").fadeOut("normal");
});
$("#fade03").click(function() {
$("#laCapa").fadeTo("slow", 0.3);
});
$("#fade10").click(function() {
$("#laCapa").fadeTo("slow", 1.0);
});
});
Enrollar y desenrollar
jQuery ofrece un conjunto de métodos para enrollar y desenrolar los elementos a modo de
persiana:
Método Propósito
El contenido se desenrolla a la velocidad indicada modificando
slideDown(velocidad[,callback])
la altura de cada elemento del conjunto de resultados , y
opcionalmente lanza un callback tras completar la animación
El contenido se enrolla a la velocidad indicada modificando la altura
slideUp(velocidad[,callback])
de cada elemento del conjunto de resultados , y opcionalmente
lanza un callback tras completar la animación
Cambia la visualización del contenido enrollando o desenrollando
el contenido a la velocidad indicada modificando la altura de cada
slideToggle(velocidad[,callback])
elemento del conjunto de resultados, y opcionalmente lanza un
callback tras completar la animación
De manera similar al ejemplo anterior, vamos a dibujar un cuadrado con 3 botones para mostrar
estos efectos:
<!DOCTYPE html>
<html lang="es">
<head>
<title>Enrollar y Desenrollar</title>
<meta charset="UTF-8">
<style type="text/css">
div#laCapa {
width: 250px; height: 180px; margin: 10px; padding: 20px;
background: blue; border: 2px solid black; cursor: pointer;
}
203
JavaScript
p, span {
font-size: 16pt;
}
button {
margin: 5px;
}
</style>
</head>
<body>
<p>Enrollando y Desenrollando un elemento</p>
<div id="laCapa"></div>
<button id="enrollar">Enrollar</button>
<button id="desenrollar">Desenrollar</button>
<button id="cambiar">Cambiar (Toggle)</button>
</body>
</html>
$(function() {
$("#enrollar").click(function() {
$("#laCapa").slideUp("normal");
});
$("#desenrollar").click(function() {
$("#laCapa").slideDown(2000);
});
$("#cambiar").click(function() {
$("#laCapa").slideToggle("slow");
});
});
Creando animaciones
Para crear animaciones personalizadas sobre las propiedades de los elementos llamaremos
a la función animate() , y para detenerlas a stop() .
Método Propósito
Crea una animación personalizada donde parámetros indica un
animate(parámetros,
objeto CSS con las propiedades a animar, con una duración y
duración,
easing (linear o swing) determinados y lanza un callback tras
easing, callback)
completar la animación
Crea una animación personalizada donde parametros indica las
animate(parametros,
propiedades a animar y las opciones de las animación (complete,
opciones)
step, queue)
stop() Detiene todas las animaciones en marcha para todos los elementos
De manera similar al ejemplo anterior, vamos a dibujar un cuadrado con 4 botones para
cambiar el tamaño de un elemento, el tamaño del texto, mover el elemento y hacerlo todo a
la vez:
<!DOCTYPE html>
204
JavaScript
<html lang="es">
<head>
<title>Animaciones</title>
<meta charset="UTF-8">
<style type="text/css">
div#laCapa {
position: relative; width: 250px; height: 180px; margin: 10px; padding:
20px;
background: blue; border: 2px solid black; cursor: pointer;
}
p, span {
font-size: 16pt;
}
button {
margin: 5px;
}
</style>
</head>
<body>
<p>Animaciones</p>
<div id="laCapa">Anímame un poco!</div>
<button id="derecha">Crecer a la derecha</button>
<button id="texto">Texto grande</button>
<button id="mover">Mover la capa</button>
<button id="todo">Todo</button>
</body>
</html>
$(function() {
$("#derecha").click(function() {
$("#laCapa").animate({ width: "500px" }, 1000);
});
$("#texto").click(function() {
$("#laCapa").animate({ fontSize: "24pt" }, 1000);
});
$("#mover").click(function() {
$("#laCapa").animate({ left: "500" }, 1000, "swing");
});
$("#todo").click(function() {
$("#laCapa").animate({ width: "500px", fontSize: "24pt", left: "500"
}, 1000, "swing");
});
});
Ejemplo carrusel
Para poner estos efectos en practica y por comparar funcionalidad realizada en JavaScript
respecto a la misma con jQuery, vamos a realizar un carrusel de imagenes de manera similar
a la realizada en sesiones anteriores.
Para ello, vamos a definir una capa para cada una de las imágenes
<!DOCTYPE html>
205
JavaScript
<html lang="es">
<head>
<title>Carrusel jQuery</title>
<meta charset="UTF-8">
<style type="text/css">
#carrusel {
height:400px; width:400px;
}
#carrusel div {
position:absolute; z-index: 0;
}
#carrusel div.anterior {
z-index: 1;
}
#carrusel div.actual {
z-index: 2;
}
</style>
</head>
<body>
<h1>Carrusel jQuery</h1>
<div id="carrusel">
<div class="actual"><img src="imagenes/
hierba.jpg" width="400" height="400" class="galeria" /></div>
<div><img src="imagenes/hoja.jpg"
width="400" height="400" class="galeria" /></div>
<div><img src="imagenes/
primavera.jpg" width="400" height="400" class="galeria" /></div>
<div><img src="imagenes/
agua.jpg" width="400" height="400" class="galeria" /></div>
</div>
</body>
</html>
A continuación, añadimos el código jQuery para que cada 2 segundos cambie de imagen,
teniendo en cuenta que al llegar a la última, vuelva a mostrar la primera:
$(function () {
setInterval("carruselImagenes()", 2000);
});
function carruselImagenes() {
var fotoActual = $('#carrusel div.actual');
var fotoSig = fotoActual.next();
if (fotoSig.length == 0) {
fotoSig = $('#carrusel div:first');
}
fotoActual.removeClass('actual').addClass('anterior');
fotoSig.css({ opacity: 0.0 }).addClass('actual')
.animate({ opacity: 1.0 }, 1000,
function () { fotoActual.removeClass('anterior'); });
}
206
JavaScript
Cambiamos la clase CSS de la foto actual para que se posicione detrás y se oculte
La siguiente foto la hacemos transparente, la marcamos como actual
Le añadimos una animación de 1 segundo en la que pasa de transparente a visible
Al terminar la animación, le quitamos la clase de anterior para que pase al frente
$.fx.off = true;
// Volvemos a activar los efectos
$.fx.off = false;
Al estar deshabilitadas, los elementos aparecerán y desaparecer sin ningún tipo de animación.
7.2. AJAX
Todas las peticiones AJAX realizadas con jQuery se realizan con el método $.ajax() (http://
api.jquery.com/category/ajax/).
Para simplificar el trabajo, jQuery ofrece varios métodos que realmente son sobrecargas sobre
el método $.ajax() . Si comprobamos el código fuente de este función podremos ver como
es mucho más complejo que lo que estudiamos en la sesión de JavaScript.
Método Propósito
selector.load(url) Incrustra el contenido crudo de la url sobre el selector
Realiza una petición GET a la url. Una vez recuperada la
$.get(url, callback,
respuesta, se invocará el callback el cual recuperará los datos del
tipoDatos)
tipoDatos
$.getJSON(url, Similar a $.get() pero recuperando datos en formato JSON
callback)
$.getScript(url, Carga un archivo JavaScript de la url mediante un petición GET, y
callback) lo ejecuta.
$.post(url, Realiza una petición POST a la url, enviando los datos como
datos, callback) parámetros de la petición
Todos estos métodos devuelven un objeto jqXHR el cual abstrae el mecanismo de conexión,
ya sea un objeto HTMLHttpRequest , un objeto XMLHTTP o una etiqueta <script> .
$.ajax()
Ya hemos comentado que el método $.ajax() es el que centraliza todas las llamadas
AJAX. Pese a que normalmente no lo vamos a emplear directamente, conviene conocer todas
las posibilidades que ofrece. Para ello, recibe como parámetro un objeto con las siguientes
propiedades:
Propiedad Propósito
url URL a la que se realiza la petición
207
JavaScript
Propiedad Propósito
type Tipo de petición (GET o POST)
dataType Tipo de datos que devuelve la petición, ya sea texto o binario.
Callback que se invoca cuando la petición ha sido exitosa y que
success
recibe los datos de respuesta como parámetro
error Callback que se invoca cuando la petición ha fallado
complete Callback que se invoca cuando la petición ha finalizado
$.ajax({
url: "fichero.txt",
type: "GET",
dataType: "text",
success: todoOK,
error: fallo,
complete: function(xhr, estado) {
console.log("Peticion finalizada " + estado);
}
});
function todoOK(datos) {
console.log("Todo ha ido bien " + datos);
}
Si quisiéramos incrustar el contenido recibido por la petición, en vez de sacarlo por consola,
lo podríamos añadir a una capa o un párrafo:
function todoOK(datos) {
$("#resultado").append(datos);
}
load()
Si queremos incrustrar contenido proveniente de una URL, con la misma funcionalidad que un
include estático en JSP, usaremos el método load(url) . Por ejemplo:
$("body").load("contacto.html");
De este modo incluiríamos hasta la cabecera del documento html. Para indicarle que estamos
interesados en una parte del documento, podemos indicar que cargue el elemento cuya clase
CSS sea contenido .
$("body").load("contacto.html .contenido");
208
JavaScript
Mediante este método vamos a poder incluir contenido de manera dinámica al lanzarse un
evento. Supongamos que tenemos un enlace a contacto.html . Vamos a modificarlo para
que en vez de redirigir a la página, incruste el contenido:
<a href="contacto.html">Contacto</a>
<div id="contenedor"></div>
<script>
$('a').on('click', function(evt) {
var href = $(this).attr('href');
$('#contenedor').load(href + ' .contenido');
evt.preventDefault();
});
</script>
Así pues, el mismo ejemplo visto anteriormente puede quedar reducido al siguiente fragmento:
$.get("fichero.txt", function(datos) {
console.log(datos);
});
En el caso de XML, seguiremos usando el mismo método pero hemos de tener en cuenta el
formato del documento. Supongamos que tenemos el siguiente documento heroes.xml :
<heroe>
<nombre>Batman</nombre>
<email>batman@heroes.com</email>
</heroe>
Para poder recuperar el contenido hemos de tener el cuenta que trabajaremos con las
funciones DOM que ya conocemos:
$.get("heroes.xml", function(datos) {
var nombre = datos.getElementsByTagName("nombre")[0];
var email = datos.getElementsByTagName("email")[0];
var val = nombre.firstChild.nodeValue + " " + email.firstChild.nodeValue;
$("#resultado").append(val);
},"xml");
209
JavaScript
Por ejemplo, supongamos que queremos acceder a Flickr para obtener las imágenes que
tienen cierta etiqueta:
function formateaImagenes(datos) {
$.each(datos.items, function(i, elemento) {
$("<img>").attr("src", elemento.media.m).appendTo("#contenido");
if (i === 4) {
return false;
}
});
}
$.each()
La utilidad $.each() se trata de un método auxiliar que ofrece jQuery para
iterar sobre una colección, ya sea:
Finalmente, en ocasiones necesitamos inyectar código adicional al vuelo. Para ello, podemos
recuperar un archivo JavaScript y que lo ejecute a continuación mediante el método
$.getScript(urlScript, callback) . Una vez finalizada la ejecución del script, se
invocará al callback.
210
JavaScript
$("#resultado").html("<strong>getScript</strong>");
Vamos a crear un ejemplo con un formulario sencillo para realizar el envío mediante AJAX:
Ejemplo $.post()
$('form').on('submit', function(evt) {
evt.preventDefault();
// var nom = $(this).find('#inputName').val();
var datos = $(this).serialize(); // nombre=asdf&email=asdf
$.post("/GuardaFormServlet", datos, function (respuestaServidor) {
console.log("Completado " + respuestaServidor);
});
});
211
JavaScript
Obtener el contenido de los campos, lo cual podemos hacerlo campo por campo como
en la línea 3, u obtener una representación de los datos como parámetros de una URL
mediante el método serialize() como en la línea 4.
Enviar el contenido a un script del servidor y recuperamos la respuesta. Mediante
$.post() le pasaremos el destino del envío, los datos a envíar y una función callback
que se llamará cuando el servidor finalice la petición.
Tipos de datos
Ya hemos visto que podemos trabajar con cuatro tipos de datos. A continuación vamos a
estudiarlos para averiguar cuando conviene usar uno u otro:
• Fragmentos HTML necesitan poco para funcionar, ya que mediante load() podemos
cargarlos sin necesidad de ejecutar ningún callback. Como inconveniente, los datos puede
que no tengan ni la estructura ni el formato que necesitemos, con lo que estamos acoplando
nuestro contenido con el externo.
• Archivos JSON, que permiten estructurar la información para su reutilización. Compactos
y fáciles de usar, donde la información es auto-explicativa y se puede manejar mediante
objetos mediante JSON.parse() y JSON.stringify() . Hay que tener cuidado con
errores en el contenido de los archivos ya que pueden provocar efectos colaterales.
• Archivos JavaScript, ofrecen flexibilidad pero no son realmente un mecanismo de
almacenamiento, ya que no podemos usarlos desde sistemas heterogéneos. La posibilidad
de cargar scripts JavaScripts en caliente permite refactorizar el código en archivos externos,
reduciendo el tamaño del código hasta que sea necesario.
• Archivos XML, han perdido mercado en favor de JSON, pero se sigue utilizando para
permitir que sistemas de terceros sin importar la tecnología de acceso puedan conectarse
a nuestros sistemas.
A día de hoy, JSON tiene todas las de ganar, tanto por rendimiento en las comunicaciones
como por el tamaño de la información a transmitir.
Método Propósito
Registra un manejador que se invocará cuando la petición AJAX se
ajaxComplete()
complete
Registra un manejador que se invocará cuando la petición AJAX se
ajaxError()
complete con un error
Registra un manejador que se invocará cuando la primera petición
ajaxStart()
AJAX comience
Registra un manejador que se invocará cuando todas las peticiones
ajaxStop()
AJAX hayan finalizado
Adjunta una función que se invocará antes de enviar la petición
ajaxSend()
AJAX
Adjunta una función que se invocará cuando una petición AJAX
ajaxSuccess()
finalice correctamente
212
JavaScript
Recordad que estos métodos son globales y se ejecutan para todas las
peticiones AJAX de nuestra aplicación, de ahí que se sólo se adjunten al
objeto document .
Por ejemplo, si antes de recuperar el archivo de texto registramos todos estos manejadores:
$(document).ready(function() {
$(document).ajaxStart(function () {
console.log("AJAX comenzando");
});
$(document).ajaxStop(function () {
console.log("AJAX petición finalizada");
});
$(document).ajaxSend(function () {
console.log("Antes de enviar la información...");
});
$(document).ajaxComplete(function () {
console.log("Todo ha finalizado!");
});
$(document).ajaxError(function (evt, jqXHR, settings, err) {
console.error("Houston, tenemos un problema: " + evt + " - jq:" +
jqXHR + " - settings :" + settings + " err:" + err);
});
$(document).ajaxSuccess(function () {
console.log("Parece que ha funcionado todo!");
});
getDatos();
});
function getDatos() {
$.getJSON("http://www.omdbapi.com/?s=batman&callback=?", todoOk);
}
function todoOk(datos) {
console.log("Datos recibidos y adjuntándolos a resultado");
$("#resultado").append(JSON.stringify(datos));
}
Tras ejecutar el código, por la consola aparecerán los siguientes mensajes en el orden en el
que se ejecutan:
213
JavaScript
Autoevaluación
Si en el código renombramos la URL por la de un fichero que no encuentra,
12
¿Qué evento se lanza ahora y cual deja de lanzarse? ¿Qué saldrá por
13
consola?
7.3. Utilidades
A continuación veremos un conjunto de utilidades que ofrece jQuery.
Comprobación de tipos
El siguiente conjunto de funciones de comprobación de tipos, también conocidas como de
introspección de objetos, nos van a permitir:
Función Propósito
Determina si array es un Array. Si es un objeto array-like devolverá
$.isArray(array)
falso
$.isFunction(función) Determina si función es una Función
$.isEmptyObject(objeto)Determina si objeto esta vacío
Determina si objeto es un objeto sencillo, creado como un objeto
$.isPlainObject(objeto)
literal (mediante las llaves) o mediante new objeto .
$.isXmlDoc(documento)Determina si el documento es un documento XML o un nodo XML
$.isNumeric(objeto) Determina si objeto es un valor numérico escalar.
$.isWindow(objeto) Determina si objeto representa una ventana de navegador
Obtiene la clase Javascript del objeto. Los posibles valores son
$.type(objeto) boolean , number , string , function , array , date ,
regexp , object , undefined o null
Para ver estas utilidades en funcionamiento, vamos a crear un ejemplo sobre un fragmento
de código que realiza una llamada a una función:
12
Antes se lanzaba ajaxSuccess y ahora se lanza ajaxError
13
AJAX Comenzando, Antes de enviar la información…, GET url Not found, Houston tenemos un problema: Not
found, Todo ha finalizado!, AJAX petición finalizada
214
JavaScript
function saluda() {
$("#resultado").append("Saludando desde la función <br />");
}
$(function() {
llamaOtraFuncion(3, 500, saluda);
});
var i = 0;
// resto de código...
De modo que ahora podemos realizar diferentes llamadas sobrecargando los parámetros:
Manipulación de colecciones
El siguiente conjunto de funciones de manipulación de objetos nos permiten trabajar con arrays
y objetos simplificando ciertas tareas:
Función Propósito
Convierte el objeto en un array. Se utiliza cuando necesitamos
llamar a funciones que sólo soportan los arrays, como join
$.makeArray(objeto)
o reverse , o cuando necesitamos pasar un parametro a una
función como array
Determina si el array contiene el valor. Devuelve -1 o el índice
$.inArray(valor,
que ocupa el valor. Un tercer parámetro opcional permite indicar el
array)
índice por el cual comienza la búsqueda.
$.unique(array) Elimina cualquier elemento duplicado que se encuentre en el array
$.merge(array1, Combina los contenidos de array1 y array2, similar a la función
array2) concat() .
$.map(array, Construye un nuevo array cuyo contenido es el resultado de llamar
callback) al callback para cada elemento, similar a la funcion map() .
Filtra el array mediante el callback, de modo que añadirá los
$.grep(array,
elementos que pasen la función, la cual recibe un objeto DOM
callback
como parámetro, y devuelve un array JavaScript, similar a la
[,invertido])
función filter()
215
JavaScript
$.unique(miArray);
console.log(miArray); // [1, 2, 3, 4, 5]
$.merge(miArray, miArray2);
console.log(miArray);
Autoevaluación
14
¿Qué saldrá por la consola tras ejecutar el método $.merge() ?
Copiando objetos
Si tenemos uno o varios objetos de los cuales queremos copiar sus propiedades en uno final,
podemos hacer uso del método $.extend(destino, origen) ;
var animal = {
comer: function() {
console.log("Comiendo");
}
}
var perro = {
ladrar: function() {
console.log("Ladrando");
}
}
$.extend(perro, animal);
perro.comer(); // Comiendo
Es decir, permite copiar miembros de un objeto fuente en uno destino, sin realizar herencia,
sólo clonando las propiedades. Si hay un conflicto, se sobreescribirán con las propiedades del
objeto fuente, y si tenemos múltiples objetos fuentes, de izquierda a derecha.
Si los objetos que vamos a clonar contienen objetos anidados, necesitamos indicarle a jQuery
que el clonado debe ser recursivo, mediante un booleano a true como primer parámetro:
14
Un array con los valores: 5, 4, 3, 2, 1, 6, 7, 8
216
JavaScript
var animal = {
acciones: {
comer: function() {
console.log("Comiendo");
},
sentar: function() {
console.log("Sentando");
}
}
};
var perro = {
acciones: {
ladrar: function() {
console.log("Ladrando");
},
cavar: function() {
console.log("Cavando");
}
}
};
var perroCopia = {};
$.extend(perroCopia, perro);
perroCopia.acciones.ladrar(); // Ladrando
$.extend(perro, animal);
perro.acciones.comer(); // Comiendo
perro.acciones.ladrar(); // error
Copiamos los atributos de perro en perroCopia , con lo que podemos acceder a las
propiedades
Al hacer una copia recursiva, en vez de sustituir la propiedad acciones de
perroCopia por la de animal , las fusiona
En cambio, si no hacemos la copia recursiva, podemos acceder a las propiedades de
animal pero no a las de perro
Más información en http://api.jquery.com/jquery.extend/
7.4. Plugins
Aunque el núcleo de jQuery ya ofrece multitud de funcionalidad y utilidades, también soporta
una arquitectura de plugins para extender la funcionalidad de la librería.
217
JavaScript
Aunque jQuery ofrece múltitud de plugins que extienden el código, en ocasiones necesitamos
ir un poco más allá y nos toca escribir nuestro propio código que podemos empaquetar como
un nuevo plugin.
El archivo fuente que contenga nuestro plugin debería cumplir la siguiente convención de
nombrado:
jquery.nombrePlugin.js
jquery.nombrePlugin-1.0.js
Creando un plugin
A la hora de crear un plugin, lo primero que asumimos es que jQuery ha cargado. Lo que
no podemos asumir es que el alias $ esté disponible. Por ello, dentro de nuestros plugins
usaremos el nombre completo jQuery o definiremos el $ por nosotros mismos.
Conviene recordar que podemos hacer uso de una IIFE para poder usar el $ dentro de nuestro
plugin:
(function($) {
// código del plugin
})(jQuery);
Funciones globales
Del mismo modo que jQuery ofrece la función $.ajax() , la cual no necesita ningún objeto
para funcionar, nosotros podemos extender el abanico de funciones de utilidades que ofrece
jQuery.
Para añadir una función al espacio de nombre de jQuery únicamente hemos de asignar la
función como una propiedad del objeto jQuery :
(function($) {
$.suma = function(array) {
var total = 0;
$.each(array, function (indice, valor) {
valor = $.trim(valor);
valor = parseFloat(valor) || 0;
total += valor;
});
return total;
};
})(jQuery);
218
JavaScript
También podríamos crear un nuevo espacio de nombres para las funciones globales que
queramos añadir, y así evitar conflictos que puedan aparecer con otros plugins. Para ello, sólo
hemos de asociar a nuestra función global un objeto el cual contenga como propiedades las
funciones que queramos añadir como plugin:
(function($) {
$.MathUtils = {
suma : function(array) {
// código de la función
},
media: function(array) {
// código de la función
}
};
})(jQuery);
Métodos de objeto
Si queremos extender las funciones de jQuery, mediante prototipos podemos crear métodos
nuevos que se apliquen al objeto jQuery activo. jQuery utiliza el alias fn en vez
prototype :
(function($) {
$.fn.nombreNuevaFuncion = function() {
// código nuevo
};
})(jQuery);
Normalmente no sabemos si la función trabajará sobre un sólo objeto o sobre una colección,
ya que un selector de jQuery puede devolver cero, uno o múltiples elementos. De modo que
una buena práctica es plantear un escenario donde recibimos un array de datos.
La manera más facil de garantizar este comportamiento es iterar sobre la colección mediante
el método each() .
$.fn.nombreNuevaFuncion = function() {
this.each(function() {
// Hacemos algo con cada elemento
});
219
JavaScript
};
Así pues, si quisiéramos extender jQuery y ofrecer un nuevo método que le cambiase la clase
CSS a un nodo haríamos:
(function($) {
$.fn.cambiarClase = function(clase1, clase2) {
this.each(function() {
var elem = $(this);
if (elem.hasClass(clase1)) {
elem.removeClass(clase1).addClass(clase2);
} else if (elem.hasClass(clase2)) {
elem.removeClass(clase2).addClass(clase1);
}
});
};
})(jQuery);
Recorremos la colección de objetos que nos devuelve el selector. En este punto this
referencia al objeto devuelto por jQuery
Cacheamos la referencia al elemento en concreto sobre el que iteramos
Esto permitirá que posteriormente realicemos una llamada al nuevo método mediante
cualquier selector:
$("div").cambiarClase("principal","secundaria");
Encadenar funciones
Al crear una función prototipo, hemos de tener en cuenta que probablemente, después de
llamar a nuestra función, es posible que el desarrollador quiera seguir encadenando llamadas.
Es por ello, que es muy importante que la función devuelva un objeto jQuery para permitir
que continúe el encadenamiento. Este objeto normalmente es el mismo que this .
En nuestro caso, podemos modificar el plugin para devolver el objeto que iteramos:
(function($) {
$.fn.cambiarClase = function(clase1, clase2) {
return this.each(function() {
var elem = $(this);
if (elem.hasClass(clase1)) {
elem.removeClass(clase1).addClass(clase2);
} else if (elem.hasClass(clase2)) {
elem.removeClass(clase2).addClass(clase1);
}
});
};
})(jQuery);
220
JavaScript
$.fn.pluginQueRecibeFuncion = function(funcionParam) {
if ($.isFunction(funcionParam)) {
funcionParam.call(this);
}
return this;
}
Opciones
Conforme los plugins crecen, es una buena práctica permitir que el plugin reciba un objeto con
las opciones de configuración del mismo.
$.fn.pluginConConf = function(opciones) {
var confFabrica = {prop: "valorPorDefecto"};
var conf = $.extend(confFabrica, opciones);
return this.each(function() {
// código que trabaja con conf.prop
});
};
Sobreescribe los valores de fabrica del objeto de configuración con el recibido como
parámetro
Dentro de la iteración, usamos los valores que contiene el objeto de configuración
Además, es conveniente que permitamos modificar los valores de fabrica para permitir mayor
flexibilidad. Para ello, extraemos los valores de fábrica del plugin a una propiedad del método:
$.fn.pluginConConf = function(opciones) {
return this.each(function() {
// código que trabaja con conf.prop
});
};
$.fn.pluginConConf.confFabrica.prop = "nuevoValor";
221
JavaScript
7.5. Rendimiento
A la hora de escribir código jQuery es útil conocer la manera de que tenga el mejor rendimiento
sin penalizar la comprensión ni mantenibilidad del código. Sin embargo, es importante tener
en mente que la optimización prematura suele ser la raíz de todos los males.
Consejos de rendimiento
Una vez localizado el problema mediante el profiling del código que vimos en la sesión de
JavaScript, llega el momento de optimizar el código. Para ello, deberemos:
console.time("Sin cachear");
for (var i=0; i < 1000; i++) {
var s = $("div");
}
console.timeEnd("Sin cachear");
console.time("Cacheando");
var miCapa = $("div");
for (var i=0; i < 1000; i++) {
var s = miCapa;
}
console.timeEnd("Cacheando");
Y por la consola tendremos que el fragmento que no cachea tarda 8.990ms mientras que
el que cachea sólo 0.045ms, es decir, 200 veces menos.
3. Cachear otros elementos, como llamadas a métodos o acceso a propiedades dentro de
bucles. Por ejemplo, el siguiente fragmento recorre un conjunto de 1000 capas:
console.time("Sin cachear");
var miCapa = $("div");
var s = 0;
for (var i=0; i < miCapa.length; i++) {
s += i;
}
console.timeEnd("Sin cachear");
console.time("Cacheando");
var miCapa = $("div");
222
JavaScript
function getDatos(id) {
if (!api[id]) {
var url = "http://www.omdbapi.com/?s=" + busqueda + "&callback=?";
console.log("Petición a " + url);
api[id] = $.getJSON(url);
}
api[id].done(todoOk).fail(function() {
$("#resultado").html("Error");
});
}
function todoOk(datos) {
console.log("Datos recibidos y adjuntándolos a resultado");
$("#resultado").append(JSON.stringify(datos));
}
7.6. Ejercicios
(0.4 ptos) Ejercicio 71. Apuntes 2.0
A partir del ejercicio 62 realizado en la sesión anterior donde modificábamos la página de
apuntes, vamos a mejorarlo de manera que:
• Al cargar la página, todos los capítulos estén ocultos, incluidas las notas al pie.
• Al pulsar sobre un enlace de la tabla de contenidos, se muestre el contenido de dicho
capítulo. Si había un capítulo mostrado, debe ocultarse. Es decir, sólo puede visualizar en
pantalla un único capítulo.
223
JavaScript
• Tanto la aparición como la desaparición del contenido se debe realizar mediante una
animación.
Ahora vamos a mostrar un listado con todos los personajes. Al elegir uno de los disponibles
se mostrará su információn básica, así como el título de las películas en las que aparece.
Cuando se cambie el personaje, mediante una animación se ocultará la información del antiguo
personaje (borrando sus datos) antes de mostrar sólo la información del nuevo.
<!DOCTYPE html>
<html>
<head lang="es">
<meta charset="UTF-8">
<title>Ejercicio 72</title>
</head>
<body>
<h1>Star Wars API</h1>
<h2>Personajes - <span id='total'></span></h2>
Elige entre los principales personajes: <select id='personajes'></select>
<br />
<br />
<div id='personaje' style='display:none; background:ghostwhite'>
Nombre: <span id='nombre'></span><br />
Sexo: <span id='sexo'></span><br />
Especie: <span id='especie'></span><br />
Películas: <ul id='peliculas'></ul><br />
</div>
<script src="jquery-1.11.2.js"></script>
<script src="ej72.js"></script>
224
JavaScript
</body>
</html>
El plugin debe permitir modificar la configuración base para que el final de la cadena sea
diferente a HHH .
225
JavaScript
226
JavaScript
8. Promesas
Las promesas son una característica novedosa para gestionar los eventos asíncronos,
permitiendo escribir código más sencillo, callbacks más cortos, y mantener la lógica de la
aplicación de alto nivel separada de los comportamientos de bajo nivel.
Al utilizar promesas, podemos usar callbacks en cualquier situación, y no solo con eventos.
Además ofrecen un mecanismo estándar para indicar la finalización de tareas.
En la vida real, cuando vamos a un restaurante de comida rápida y pedimos un menú con
hamburguesa con bacón y patatas fritas, estamos obteniendo una promesa con el número del
pedido, porque primero lo pagamos y esperamos recibir nuestra comida, pero es un proceso
asíncrono el cual inicia una transacción. Mientras esperamos a que nos llamen con nuestra
sabrosa hamburguesa, podemos realizar otras acciones, como decirle a nuestra pareja que
busque una mesa o preparar un tuit con lo rica que está nuestra hamburguesa. Estas acciones
se traducen en callbacks, los cuales se ejecutarán una vez se finalice la operación. Una
vez nuestro pedido está preparado, nos llaman con el número del mismo y nuestra deseada
comida. O inesperadamente, se ha acabado el bacón y nuestra promesa se cumple pero
erroneamente. Así pues, un valor futuro puede finalizar correctamente o fallar, pero en ambos
casos, finalizar.
Hola Promesa
Para crear una promesa, utilizaremos el contructor Promise() , el cual recibe como
argumento un callback con dos parámetros, resolver y rechazar . Este callback se
ejecuta inmediatamente.
Dentro de la promesa, realizaremos las acciones deseadas y si todo funciona como esperamos
llamaremos a resolver . Sino, llamaremos a rechazar (mejor pasándole un objeto
Error para capturar la pila de llamadas)
227
JavaScript
var ok;
if (ok) {
resolver("Ha funcionado"); // resuelve p
} else {
rechazar(Error("Ha fallado")); // rechaza p
}
});
Así pues, los parámetros resolver y rechazar tienen la capacidad de manipular el estado
interno de la instancia de promesa p . Esto se conoce como el patrón Revealing Constructor
(http://blog.domenic.me/the-revealing-constructor-pattern/), ya que el constructor revela sus
entrañas pero solo al código que lo construye, el cual es el único que puede resolver o rechazar
la promesa. Por ello, cuando le pasamos la promesa a cualquier otro método, éste solo podrá
añadir callbacks sobre la misma:
hacerCosasCon(promesa);
Uso básico
Una vez tenemos nuestra promesa, independientemente del estado en el que se encuentre,
mediante then(callbackResuelta, callbackRechazada) podremos actuar en
consecuencia dependiendo del callback que se dispare:
promesa.then(
function(resultado) {
console.log(resultado); // "Ha funcionado"
}, function(err) {
console.error(err); // Error: "Ha fallado"
}
);
El primer callback es para el caso completado, y el segundo para el rechazado. Ambos son
opcionales, con lo que podemos añadir un callback para únicamente el caso completado o
rechazado.
Uno de los ejemplos más sencillos es una petición AJAX mediante jQuery:
var p = $.get("http://www.omdbapi.com/?t=Interstellar&r=json");
p.then(function(resultado) {
console.log(resultado);
});
228
JavaScript
Una vez que una promesa se completa o se rechaza, se mantendrá en dicho estado para
siempre (conocido como settled), y sus callbacks nunca se volverán a disparar. Tanto el estado
como cualquier valor dado como resultado no se pueden modificar.
Dicho de otro modo, una promesa sólo puede completarse o rechazarse una vez. No puede
hacerlo dos veces, ni cambiar de un estado completado a rechazado, o viceversa.
Autoevaluación
A partir del siguiente código anterior, ¿Qué mensajes saldrán por pantalla?
15
promesa.then(function (num) {
console.log("El número es " + num)
})
15
Sólo el valor de PI, ya que una vez resuelta, la promesa no puede tomar otro valor
229
JavaScript
Es decir, se queda en un estado inmutable, el cual se puede observar tantas veces como
queramos. Por ello, una vez resuelta, podemos pasar tranquilamente la promesa a cualquier
otra función, ya que nadie va a poder modificar su estado.
Y ahora viene la pregunta del millón: Si la función rechazar pasa una promesa al estado
rechazado, ¿por qué la función resolver no pasa la promesa al estado resuelta en vez de
al estado completada? La respuesta corta es que resolver una promesa no es lo mismo
que completarla. La larga es que cuando a la función resolver se le pasa un valor,
la promesa se completa automáticamente. En cambio, cuando se le pasa otra promesa
(por ejemplo promesa.resolver(otraPromesa) ), las promesas se unen para crear una
única promesa, de manera que cuando se resuelve la 2ª promesa ( otraPromesa ), ambas
promesas se resolverán. Del mismo modo, si se rechaza la 2ª promesa, las dos promesas
se rechazarán.
Tanto resolver como rechazar se pueden llamar sin argumentos, en cuyo caso el valor
de la promesa será undefined .
Estos métodos son útiles cuando ya tienes el elemento que debería resolver o rechazar la
promesa.
Consumiendo promesas
Una gran ventaja de emplear promesas es que podemos adjuntar tantos callbacks a una
promesa como queramos, los cuales se ejecutarán una vez que la promesa se resuelva o
rechace. Es decir, una promesa la pueden consumir varios consumidores.
Veamos un ejemplo completo. Supongamos que al entrar a una aplicación, queremos mostrar
la información del usuario tanto en la barra de navegación como en el titulo de la página.
var usuario = {
perfilUsuario: null,
230
JavaScript
obtenerPerfil: function() {
if (!this.perfilUsuario) {
var xhr = new XMLHttpRequest();
xhr.open("GET","usuario.json", true);
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
perfilUsuario = JSON.parse(xhr.responseText);
}
};
xhr.send(null);
}
}
};
usuario.obtenerPerfil();
if (usuario.perfilUsuario) {
document.getElementById("navbar").innerHTML = usuario.login;
document.getElementById("titulo").innerHTML = usuario.nombre;
}
Pese a parecer que este código funciona, nunca se rellenarán los datos porque al ser
una llamada asíncrona, la variable perfilUsuario estará a null.
Para que funcione correctamente, el código de rellenar la página se tiene que acoplar con la
petición AJAX:
var usuario = {
perfilUsuario: null,
obtenerPerfil: function() {
if (!this.perfilUsuario) {
var xhr = new XMLHttpRequest();
xhr.open("GET","usuario.json", true);
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
perfilUsuario = JSON.parse(xhr.responseText);
document.getElementById("navbar").innerHTML =
perfilUsuario.login;
document.getElementById("titulo").innerHTML =
perfilUsuario.nombre;
}
};
xhr.send(null);
}
}
};
usuario.obtenerPerfil();
Prometiendo AJAX
En cambio, si al obtener los datos AJAX, los envolvemos en una promesa, el código queda
más legible y ahora sí que podemos desacoplar la petición AJAX del dibujado HTML:
231
JavaScript
var usuario = {
promesaUsuario: null,
obtenerPerfil: function() {
if (!this.promesaUsuario) {
this.promesaUsuario = new Promise(function(resolver, rechazar) {
var xhr = new XMLHttpRequest();
xhr.open("GET","usuario.json", true);
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
resolver(JSON.parse(xhr.responseText));
}
};
xhr.onerror = function() {
rechazar(Error("Error al obtener usuario"));
};
xhr.send(null);
});
}
return this.promesaUsuario;
}
};
var navbar = {
mostrar: function(usuario) {
usuario.obtenerPerfil().then(function(perfil) {
document.getElementById("navbar").innerHTML = perfil.login;
});
}
}
var titulo = {
mostrar: function(usuario) {
usuario.obtenerPerfil().then(function(perfil) {
document.getElementById("titulo").innerHTML = perfil.nombre;
});
}
}
navbar.mostrar(usuario);
titulo.mostrar(usuario);
Además, podemos añadir más callback cuando queramos, incluso si la promesa ya ha sido
resuelta o rechazada (en dicho caso, se ejecutarán inmediatamente).
Devolviendo promesas
¿Y si queremos realizar una acción tras mostrar los datos del usuario? Al tratarse de una tarea
asíncrona, necesitamos a su vez, que las operaciones de mostrar los datos también devuelvan
una promesa.
232
JavaScript
Cualquier función que utilice una promesa debería devolver una nueva
promesa
// ...
var titulo = {
mostrar: function(usuario) {
return usuario.obtenerPerfil().then(function(perfil) {
document.getElementById("titulo").innerHTML = perfil.nombre;
});
}
}
Como then devuelve una promesa, a su vez, hacemos que mostrar la propague
Prometiendo XMLHttpRequest
A día de hoy XMLHttpRequest no devuelve una promesa. Siendo, probablemente, la
operación asíncrona más empleada por su uso en AJAX, el hecho de que devuelva una
promesa va a simplicar las peticiones AJAX. Para ello, podemos crear una función que
devuelve una promesa del siguiente modo:
function ajaxUA(url) {
return new Promise(function(resolver, rechazar) {
var req = new XMLHttpRequest();
req.open('GET', url);
req.onload = function() {
if (req.status == 200) {
resolver(req.response);
} else {
rechazar(Error(req.statusText));
}
};
req.onerror = function() {
reject(Error("Error de Red"));
};
req.send();
});
}
Ejemplo then
233
JavaScript
ajaxUA('heroes.json').then(function(response) {
console.log("¡Bien!", response);
}, function(error) {
console.error("¡Mal!", error);
});
Gracias al encadenamiento de promesas, podemos obtener el texto de una petición AJAX del
siguiente modo:
ajaxUA('heroes.json').then(function(response) {
return JSON.parse(response);
}).then(function(response) {
console.log("JSON de Heroes:", response);
});
ajaxUA('heroes.json').then(JSON.parse).then(function(response) {
console.log("JSON de Heroes:", response);
});
Encadenando promesas
Acabamos de ver que tanto then como catch devuelven una promesa, lo que facilita su
encadenamiento, aunque no devuelven una referencia a la misma promesa. Cada vez que se
llama a uno de estos métodos se crea una nueva promesa y se devuelve, siendo estas dos
promesas diferentes:
var p1,p2;
p1 = Promise.resolve();
p2 = p1.then(function() {
// ....
});
console.log(p1 !== p2); // true
paso1().then(
function paso2(resultadoPaso1) {
// Acciones paso 2
}
).then(
function paso3(resultadoPaso2) {
// Acciones paso 3
}
).then(
234
JavaScript
function paso4(resultadoPaso3) {
// Acciones paso 3
}
)
Cada llamada a then devuelve una nueva promesa que podemos emplear para adjuntarla
a otro callback. Sea cual sea el valor que devuelve el callback resuelve la nueva promesa.
Mediante este patrón podemos hacer que cada paso envíe su valor devuelto al siguiente paso.
Si un paso devuelve una promesa en vez de un valor, el siguiente paso recibe el valor empleado
para completar la promesa. Veamos este caso mediante un ejemplo:
Promise.resolve('Hola!').then(
function paso2(resultado) {
console.log('Recibido paso 2: ' + resultado);
return 'Saludos desde el paso 2'; // Devolvemos un valor
}
).then(
function paso3(resultado) {
console.log('Recibido paso 3: ' + resultado); // No devolvemos
nada
}
).then(
function paso4(resultado) {
console.log('Recibido paso 4: ' + resultado);
return Promise.resolve('Valor completado'); // Devuelve una promesa
}
).then(
function paso5(resultado) {
console.log('Recibido paso 5: ' + resultado);
}
);
// Consola
// "Recibido paso 2: Hola!"
// "Recibido paso 3: Saludos desde el paso 2"
// "Recibido paso 4: undefined"
// "Recibido paso 5: Valor completado"
Al devolver un valor de manera explícita como en el paso dos, la promesa se resuelve. Como
el paso 3 no devuelve ningún valor, la promesa se completa con undefined . Y el valor
devuelto por el paso 4 completa la promesa que envuelve el paso 4.
235
JavaScript
promesa.then(function() {
console.log("Dentro del callback de completado");
});
console.log("Fin de la cita");
// Consola
// Antes de la funcion resolver
// Fin de la cita
// Dentro del callback de completado
Gestión de errores
Los rechazos y los errores se propagan a través de la cadena de promesas. Cuando se rechaza
una promesa, todas las siguientes promesas de la cadena se rechazan como si fuera un
dominó. Para detener el efecto dominó, debemos capturar el rechazo.
Para ello, además de utilizar el método then y pasar como segundo argumento la función
para gestionar los errores, el API nos ofrece el método catch :
Ejemplo catch
ajaxUA('heroes.json').then(function(response) {
console.log("¡Bien!", response);
}).catch(function(error) {
console.error("¡Mal!", error);
});
236
JavaScript
}
);
// Consola
// Algo ha fallado por el camino
// Error: Algo ha ido mal
Excepciones y promesas
Una promesa se rechaza si se hace de manera explicita mediante la función rechazar del
constructor, con Promise.reject o si el callback pasado a then lanza un error:
rechazarCon("¡Malas noticias!").then(
function paso2() {
console.log("Por aquí no pasaré")
}
).catch(
function (err) {
console.error("Y vuelve a fallar");
console.error(err);
}
);
function rechazarCon(cadena) {
return new Promise(function (resolver, rechazar) {
throw Error(cadena);
resolver("No se utiliza");
});
}
// Consola
// Y vuelve a fallar
// Error: ¡Malas noticias!
Este comportamiento resalta la ventaja de realizar todo el trabajo relacionado con la promesa
dentro del callback del constructor, para que los errores se capturen automáticamente y se
conviertan en rechazos.
Autoevaluación
A partir del siguiente código anterior, ¿Qué mensajes saldrán por pantalla?
16
16
¡Mal! y el error de parseo de JSON inválido, ya que se lanza una excepción en el constructor y por tanto no se
resuelve la promesa
237
JavaScript
promesaJSON.then(function(datos) {
console.log("¡Bien!", datos);
}).catch(function(err) {
console.error("¡Mal!", err);
});
ajaxUA('heroes.json').then(function(response) {
console.log("¡Bien!", response);
}).then(undefined, function(error) {
console.error("¡Mal!", error);
});
Pero que sean similares no significan que sean iguales. Hay una sutil diferencia entre
encadenar dos funciones then a hacerlo con una sola. Al rechazar una promesa se pasa
al siguiente then con un callback de rechazo (o catch , ya que son equivalentes). Es
decir, con then(func1, func2) , se llamará a func1 o a func2, nunca a las dos. Pero con
then(func1).catch(func2) , se llamarán a ambas si func1 rechaza la promesa, ya que
son pasos separados de la cadena.
paso1().then(function() {
return paso2();
}).then(function() {
return paso3();
}).catch(function(err) {
return recuperacion1();
}).then(function() {
return paso4();
}, function(err) {
return recuperacion2();
}).catch(function(err) {
console.error("No me importa nada");
}).then(function() {
console.log("¡Finiquitado!");
});
Este flujo de llamadas es muy similar a emplear try/catch en el lenguaje estandard de manera
que los errores que suceden dentro de un try saltan inmediatamanete al bloque catch .
238
JavaScript
Promesas en paralelo
Si tenemos un conjunto de promesas que queremos ejecutar, y lo hacemos mediante un bucle,
se ejecutarán en paralelo, en un orden indeterminado finalizando cada una conforme al tiempo
necesario por cada tarea.
Supongamos que tenemos una aplicación bancaria en la cual tenemos varias cuentas, de
manera que cuando el usuario entra en la aplicación, se le muestra el saldo de cada una de
ellas:
cuentas.forEach(function(cuenta) {
ajaxUA(cuenta).then(function(balance) {
console.log(cuenta + " Balance -> " + balance);
});
});
// Consola
// Banco 1 Balance -> 234
// Banco 3 Balance -> 1546
// Banco 2 Balance -> 789
Si además queremos mostrar un mensaje cuando se hayan consultado todas las cuentas,
necesitamos consolidar todas las promesas en una nueva. Para ello, mediante el método
Promise.all , podemos escribir código de finalización de tareas paralelas, del tipo "Cuando
todas estas cosas hayan finalizado, haz esta otra".
El comportamiento de
Promise.all(arrayDePromesas).then(function(arrayDeResultados) es el
siguiente: devuelve una nueva promesa que se cumplirá cuando lo hayan hecho todas las
promesas recibidas. Si alguna se rechaza, la nueva promesa también se rechazará. El
resultado es un array de resultados que siguen el mismo orden de las promesas recibidas.
Promise.all(peticiones).then(function (balances) {
console.log("Los " + balances.length + " han sido actualizados");
}).catch(function(err) {
console.error("Error al recuperar los balances", err);
})
// Consola
// Los 3 balances han sido actualizados
239
JavaScript
Promesas en secuencia
Cuando hemos estudiado como encadenar promesas, hemos visto como podemos codificar
que una promesa se ejecute cuando ha finalizado la anterior. Para ello, de antemano tenemos
que saber las promesas que vamos a gestionar.
array.forEach(function (elem) {
seq = seq.then(function() {
return callback(elem);
});
});
}
en cada iteración, seq contiene el valor de la secuencia anterior hasta que se resuelve
y almacena la nueva promesa
Si empleamos la función reduce , el código se reduce al ahorrarnos la variable seq :
240
JavaScript
Aunque ambos planteamientos obtengan el mismo resultado, los dos enfoques tienen sus
diferencias. Mientras que mediante el bucle se contruye la cadena entera de promesas sin
esperar a que se resuelvan, mediante el planteamiento recursivo, las promesas se añaden
bajo demanda tras resolver la promesa anterior. Así pues, el enfoque recursivo permite decidir
si continuar o romper la cadena en base al resultado de una promesa anterior.
Carrera de promesas
En ocasiones vamos a realizar diferentes peticiones que realizan la misma acción a diferentes
servicios, pero sólo nos interesará el que nos devuelve el resultado más rápidamente.
Mediante esta operación, podemos crear un mecanismo para gestionar la latencia de una
petición a servidor, de manera que si tarda más de X segundos, acceda a una caché. En el
caso de que falle la cache, el mecanismo fallará.
function obtenerDatos(url) {
var tiempo = 500; // ms
var caduca = Date.now() + tiempo;
241
JavaScript
Autoevaluación
¿Qué pasaría se la petición a los datos del servidor falla inmediatamente
17
debido a un fallo de red?
El objeto window ofrece el método fetch , con un primer argumento con la URL de la
petición, y un segundo opcional con un objeto literal que permite configurar la petición:
}).catch(function(err) {
// Error :(
});
Hola Fetch
A modo de ejemplo, vamos a reescibir el ejemplo de la sesión 5 que hacíamos uso de AJAX,
pero ahora con la Fetch API. De esta manera, el código queda mucho más concreto:
fetch('http://www.omdbapi.com/?s=batman', {
method: 'get'
17
La promesa datosServer se rechazaría y por consiguiente no se comprobaría la caché
242
JavaScript
}).then(function(respuesta) {
if (!respuesta.ok) {
throw Error(respuesta.statusText);
}
return respuesta.json();
}).then(function(datos) {
var pelis = datos.Search;
for (var numPeli in pelis) {
console.log(pelis[numPeli].Title + ": " + pelis[numPeli].Year);
}
}).catch(function(err) {
console.error("Error en Fetch de películas de Batman", err);
});
function estado(respuesta) {
if (respuesta.ok) {
return Promise.resolve(respuesta);
} else {
return Promise.reject(new Error(respuesta.statusText));
}
}
fetch('http://www.omdbapi.com/?s=batman', {
method: 'get'
}).then(estado)
.then(function (respuesta) {
return respuesta.json();
}).then(function(datos) {
var pelis = datos.Search;
for (var numPeli in pelis) {
console.log(pelis[numPeli].Title + ": " + pelis[numPeli].Year);
}
}).catch(function(err) {
console.error("Error en Fetch de películas de Batman", err);
});
Cabeceras
Una ventaja de este API es la posibilidad de asignar las cabeceras de las peticiones mediante
el objeto Headers() , el cual tiene una estructura similar a una mapa. A continuación se
muestra un ejemplo de como acceder y manipular las cabeceras mediante los métodos
append , has , get , set y delete :
243
JavaScript
headers.append('Content-Type', 'text/plain');
headers.append('Mi-Cabecera-Personalizada', 'cualquierValor');
headers.has('Content-Type'); // true
headers.get('Content-Type'); // "text/plain"
headers.set('Content-Type', 'application/json');
headers.delete('Mi-Cabecera-Personalizada');
Para usar las cabeceras, las pasaremos como parámetro de creación de una petición mediante
una instancia de Request :
Petición
Para poder configurar toda la información que representa una petición se emplea el objeto
Request . Este objeto puede contener las siguientes propiedades:
244
JavaScript
Realmente, el método fetch recibe una URL a la que se envía una petición, y un objeto
literal con la configuración de la petición, por lo que el código queda mejor así:
fetch('/heroes.json', {
method: 'GET',
mode: 'cors',
headers: new Headers({
'Content-Type': 'text/plain'
})
}).then(function() { /* manejar la respuesta */ });
Enviando datos
Ya sabemos que un caso muy común es enviar la información de un formulario vía AJAX.
Para ello, además de configurar que la petición sea POST , le asociaremos en la propiedad
body un FormData creado a partir del identificador del elemento del formulario:
fetch('/submit', {
method: 'post',
body: new FormData(document.getElementById('formulario-cliente'))
});
fetch('/submit-json', {
method: 'post',
body: JSON.stringify({
email: document.getElementById('email').value
comentarios: document.getElementById('comentarios').value
})
});
fetch('/submit-urlencoded', {
method: 'post',
headers: {
"Content-type": "application/x-www-form-urlencoded; charset=UTF-8"
},
body: 'heroe=Batman&nombre=Bruce+Wayne'
});
245
JavaScript
Respuesta
Una vez realizada la petición, dentro del manejador, reciberemos un objeto Response ,
compuesto de una serie de propiedades que representan la respuesta del servidor, de las
cuales extraremos la información:
• type : indican el origen de la petición. Dependiendo del tipo, podremos consultar diferente
información:
Además, el objeto Response ofrece los siguientes métodos para crear nuevas respuestas
con diferente código de estado o a un destino distino:
Finalmente, para transformar la respuesta a una promesa con un tipo de dato del cual extraer
la información de la petición, tenemos los siguientes métodos:
Parseando la respuesta
A día de hoy, el estandar es emplear el formato JSON para las respuestas. En vez de utilizar
JSON.parse(cadena) para transformar la cadena de respuesta en un objeto, podemos
utilizar el método json() :
fetch('heroes.json').then(function(response) {
return response.json();
}).then(function(datos) {
console.log(datos); // datos es un objeto JavaScript
246
JavaScript
});
Si la información, viene en texto plano o como documento HTML, podemos emplear text() :
fetch('/siguientePagina').then(function(response) {
return response.text();
}).then(function(texto) {
// <!DOCTYPE ....
console.log(texto);
});
fetch('paisaje.jpg').then(function(response) {
return response.blob();
})
.then(function(blobImagen) {
document.querySelector('img').src = URL.createObjectURL(blobImagen);
});
$.Deferred()
Cada promesa de jQuery comienza con un Deferred . Un Deferred es solo una promesa
con métodos que permiten a su propietario resolverla o rechazarla. Todas las otras promesas
son de sólo lectura.
Para crear un Deferred , usaremos el constructor $.Deferred() . Una vez creada una
promesa podemos invocar los siguientes métodos:
deferred.state();
deferred.resolve();
deferred.state();
deferred.reject();
247
JavaScript
Si al método constructor le pasamos una función, ésta se ejecutará tan pronto como el objete se
cree, y la función recibe como parámetro el nuevo objeto deferred. Mediante esta característica
podemos crear un envoltorio que realiza una tarea asíncrona y que dispare un callback cuando
haya finalizado:
function realizarTarea() {
return $.Deferred(function(def) {
// tarea async que dispara un callback al acabar
});
}
Podemos obtener una promesa "pura" a partir del método promise() . El resultado es similar
a Deferred , excepto que faltan los métodos de resolve() y reject() .
promise.state(); // "pending"
deferred.reject();
promise.state(); // "rejected"
obteniendoProductos.state(); // "pending"
obteniendoProductos.resolve; // undefined
Manejadores de promesas
Una vez tenemos una promesa, podemos adjuntarle tantos callbacks como queremos
mediante los métodos:
248
JavaScript
Por ejemplo:
promesa.fail(function() {
console.log("Se ejecutará cuando la promesa se rechace.");
});
promesa.always(function() {
console.log("Se ejecutará en cualquier caso.");
});
Por ejemplo, supongamos que tenemos tres botones que modifican la promesa:
<button id="btnResuelve">Resolver</button>
<button id="btnRechaza">Rechazar</button>
<button id="btnEstado">¿Estado?</button>
$("#btnResuelve").click(function() {
promesa.resolve();
});
$("#btnRechaza").click(function() {
promesa.reject();
});
$("#btnEstado").click(function() {
console.log(promesa.state());
});
Encadenando promesas
Los métodos de los objetos Deferred se pueden encadenar, de manera que cada método
devuelve a su vez un objeto Deferred donde poder volver a llamar a otros métodos:
promesa.done(function() {
console.log("Se ejecutará cuando la promesa se resuelva.");
}).fail(function() {
249
JavaScript
Mediante el método then() podemos agrupar los tres callbacks de una sola vez:
promesa.then(function() {
console.log("Se ejecutará cuando la promesa se resuelva.");
}, function() {
console.log("Se ejecutará cuando la promesa se rechace.");
}, function() {
console.log("Se ejecutará en cualquier caso.");
});
Además, el orden en el que se adjuntan los callbacks definen su orden de ejecución. Por
ejemplo, si duplicamos los callbacks y volvemos a añadirlos, tendremos:
Supongamos que reescribimos el ejemplo anterior con el siguiente fragmento para manejar
las promesas:
promesa.fail(function() {
console.log("Houston! Tenemos un problema");
});
promesa.always(function() {
console.log("Dentro del always");
}).done(function() {
console.log("Y un cuarto callback si todo ha ido bien");
});
"Primer callback."
"Segundo callback."
"Tercer callback."
"Dentro del always"
250
JavaScript
Queremos asegurarnos que el formularios solo se envia una vez y que el usuario recibe
una confirmación cuando envía las observaciones. Además, queremos separar el código que
describe el comportamiento de la aplicación del código que trabaja el contenido de la página.
Esto facilita el testing y minimiza la cantidad de código necesario que necesitaríamos modificar
si cambiamo la disposición de la página.
enviandoObservaciones.done(function(input) {
$.post("/observaciones", input);
});
$("#observaciones").submit(function() {
enviandoObservaciones.resolve($("textarea", this).val());
return false;
});
enviandoObservaciones.done(function() {
$("#contenido").append("<p>¡Gracias por las observaciones!</p>");
});
Para ello, podemos adjuntar callbacks a la promesa que devuelve $.post . El problema
viene cuando tenemos que manipular el DOM desde esos callbacks, y hemos planteado usar
promesas para separar el código de aplicación del de manipulación del DOM.
251
JavaScript
Una manera de separar la creación de una promesa del callback de lógica de aplicación es
reenviar los eventos de resolve / reject desde la promesa POST a una promesa que se
encuentre fuera de nuestro alcance. En vez de necesitar varias líneas con código anidado del
tipo promesa1.done(promesa2.resolve);.. , lo haremos mediante then() (antes de
jQuery 1.8 mediante pipe() ).
Para ello, then() devuelve una nueva promesa que permite filtrar el estado y los valores de
una promesa mediante una función, lo que la convierte en una ventana al futuro, permitiendo
adjuntar comportamiento a una promesa que todavía no existe.
Si ahora mejoramos el código del formulario haciendo que nuestra promesa POST se encole
en una nueva promesa llamada guardandoObservaciones , tendremos que el código de
aplicación quede así:
$("#observaciones").submit(function() {
enviandoObservaciones.resolve($("textarea", this).val());
return false;
});
enviandoObservaciones.done(function() {
$("#contenido").append("<div class='spinner'>");
});
guardandoObservaciones.then(
function() { // done
$("#contenido").append("<p>¡Gracias por las observaciones!</p>");
}, function() { // fail
$("#contenido").append("<p>Se ha producido un error al contactar con
el servidor.</p>");
}, function() { // always
$("#contenido").remove(".spinner");
});
Intersección de promesas
Parte de la magia de las promesas es su naturaleza binaria. Como solo tienen dos estados,
se pueden combinar igual que si fuesen booleanos (aunque todavía no sepamos que valores
tendrán).
De la misma manera que tenemos Promise.all , jQuery ofrece el método $.when() como
operador equivalente a la intersección (Y).
252
JavaScript
Así pues, dada una lista de promesas, when() devuelve una nueva promesa como resultado
de otras promesas y que cumple estas reglas:
• Cuando todas las promesas recibidas se resuelven, la nueva promesa estará resuelta.
• Cuando alguna de las promesas recibidas se rechaza, la nueva promesa se rechaza.
Por ello, cuando estemos esperando que múltiples eventos desordenados ocurran y
necesitamos realizar una acción cuando todos hayan finalizado deberemos utilizar when() .
Por ejemplo, el caso de uso más común es al realizar varias llamadas AJAX simultáneas:
$("#contenido").append("<div class='spinner'>");
$.when( $.get("/datosEncriptados"),$.get("/claveEncriptacion"))
.then(function() { // done
// ambas llamadas han funcionado
}, function() { // fail
// una de las llamadas ha fallado
}, function() { // always
$("#contenido").remove(".spinner");
});
Otro caso de uso es permitir al usuario solicitar un recurso que puede o no estar disponible.
Por ejemplo, supongamos un widget de un chat que cargamos con YepNope:
$.when(cargandoChat, lanzandoChat).done(function() {
$("#contenidoChat").remove(".spinner");
// comienza el chat
});
Deferreds y AJAX
Las promesas facilitan mucho el trabajo con AJAX y el trabajo con acceso simultáneos a
recursos remotos.
El objeto jxXHR que se obtiene de los métodos AJAX como $.ajax() o $.getJSON()
implementan el interfaz Promise , por lo que vamos a poder utilizar los métodos done ,
fail , then , always y when .
253
JavaScript
Así pues, vamos a poder escribir código similar al siguiente, donde tras hacer la petición vamos
a poder utilizar los callbacks que ofrecen las promesas:
function getDatos() {
var peticion = $.getJSON("http://www.omdbapi.com/?s=batman&callback=?");
peticion.done(todoOk).fail(function() {
console.log("Algo ha fallado");
});
peticion.always(function() {
console.log("Final, bien o mal");
});
}
function todoOk(datos) {
console.log("Datos recibidos y adjuntándolos a resultado");
$("#resultado").append(JSON.stringify(datos));
}
8.5. Ejercicios
(0.5 ptos) Ejercicio 81. Star Wars Fetch
A partir del ejercicio 72 sobre Star Wars API, vamos a reescribir el contenido mediante Fetch
API y el uso de promesas para reducir el código y simplificar la lógica de las llamadas.
En este caso, el listado de películas se tiene que pintar en el DOM de una sola vez, no conforme
se reciben los títulos de las películas con cada petición AJAX.
De igual manera que el ejercicio anterior, el listado de películas se tiene que pintar en el DOM
de una sola vez, no conforme se reciben los títulos de las películas con cada petición AJAX.
254