¡Esta es una revisión vieja del documento!
Tabla de Contenidos
Gestión estructurada de excepciones y depuración de código
Módulo perteneciente al curso Programación avanzada con JavaScript y ECMAScript.
Introducción
Asumámoslo: errar es humano, y si eres programador tienes más probabilidades todavía de cometer errores, puesto que escribir código no es nada fácil generalmente.
Cuando uno comienza en programación sorprende descubrir que se dedica más tiempo a corregir errores y excepciones que a crear nuevo código. Es un hecho universal y conviene asumirlo.
Así que, dando por hecho que vas a cometer errores, el propósito de este módulo es repasar las herramientas básicas de depuración con las que cuenta un programador de JavaScript directamente en los navegadores más populares.
En este módulo aprenderás:
- Cómo gestionar excepciones y errores en JavaScript
- Las herramientas para desarrolladores de Chrome y cómo nos ayudan a depurar
- El manejo de la consola y la pestaña “Sources” para depurar nuestro código JavaScript.
Carga asíncrona y diferida de scripts
Aunque una cuestión como “¿dónde coloco una etiqueta en una página?” puede parecer carente de sentido a simple vista, no lo es si pensamos en la enorme cantidad de scripts que se cargan hoy en día en algunas aplicaciones, y en especial en las aplicaciones de tipo Single Page Application (SPA).
Por ejemplo, observa los scripts que carga inicialmente una SPA como es GMail y que además está súper-optimizada por parte de Google:
Carga de recursos JavaScript de GMail
Como vemos son en total 70 scripts que pesan más de 1,5 MB entre todos, y tardan un tiempo considerable en cargar. Y eso sin contar los scripts que van embebidos en la propia página y que por lo tanto no se cargan de forma externa.
Evidentemente GMail no carga todos estos scripts con etiquetas de tipo <script>, sino que usa otras técnicas y los va cargando bajo demanda a medida que los necesita, etc… Es solo una forma de mostrar la complejidad de scripts de algunas aplicaciones en la actualidad.
Así pues, ante tanto script y con semejante tamaño: ¿En qué parte de una página es mejor colocar los scripts? ¿Es posible ganar rendimiento en su carga de alguna manera?
El procesamiento de los elementos de una página
Si analizamos cómo carga una página sus contenidos podemos ver enseguida que el lugar en el que se coloquen puede tener mucha influencia sobre su rendimiento.
Una vez que el navegador se descarga el código fuente de una página web comienza a analizar sus etiquetas por orden, de arriba a abajo. Es decir, en una página normal primero leerá el doctype, luego la etiqueta raíz HTML, dentro de ésta la cabecera, los elementos de ésta, luego el cuerpo, etc…
En el caso concreto de los scripts, éstos deben ejecutarse a medida que se encuentran en la página. Por ello si colocamos una etiqueta <script> apuntando a un archivo .js externo, antes de continuar cargando el resto de la página y seguir montando el DOM, el navegador debe descargar ese archivo e interpretar y ejecutar su código.
Por este motivo, los scripts que están en la cabecera deben descargarse del servidor y deben ser interpretados antes de hacer nada más. Es evidente que esto, con scripts grandes y complejos, ralentiza la carga de la página.
Sin embargo, el lugar tradicional para ubicar los scripts siempre ha sido la cabecera. Es más, incluso se consideraba una buena práctica porque así estaban todos en el mismo sitio y era todo más ordenado. Claro que eran otros tiempos, y los scripts de la época eran por regla general sencillos y poco pesados.
Que quede claro que aunque el funcionamiento es como el que describo, en una página normal y corriente el efecto de poner los scripts en un lugar o en otro es totalmente indetectable y no se notará disminución de velocidad ni merma del rendimiento. Yo mismo, de hecho, suelo ponerlos siempre en la cabecera por costumbre. Es solamente en páginas con scripts grandes y complejos donde este efecto puede llegar a notarse.
Por todo esto, la recomendación en aplicaciones complejas es que pongamos las etiquetas de script lo más tarde posible en la página, justo antes de la etiqueta de cierre del cuerpo (</body>), de modo que todo el HTML haya ya sido procesado, el DOM esté creado y el retardo debido a cargar e interpretar los scripts no influya en el tiempo de carga percibido por el usuario.
Otra cuestión importante a tener en cuenta es que, en la mayor parte de las aplicaciones SPA grandes, los scripts se crean en forma de módulos, bien de ECMAScript o bien que usen alguna otra técnica de módulos como la API Asynchronous Module Definition o AMD, y se cargan dinámicamente. Esto presenta, entre otras muchas, la ventaja de que solo se cargan los módulos cuando se necesitan y no antes (acelerando la carga global de la página) y que además definimos dependencias entre ellos, el orden adecuado para cargar, la carga asíncrona de los mismos, etc…
El atributo async
Otra opción muy interesante que nos habilita HTML5 es especificar explícitamente que la carga de nuestros scripts debe hacerse de manera asíncrona.
En realidad, si los scripts que debemos cargar no ejecutan código, sino que se encargan simplemente de definir clases y módulos que luego vamos a utilizar al terminar de cargar la página, obligar a que éstos se procesen e interpreten durante la carga no es necesario.
Para conseguirlo lo único que tenemos que hacer es marcar la etiqueta script con el atributo async, así:
<script type="text/javascript" src="scripts/miScript.js" async></script>
De este modo cuando el intérprete del navegador se encuentra esta etiqueta en una página lo que hace es empezar la descarga e interpretación del script en segundo plano, mientras sigue con el procesamiento del resto del código fuente de la página. Así, no se detiene la carga de la página como pasaría normalmente.
El soporte de esta etiqueta es casi universal como podemos ver en CanIUse:
imagen
Y solo los navegadores antiguos (concretamente IE9 o anterior) cargarían los scripts del modo normal.
Dos cosas importantes a tener en cuenta con este atributo:
- Solo funciona en scripts externos, es decir, scripts que tengan el atributo src para apuntar a un archivo .js. En scripts en línea, por supuesto, carece de sentido.
- Dado que los scripts se cargan en segundo plano puede darse la circunstancia de que archivos que están más adelante en el código pero son más pequeños se carguen e interpreten antes que archivos .js anteriores en el código. Si el orden de ejecución es importante no nos servirá, así que mucho ojo.
El atributo defer
Otro atributo muy interesante para los scripts es defer. Este atributo lo que consigue es diferir la carga e interpretación de los scripts marcados con el mismo hasta que la página se haya cargado por completo, independientemente de donde estén colocados en el código.
Su sintaxis es muy sencilla:
<script type="text/javascript" src="scripts/miScript.js" defer></script>
De este modo la etiqueta no se utiliza hasta que la página ha cargado por completo. Así, el incluir los scripts aunque sea en la cabecera no ralentiza en absoluto la carga de la página.
El soporte es universal también:
imagen
En este caso el orden de ejecución sí que se conserva por lo que no tiene el problema que causaba el uso de async, y garantizamos un orden de ejecución.
Del mismo modo este atributo solo tiene efecto en scripts externos, y no en scripts en línea.
Internet Explorer fue el “inventor” de este atributo allá por el año 1997 ¡con IE4!. En versiones antiguas de este navegador (hasta la 9, por eso están en un verde diferente en la figura anterior) el atributo defer sí que actuaba sobre los fragmentos en línea, lo cual cambia el comportamiento que tenemos actualmente. A mí me parecía más lógico, la verdad. Aunque no influye demasiado en el código, salvo por el orden de ejecución, puede dar algunos problemas en versiones antiguas de IE si no lo tenemos en cuenta.
En resumen
En páginas y aplicaciones normales no tiene demasiada importancia dónde coloquemos las etiquetas de script dentro del código HTML. En aplicaciones grandes y en las que cada milésima de segundo cuenta para el rendimiento tenemos diversas opciones para cargar scripts de manera que no impacten en la velocidad de carga de la página:
- Colocarlos al final de todo del cuerpo, antes de la etiqueta
</body>. - Usar alguna API de carga asíncrona de módulos, como AMD, módulos de ECMAScript o bibliotecas especializadas como WebPack.
- Utilizar el atributo
async. - Usar el atributo
defer.
Gestión estructurada de excepciones en JavaScript
Hay una verdad universal en el desarrollo de software y es que “las excepciones son inevitables”.
Por muy bien que esté nuestro código siempre se van a dar situaciones en las que fallará el programa debido a cualquier tipo de cuestión: valores para una función con los que no contábamos, algo extraño que introduce un usuario, cambios en los navegadores, fallos en las comunicaciones…
Pero que sean inevitables, no quiere decir que no podamos preverlos y tratar de gestionarlos para limitar sus consecuencias. Eso es lo que nos permite conseguir la gestión de excepciones.
En JavaScript tenemos la posibilidad de utilizar la conocida estructura try-catch-finally, propia de muchos otros lenguajes, sólo que con algunas limitaciones.
Los bloques de este tipo nos permiten definir una zona de código que queremos proteger (try), otra zona que es lo que queremos hacer en caso de que algo falle (catch) y lo que queremos que se ejecute siempre, pase lo que pase (finally).
La sintaxis general es la siguiente:
try { //Código a proteger } catch(e) { /Código a ejecutar si hay un fallo } finally { /Código a ejecutar siempre, haya o no fallos }
Por ejemplo, un caso muy típico es que intentemos hacer uso de una característica que no está presente en todos los navegadores y que por lo tanto puede fallar en algunos casos. Lo que debemos hacer es gestionar esos posibles fallos y actuar en consecuencia para utilizar una estrategia diferente o simplemente informar de este hecho al usuario.
Podríamos escribir un código como este:
try { hacerAlgo(); } catch (e) { alert("Se ha producido un error de tipo '" + e.name + "': " + e.message); }
Como vemos, la cláusula finally no es obligatoria y podemos obviarla.
Dentro del bloque catch podemos obtener una referencia a un objeto con información sobre el error y mostrar, por ejemplo, los detalles del mensaje.
La propiedad message del error nos devuelve información sobre por qué se ha producido.
La propiedad name nos indica el tipo de error, que puede ser de alguno de los siguientes:
- Error: error genérico lanzado por nosotros, como veremos en un instante.
- RangeError: cuando se produce un error de fuera de rango de valores válidos, por ejemplo, al pasar una cadena a una función que esperaba un número.
- ReferenceError: al hacer referencia a algo que no existe, por ejemplo llamando a una función que no está definida.
- TypeError: excepciones relacionadas con el uso del tipo incorrecto en una variable.
- EvalError: errores producidos al evaluar JavaScript dinámicamente con la función
eval. - SyntaxError: cuando al intentar ejecutar dinámicamente JavaScript con
evalse producen errores de sintaxis.
Generar excepciones propias
Podemos lanzar cualquiera de estos errores por nosotros mismos mediante la instrucción throw:
throw new Error("Se ha producido un error de comunicaciones");
Al lanzar una excepción por código detenemos la ejecución en ese punto, por lo que si está dentro de una función se saldría fuera de ésta.
Este sería el modo más habitual, instanciando un objeto de tipo Error genérico, pero podemos lanzar errores específicos de los indicados en la lista para denotar situaciones concretas.
Por ejemplo, imaginemos una función muy sencilla para calcular porcentajes de una cantidad sobre otra (sería una simple división). Si se le pasa un cero como segunda cantidad tendríamos una división entre cero, por lo que sería un valor no válido para esta función. Por otro lado, sólo podemos trabajar con números, así que si se nos pasa una cadena de texto u otro tipo deberíamos indicar que son valores no válidos también. Podríamos escribir una función como esta:
//Calcula el porcentaje de a sobre b function porcentaje(a, b) { if (typeof(a) != "number" || typeof(b) != "number") throw new TypeError("Sólo se pueden usar números"); if ( b==0 ) throw new RangeError("El segundo número no puede ser cero."); return (a/b)*100; } try { alert( porcentaje("2", 0) ); } catch (e) { alert("Se ha producido un error de tipo '" + e.name + "': " + e.message); }
Si ejecutamos este fragmento obtendremos una excepción (capturada por el catch) que nos indicará que sólo podemos utilizar números como argumentos de la misma (primera comprobación). Si cambiamos el “2” por un verdadero número (no una cadena como en el fragmento), lo que saltará entonces es una excepción de tipo RangeError para indicar que el valor 0 no es válido como segundo argumento.
De esta forma nos estamos anticipando a posibles cosas que pueden ocurrir, y generamos errores capaces de ser detectados en otras partes del código.
También podemos lanzar errores de nuestro propio tipo, que no sean ninguno de los de la lista, de la siguiente manera:
throw {name: "MiTipoDeError", message: "El mensaje de error"};
Los errores se transmiten por la pila de llamadas del programa, de modo que los podemos capturar en cualquier nivel de ésta. A este fenómeno se le llama convergencia de excepciones o “excepción bubbling”.
Así, si tenemos una función que llama a otra y ésta a una tercera, y se produce un error en la última, aunque el try-catch esté ubicado en la primera de todas, se capturará perfectamente. Si lo capturamos no sigue convergiendo. Es decir, si lo capturamos en la segunda función de la pila de llamadas, el error no se podrá detectar en un try-catch de la primera (o de la ventana, el global), a menos que lo relancemos con una instrucción throw.
Al contrario que en otros lenguajes de programación no es posible definir varios bloques catch especializados, uno por cada tipo de excepción. Si queremos tomar una acción distinta según sea el tipo de error producido, lo que tenemos que hacer es verificar el nombre de la excepción y actuar en consecuencia:
try { alert( porcentaje("2", 0) ); //Primer parámetro no válido } catch (e) { switch (e.name) { case "TypeError": alert( "Se ha pasado un valor no numérico"); break; case "RangeError": alert("No se puede dividir por cero"); break; default: alert("Se ha producido un error de tipo '" + e.name + "': " + e.message); } }
En el catch distinguimos el tipo de error a partir de su nombre actuando en consecuencia.
En un caso real, por ejemplo, si detectamos que alguno de los parámetros es del tipo incorrecto podemos rellamar a la función transformándolos antes en números con parseInt. Cada caso requerirá una acción correctora distinta, si es que es posible corregir de algún modo.
Anidamiento
Los bloques try-catch se pueden anidar.
Por ejemplo, definamos una función que nos permite obtener una referencia al objeto XMLHttpRequest (XHR) que permite lanzar peticiones HTTP en segundo plano para AJAX. Podría ser la siguiente:
function getXHR() { var xhr=null; if (window.ActiveXObject){ //IE try { xhr=new ActiveXObject("Msxml2.XMLHTTP"); } catch (e){ try{ xhr=new ActiveXObject("Microsoft.XMLHTTP"); } catch (e){ alert("Tu navegador no soporta AJAX!!"); } } } else { if (window.XMLHttpRequest) //Resto de navegadores xhr=new XMLHttpRequest() else alert("Tu navegador no soporta AJAX!!"); } }
Dentro del bloque correspondiente a Internet Explorer (el primero) intentamos instanciar el objeto usando una de las dos sintaxis posibles (según la versión del navegador). Si el primer intento falla en un navegador muy antiguo, capturamos la excepción y lo intentamos de nuevo. En el bloque catch hacemos el nuevo intento también empleando un try-catch por si fallara (sería un navegador realmente antiguo), en cuyo caso desistimos de instanciar el objeto.
Los bloques try-catch-finally añaden bastante carga al motor de JavaScript. Si abusamos de ellos podríamos notarlo en el rendimiento, si bien esto sólo es importante en caso de scripts complejos y grandes donde el rendimiento sea clave.
Antiguamente se usaban códigos devueltos como valores de retorno de las funciones para indicar que se había producido algún tipo de excepción o error y se había gestionado. Este tipo de técnicas está obsoleto y no deberíamos utilizarlo. La gestión estructurada de excepciones, que es lo que se habilita gracias al uso de try-catch-finally y la convergencia de eventos, es la manera actual y recomendada de gestionar cualquier tipo de excepción que se produzca en nuestro código.
Captura de errores a nivel de página
Un último detalle relacionado con la captura de errores.
En una página web siempre podremos capturar cualquier error que se produzca y no hayamos gestionando, sea donde sea, utilizando el evento global error de la ventana, que se puede asignar mediante una propiedad global onerror o mediante el método tradicional addEventListener.
En caso de asignarlo con onerror, se espera una función con cinco parámetros opcionales:
- El mensaje de error
- La URL de la página en la que se ha producido
- El número de línea en el que se ha producido
- El número de columna en el que se ha producido
- Una referencia a un objeto de tipo Error que, realmente, lo único que aporta es poder ver su tipo exacto llegado el caso.
Por ejemplo:
window.onerror = LogJSError; function LogJSError(msg, url, linea) { console.log("Se ha producido un error: " + msg + " en la línea " + linea ); }
Puedes capturarlo usando el modelo de eventos del DOM si quieres crear una biblioteca o usarlo de manera más profesional. En este caso, el objeto evento que se le pasa al manejador de eventos es de tipo ErrorEvent y proporciona las propiedades:
message: el mensaje de errorlineno: el número de línea en el que se ha producido el errorcolno: la columna dentro de esa líneafilename: el nombre (y ruta completa) del archivo en el que se ha poducido el error.
Funcionaría así:
window.addEventListener('error', function (ev){ console.log("Se ha producido un error: " + ev.message + " en la línea " + ev.lineno + " del archivo " + ev.filename); })
Este manejador global nos puede resultar útil en ocasiones.
El modo estricto de JavaScript
Modo restringido de trabajo que podemos activar de manera voluntaria y nos permite tener menos errores. Permite también al intérprete de JavaScript ser más eficiente.
var miVar = 1; function test() { mivar = 5; alert(miVar); } test();
Si ejecutásemos el código anterior, no habrá errores. El error tipográfico que cometimos, no escribiendo correctamente la variable (JavaScript distingue mayúsculas de minúsculas), JavaScript lo interpretó como que teníamos dos variables (miVar y mivar).
Si establecemos el modo estricto:
"use strict"; var miVar = 1; function test() { mivar = 5; alert(miVar); } test();
El código fallará con el siguiente mensaje:
Uncaught ReferenceError: mivar is not defined
Podríamos activar el modo estricto en funciones y no en todo el script:
var miVar = 1; function test() { "use strict"; mivar = 5; alert(miVar); } test();
El modo estricto también es útil para prevenir la redefinición de parámetros o propiedades.
"use strict"; // Nos equivocamos a propósito en el nombre de // uno de los parámetros, que repetimos function test(p1, p2, p1) { alert(p1); alert(p2); } test(1, 2, 3);
Al tratar de ejecutarlo, nos mostrará el siguiente error:
Uncaught SyntaxError: Strict mode function may not have duplicate parameter names
Otro ejemplo:
"use strict"; var objeto = { prop1: "Hola", prop2: "Adiós", prop1: "Otra cosa" }; alert(objeto.prop1);
Si no hubiésemos usado el modo estricto, JavaScript se quedará con la última propiedad definida (Otra cosa);
También podemos usar el modo estricto para restringir el ámbito de las variables:
// Evaluación de código JavaScript eval("var x = 5;"); alert(x);
En el modo estricto, las variables definidas en la función eval, no tienen ámbito global, así que nos mostraría el siguiente error:
Uncaught ReferenceError: x is not defined
Con el modo estricto tampoco prodíamos redefinir funciones existentes:
"use strict"; // Lo siguiente no estaría permitido: eval = "Hola"; arguments = "Adiós";
Mostraría el error:
Uncaught SyntaxError: Unexpected eval or arguments in strict mode
function test(p1) { p1 = "10"; alert(p1); alert(arguments[0]) } test(5);
Los alerts devuelven el mismo valor, pero en modo estricto tendríamos el valor que se le pasa a la función.
- Strict mode (en la MDN)
Herramientas del desarrollador
Herramientas de depuración JavaScript de Firefox
Todos los navegadores Web incorporan de serie herramientas avanzadas para los desarrolladores. No es necesario instalar nada, ni tampoco hay que gastar dinero alguno: todo lo que necesitas para depurar un script lo tienes incorporado en todos los navegadores modernos.
Hasta Internet Explorer, por ejemplo, incorpora de serie (¡desde su versión 8! desde hace ya muchísimos años) unas herramientas llamadas “F12 Developer Tools” con casi todo lo que uno pueda necesitar para depurar JavaScript, HTML y CSS. Para lanzarlas, lo único que tenemos que hacer es visitar la página que queremos depurar y pulsar la tecla F12 (de ahí el nombre). Ya las hemos visto en uso, aunque por encima, a lo largo del curso en diversos vídeos.
Otro atajo habitual para abrirlas, si no te funciona F12, es usar la combinación CTRL+MAYs+I (o CMD+OPT+Ctrl en macOS).
Aunque con pequeñas diferencias entre ellas, todas estas herramientas ofrecen multitud de utilidades para depurar: inspección de elementos, análisis de hojas de estilo, estilos resultantes, modificación al vuelo de la página, depuración paso a paso de scripts, medidor de rendimiento de descarga de páginas, análisis de tráfico de red… y otras muchas.
Google Chrome es casi el estándar de facto en estas herramientas, ya que es con diferencia el navegador más usado del mercado. Además, muchos otros navegadores como Opera, Brave o incluso Microsoft Edge utilizan Chrome por debajo, y usan por lo tanto las mismas herramientas.
Dado que en realidad cualquiera de los navegadores modernos ofrece casi lo mismo, aunque cambie la interfaz, pero en cualquier caso todo lo necesario para trabajar a buen nivel eliminando errores en JavaScript, y no podemos explicar con detalle el uso de todos, vamos a quedarnos con una de ellas.
Las que he elegido son las herramientas de Google Chrome. El motivo principal para decidirme por éstas es que son seguramente las más completas y potentes, Chrome es el navegador con mayor cuota de uso con mucha diferencia, y son idénticas en todos los sistemas operativos. De este modo, uses Windows, Linux o Mac, las herramientas de depuración para desarrolladores son exactamente iguales y funcionan con la misma capacidad.
Lo aprendido en Chrome nos servirá con ligeros cambios en los demás navegadores. Sólo hay que buscar en dónde está el menú o botón correspondiente, ya que en realidad todas las herramientas son muy parecidas.
Las herramientas de desarrollo de Chrome son un mundo y tienen infinidad de pequeñas características y detalles de interés. Explicarlo todo es imposible (necesitaríamos un curso propio solo de esto) y además le están incorporando pequeñas mejoras constantemente. En esta sección vamos a ver lo más importante. Para el resto te refiero a la documentación oficial de las herramientas del desarrollador de Chrome. Las de Firefox, por si te interesa indagar también por tu cuenta (deberías) tienen su documentación oficial aquí.
Examinando el DOM
Desde las herramientas de desarrollador, en la pestaña Elementos podemos ver todos los elementos que componen una página (etiquetas HTML).
Al colocarnos encima de cada elemento HTML, se resalta en la página para saber a qué corresponde. También podemos hacerlo al revés, es decir, colocarnos sobre un elemento de la web y se resaltará qué parte del código corresponde. Para esto último, hay que pulsar en el icono con una flecha que hay a la izquierda de la pestaña Elementos.
Podemos editar directamente el HTML y el CSS para ver cómo cambiaría la web. Este cambio es solo local y temporal. Es útil mientras desarrollamos.
Si marcamos un elemento en el DOM y pulsamos H, lo ocultará. Pulsando de nuevo H volverá a mostrarlo.
Podemos ver todos los cambios que hemos hecho, podemos elegir Cambios y ver qué cambios hemos hecho respecto al contenido original
Emulación de dispositivos y diseño responsivo
En Google Chrome, en las herramientas para desarrolladores, hay un botón en la parte superior izquierda que permite activar / desactivar la emulación de dispositivos. Podremos modificar las dimensiones, el tipo de dispositivo, si es un dispositivo de escritorio o móvil, regular la “potencia” del dispositivo, etc.
Tomando el control de los recursos de una página
En Google Chrome, en la pestaña Aplicación podremos examinar cualquier recurso de una aplicación: scripts, cookies, imágenes, tipografías, lo que se almacene en local, etc.
Análisis de la descarga de una página
En Google Chrome, desde la pestaña Network (Red), que permite analizar las comunicaciones de red que tiene el navegador con el servidor o servidores con los que se comunica.
Podemos ver peticiones de imágenes, documentos, scripts, etc. con sus estados HTTP (200, 204, 404…), versión de protocolo HTTP, dominio, tamaño de los ficheros, tiempo de descarga…
Podemos también ver el detalle de la petición de cada recurso (cabeceras que se envían, las que se reciben, tiempos…).
Depuración con las herramientas del desarrollador
Uso manual de la consola del navegador
En Google Chrome, en las herramientas para desarrolladores, vamos a la pestaña Consola. En esta pestaña podemos ver mensajes que muestra la página, errores y también nos permite ejecutar código en la página web. Cualquier instrucción JavaScript que escribamos, afectaría a la página que estamos visualizando.
EL dólar ($) es un atajo de documento.querySelector, es decir, podemos hacer $('#main-nav li'). Para obtener más de un elemento con el mismo selector, utilizaremos dos símbolos de dólar: $$('.link').
Comandos interesantes de la consola de Google Chrome:
copydir: muestra las propiedades JavaScript que tendría un elemento.monitor: monitoriza el uso de lo que le indiquemos por parámetro.unmonitor: para dejar de monitorizar cierta función (lo contrario de la función anterior).monitorEvents: notifica cuando se producen ciertos eventos.unmonitorEvents: para dejar las notificaciones de la función anterior.
