3. Programar el lado del cliente

JavaScript es un lenguaje orientado a objetos, funcional, dinámico e interpretado, usado principalmente como el lenguaje de programación de las páginas web en el lado del navegador. Actualmente, sin embargo, se usa también en la parte del servidor en entornos como Node.js o MongoDB. El nombre del estándar que regula JavaScript es ECMAScript. Los navegadores recientes entienden las últimas versiones de ECMAScript; cada año se publica una nueva versión (por ejemplo, ECMAScript 2019, también conocido como ES10).

Importante

Las habilidades que deberías adquirir con este tema incluyen las siguientes:

  • Comprender los elementos básicos de JavaScript como lenguaje de programación orientado a objetos.

  • Saber programar en JavaScript la parte del cliente de una aplicación web usando la API de los navegadores para la gestión del DOM, eventos, estilos, etc.

  • Usar las herramientas para desarrolladores integradas en los navegadores, como Chrome DevTools, para depurar un programa escrito en JavaScript.

Hazlo tú ahora

Prepárate para este tema, leyendo en primer lugar el capítulo de introducción a JavaScript del libro «Client-Side Web Development», donde se explican los conceptos básicos del lenguaje.

3.1. Una aplicación web sencilla

El siguiente código muestra en acción, a modo de introducción, los elementos básicos del lenguaje JavaScript (variables, condicionales, bucles, funciones, etc.), así como la utilización de las APIs del navegador relacionadas con la gestión del DOM, de los eventos o de los estilos, para, con todo ello, presentar una aplicación web muy sencilla que permite añadir dinámicamente contenido a una página web e interactuar con el contenido añadido. Lee los comentarios para entender el propósito de cada línea, pero ten en cuenta que en actividades posteriores ampliaremos todos estos elementos.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
<!doctype html>
<!-- Ejemplos de uso de la API DOM y de la API de eventos del navegador -->
<html lang="es">
  <head>
    <meta charset="utf-8">
    <title>Ejemplos de uso de la API DOM y de la API de eventos del navegador</title>

    <!-- Es recomendable que tanto estilos como scripts vayan en ficheros aparte, pero
    en este caso los dejamos aquí por ser una prueba de concepto -->
    <style>
      @import url('https://fonts.googleapis.com/css?family=Lato&display=swap');
      * {
        margin: 0px;
        padding: 0px;
        box-sizing: border-box;
      }
      body {
        font-size: 18px;
        font-family: 'Lato', sans-serif;
        margin: 10px;
      }
      h3.destacado {
        color: lightseagreen;
      }
      h3 {
        color: black;
        font-size: 1.3em;
        display: inline;
        margin-right: 10px;
      }
      form {
        margin-bottom: 20px;
      }
      section {
        border-top: 1px solid lightgray;
	      margin-top: 10px;
        padding-top: 5px;
        margin-bottom: 10px;
      }
    </style>

  </head>
  
  <body>

    <!-- Formularios: -->
    <form id="f1">
      <label>Crea sección con título:</label> 
      <input type="text" id="título"> 
      <button>Crea sección</button>
    </form>
    <form id="f2">
      <label>Destaca secciones cuya primera palabra sea:</label> 
      <input type="text" id="palabra"> 
      <button>Marca sección</button>
    </form>
    <form id="f3">
      <button>Desmarca todas las secciones</button>
    </form>

    <!-- El elemento main está vacío inicialmente, pero irá conteniendo las secciones que 
      se crearán dinámicamente. -->
    <main></main>

    <script>
      // Manejador del evento de marcar/desmarcar de una sección individual:
      function marcaDesmarcaUnaSección (event) {
        // event.currentTarget es el elemento al que se asoció este manejador;
        // event.target es el elemento sobre el que se produjo la acción;
        // parentNode devuelve el nodo padre de un elemento dado;
        // toggle añade el valor 'destacado' a class si el elemento no lo tiene y lo
        // borra si lo tiene:
        event.target.parentNode.querySelector('h3').classList.toggle('destacado');
      }

      // Manejador del evento de creación de una nueva sección a partir del título
      // indicado en el formulario; no se declara el parámetro del evento porque 
      // no se va a usar, pero creaSección se invocará por el navegador con el
      // argumento del evento igualmente:
      function creaSección () {
        // Obtiene el título del formulario; querySelector devuelve el primer nodo
        // que cumple con el criterio del selector desde el objeto desde el que 
        // se invoca la función (en este caso, el elemento raíz, document):
        let título= document.querySelector('#título').value;
        // Borra el campo del formulario:
        document.querySelector('#título').value= '';
        // Crea el elemento padre para la nueva sección:
        let sección= document.createElement('section');
        // Una forma de añadir HTML dinámicamente es mediante el atributo innerHTML;
        // es más cómoda, pero es más fácil cometer errores en el HTML.
        // Además, innerHTML debe ser usado con prudencia y nunca con cadenas no
        // generadas por nuestra aplicación; la siguiente instrucción desobedece
        // esta regla y hace que la aplicación pueda recibir potencialmente
        // un ataque tipo XSS (por *cross-site scripting*); para comprobarlo, prueba
        // a introducir como título: <img src="x" onerror="alert('¡Ataque XSS!')">
        // Para evitar estos ataques, se recomienda usar textContent cuando 
        // hay contenido no propio o sustituir ciertos caracteres en las cadenas
        // antes de usar innerHTML:
        sección.innerHTML= '<h3>'+título+'</h3>';
        // Otra forma de añadir HTML dinámicamente es mediante la APIS de creación
        // e inserción de nodos:
        let botón= document.createElement('button');
        botón.textContent= 'Marca/desmarca';
        sección.appendChild(botón);
        // Todos los botones individuales creados para cada sección tienen el
        // mismo manejador de eveneto; el parámetro del manejador nos permitirá
        // determinar sobre qué botón concreto se ha hecho clic:
        botón.addEventListener('click',marcaDesmarcaUnaSección,false);
        // Insertamos la sección dentro de <main>:
        document.querySelector('main').appendChild(sección);
      }

      // Itera sobre todos los elementos h3 (por eso usamos querySelectorAll y no 
      // querySelector)
      function destacaSecciónSegúnPalabra () {
        let secciones= document.querySelectorAll('h3');
        // Obtiene la palabra a buscar del formulario:
        let palabra= document.querySelector('#palabra').value;
        // Borra el campo del formulario:
        document.querySelector('#palabra').value= "";
        // Itera sobre todos los elementos h3:
        for (let i=0;i<secciones.length;i++) {
          // Primera palabra del texto contenido en el h3:
          let primera= secciones[i].textContent.split(" ")[0];
          // En JavaScript, el operador == puede realizar conversiones de tipo antes
          // de la comparación; con el operador === no hay conversiones implícitas:
          if (primera===palabra) {
            // Añade 'destacado' como class si la palabra coincide:
            secciones[i].classList.add('destacado');
          }
        }
      }

      // Desmarca todos los h3 del documento quitando la clase 'destacado'
      // de sus encabezados:
      function desmarcaTodasSecciones () {
        let secciones= document.querySelectorAll('h3');
        for (let i=0;i<secciones.length;i++) {
          secciones[i].classList.remove('destacado');
        }
      }

      // Función de inicialización:
      function init () {
        // Asignación de manejadores de eventos; el segundo parámetro es el nombre de 
        // una función, pero no se llama (aún) a dicha función; el tercer parámetro se
        // estudiará más adelante:
        let e= document.querySelector('#f1 button');
        e.addEventListener('click',creaSección,false);
        e= document.querySelector('#f2 button');
        e.addEventListener('click',destacaSecciónSegúnPalabra,false);
        e= document.querySelector('#f3 button');
        e.addEventListener('click',desmarcaTodasSecciones,false);
        // Se evita para todos los formularios que al aceptar su contenido (pulsando el
        // botón o enter) se envíe una petición al servidor y se recargue la página:
        let fs= document.querySelectorAll('form');
        for(let i=0;i<fs.length;i++) {
          fs[i].addEventListener(
            'submit',
            function (event) {
              event.preventDefault();
            }, 
            false
          );
        }
  
        // Usando construcciones recientes del lenguaje para iterar sobre un array 
        // de elementos y para indicar mediante funciones flecha (o lambda) la función
        // a ejecutar sobre cada elemento, el código anterior sería: 
        //
        // Array.from(document.querySelectorAll('form')).forEach(
        //  (formulario) => {
        //    formulario.addEventListener(
        //      'submit',
        //      (event) => {event.preventDefault();}, 
        //      false
        //    )
        //  }
        // );

      }

      // Define init como la función que será invocada cuando se dispare el
      // evento DOMContentLoaded, que señala que el árbol DOM ya está construido
      // (y sus nodos disponibles en memoria), aunque quizás aún no se hayan
      // descargado todas las imágenes, aplicados todos los estilos, etc.;
      // si en lugar de init pusiéramos init() el efecto sería diferente (y
      // probablemente no deseado):
      document.addEventListener('DOMContentLoaded',init,false);
    </script>

  </body>

</html>

3.2. El lenguaje de programación JavaScript

En esta actividad ampliaremos el estudio de los elementos fundamentales del lenguaje JavaScript. Las características adicionales a las que un programador puede acceder cuando escribe programas en JavaScript para ser ejecutados por un navegador se estudiarán en la siguiente actividad.

Los conceptos que tienes que comprender del lenguaje se encuentran recogidos en estas diapositivas.

Hazlo tú ahora

Lee el capítulo sobre programación funcional en JavaScript del libro «Client-Side Web Development», donde se explican las características del lenguaje relacionadas con el paradigma funcional, entre las que trabajaremos especialmente con el concepto de clausura. Puedes practicar con la consola de JavaScript de las Chrome Devtools o con una consola en línea.

3.2.1. Ámbitos y clausuras

Las clausuras y su relación con las variables declaradas con var o let es uno de los aspectos que más cuesta entender a los programadores que acaban de empezar con JavaScript. Existen cuatro tipos de ámbitos para las variables en JavaScript:

  • ámbito de función: corresponde a las variables locales declaradas con var; estas variables se almacenan en la pila y pueden usarse incluso antes de haber sido definidas; esto es así por un mecanismo conocido como izado (hoisting) que aupa las declaraciones de variables locales (pero no sus inicializaciones) al comienzo de la función; declarar dos o más veces una variable con var dentro de la misma función equivale a declararla una sola vez al comienzo de esta; si intentamos leer el valor de una variable antes de su declaración en el código y antes de haberle asignado ningún valor obtenemos el valor undefined;

  • ámbito de bloque: corresponde a las variables locales declaradas con let; estas variables se almacenan en la pila también, pero no hay ningún proceso de izado y la variable se circunscribe al ámbito en el que ha sido declarada; dos variables declaradas en ámbitos diferentes de una misma función tienen espacios separados en la pila; no se pueden declarar dos variables de este tipo con el mismo nombre dentro del mismo contexto; si se declara una variable con let dentro de un bucle, se reserva sitio en la pila para una variable distinta en cada iteración; el uso de let está permitido en el lenguaje desde la versión 6 de ECMAScript, publicada en 2015, por lo que es normal que encuentres muchos ejemplos de código que no lo usan;

  • ámbito global: corresponde a las variables globales declaradas fuera de cualquier función; estas variables se almacenan en el heap y sus declaraciones también son izadas al principio del ámbito global;

  • ámbito léxico: corresponde al hecho de que una función definida dentro de otra función puede acceder a las variables locales de esta última; si una función interna sobrevive a la función contenedora, las variables referenciadas no se borran de la memoria (a la asociación entre la función y las variables externas se le conoce como clausura);

Las declaraciones de funciones locales y globales también sufren el mecanismo de izado en sus ámbitos respectivos.

Estudia el siguiente código y ejécutalo (pulsando en run) después de dedicar un rato a pensar qué valores imprime por la consola.

function f () { var i=0; var x= {}; { var i=0; x.f1= function() { console.log(i); }; } i++; { var i=1; x.f2= () => {console.log(i);}; } i++; return x; } var x= f(); x.f1(); x.f2();

La variable i se declara con var y, por tanto, su ámbito es el de la función f. No importa que usemos var varias veces a continuación dentro de la función, incluso aunque estas declaraciones adicionales estén dentro de un nuevo ámbito. En la pila de ejecución solo se reserva sitio para una variable cuando se ejecuta la función y esta única posición es la que no se destruye al salir de la función debido a las clausuras que se crean por las dos funciones anónimas asignadas a x.f1 y x.f2. La variable i vale 2 al salir de la función y este valor es el que se usa al llamar a las dos funciones.

Nota

Observa de paso que para introducir nuevos ámbitos no es necesario usar una instrucción condicional o un bucle, sino que en JavaScript, como en la mayoría de lenguajes, basta con encerrar un bloque de código entre llaves para conseguirlo.

Sin embargo, si usamos let en lugar de var en las declaraciones de i, el ámbito de cada variable será el del bloque y tendremos tres variables distintas en la pila de ejecución justo antes de salir de la función. La primera de ellas será destruida en ese momento, pero las otras dos se salvarán de dicha destrucción para mantener las clausuras:

function f () { let i=0; let x= {}; { let i=0; x.f1= function() { console.log(i); }; } i++; { let i=1; x.f2= () => {console.log(i);}; } i++; return x; } let x= f(); x.f1(); x.f2();

3.3. Las APIs del navegador para programar el lado del cliente

Los navegadores incluyen una serie de librerías estandarizadas para programar la parte de la aplicación web que se ejecuta en el navegador (lo que se conoce como el front-end de la aplicación, en oposición al back-end, que denotaría la parte del servidor). En esta actividad profundizaremos en las APIs para el manejo del árbol DOM, la gestión de eventos y el control de estilos. Otras APIs, como la que permite realizar peticiones asíncronas desde el cliente a un servidor, se explorarán más adelante.

Los conceptos que tienes que estudiar de estas APIs se encuentran recogidos en estas otras diapositivas.

Hazlo tú ahora

Lee el capítulo sobre el Document Object Model del libro «Client-Side Web Development», donde se explican las principales funciones de las APIs ya mencionadas. Puedes practicar con la consola de JavaScript de las Chrome Devtools o con entornos como JSFiddle.

3.4. Herramientas para desarrolladores

Las herramientas para desarrolladores que incorporan los navegadores permiten depurar el código en JavaScript de la parte del cliente de una aplicación web.

Hazlo tú ahora

Familiarízate con las opciones de depuración de JavaScript de las pestañas Console y Sources del entorno de las Chrome DevTools siguiendo estas páginas de su documentación: Console Overview , Get Started, Pause your Code with Breakpoints y JS Debugging Reference. Practica las distintas posibilidades en DevTools con una aplicación web como la de la primera actividad de este tema.

3.5. Profundizar en JavaScript

JavaScript tiene obviamente muchos más elementos que los que hemos explorado en este tema. Si quieres ampliar por tu cuenta tu conocimiento del lenguaje, puedes seguir con referencias como The Modern JavaScript Tutorial o Eloquent JavaScript.