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 2020, también conocido como ES11).
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.
3.1. Introducción al lenguaje de programación JavaScript¶
Hazlo tú ahora
Estudia todos los vídeos de la serie «Introducción a JavaScript» en los que se presentan los elementos principales del lenguaje: parte 1-1, parte 1-2, parte 1-3, parte 1-4, parte 1-5 y parte 1-6.
Elementos más avanzados del lenguaje o 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 más adelante. Los conceptos que tienes que comprender del lenguaje se encuentran recogidos en estas diapositivas, que también contiene información sobre elementos más avanzados que estudiaremos más adelante.
Hazlo tú ahora
Practica con los diferentes conceptos de JavaScript hasta tener claro su uso. Puedes practicar usando la consola de JavaScript de las Chrome Devtools, instalando Node.js, abriendo una consola en línea o creando un proyecto en repl.it de tipo Node.js.
Nota
A diferencia de lo que ocurre en otros lenguajes, el punto y coma actúa en JavaScript como un separador de instrucciones y no como un finalizador de ellas. El estándar establece que su uso es opcional en ciertos casos (básicamente, cuando el intérprete puede determinar dónde acaba una instrucción y empieza la siguiente), pero en ocasiones esto puede llevar a que el motor de JavaScript interprete el código de forma diferente a la que teníamos en mente. Por ejemplo, el código siguiente:
1 2 3 4 | const a = 1
const b = 2
const c = a + b
(a + b).toString()
|
produce un error b is not a function
porque el intérprete de JavaScript lo analiza como si estuviera escrito así:
1 2 3 | const a = 1;
const b = 2;
const c = a + b(a + b).toString();
|
Por ello, se recomienda a los novicios del lenguaje que siempre finalicen cada instrucción con punto y coma.
3.2. Una aplicación web sencilla¶
El código que se incluye más abajo 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.
Hazlo tú ahora
Lee los comentarios para entender el propósito de cada línea, pero ten en cuenta que en actividades posteriores ampliaremos estos elementos. Estudia después todos los vídeos de la serie «Una aplicación web sencilla con JavaScript» en los que se comenta este código con detalle: parte 2-1, parte 2-2, parte 2-3, parte 2-4 y parte 2-5. Repasa después, otra vez, el código y asegúrate de que ahora comprendes mejor cada una de sus instrucciones.
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 evento; 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>
|
Nota
Observa lo que se menciona sobre ataques XSS en los comentarios del código anterior. En un entorno de producción nunca deberías integrar una subcadena no verificada (por ejemplo, un valor introducido por el usuario en un formulario o el resultado devuelto por un servidor) en una cadena que se asigna a un atributo innerHTML
, ya que el intérprete de JavaScript analiza e interpreta toda la cadena como HTML y podría terminar ejecutando código no deseado. La mejor recomendación es que uses innerHTML
para crear rápidamente nodos, pero que cuando vayas a añadir contenido de terceros lo metas directamente como valor del atributo node.textContent
, donde node
es el nodo donde quieres insertar el texto. El navegador no intenta interpretar el contenido de textContent
como HTML, sino que lo considera como texto plano, por lo que no hay riesgos en este caso. Modifica el código anterior para que funcione de esta manera.
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.
Nota
Otra opción para pausar la ejecución del código JavaScript en un determinado punto es añadir la instrucción debugger
(recogida en el estándar de ECMAScript) en una línea. Si las herramientas de desarrolladores están abiertas, la ejecución se detendrá en esa línea como si hubiéramos definido un punto de ruptura:
function foo (x) {
if (x < 0) {
debugger;
return x*x;
}
}
3.5. Objetos y prototipos¶
La manera en la que se gestionan los objetos en JavaScript es diferente a la que puedas conocer por otros lenguajes como Java o C++. JavaScript sigue el paradigma de la programación basada en prototipos, que algunos denominan también programación orientada a objetos sin clases. Para familiarizarte con este enfoque de la programación orientada a objetos, vas en esta ocasión a leer un texto en inglés, lo que te permitirá no solo profundizar en el tema, sino desarrollar tu habilidad para leer documentación en este idioma (recuerda que las fuentes primarias de documentación tecnológica suelen estar en inglés y que muchas veces el tiempo necesario para que haya información de calidad en otros idiomas sobre una materia novedosa puede ser inaceptablemente largo). En la actividad «Introducción al lenguaje de programación JavaScript» ya realizamos una introducción rápida a los objetos en JavaScript. Ahora vamos a estudiarlos con más detalle.
Hazlo tú ahora
Lee el capítulo «The secret life of objects» de la tercera edición del libro con licencia abierta «Eloquent JavaScript». Practica por tu cuenta cada concepto estudiado con un intérprete de JavaScript. Lee únicamente desde el principio hasta el apartado «Overriding derived properties» incluido.
La recomendación es que leas el fragmento del capítulo en su idioma original, pero si te cuesta seguir el texto de este modo, puedes leer una versión traducida automáticamente del capítulo. Recuerda que aunque la traducción automática entre lenguas similares ha mejorado mucho en los últimos años, los sistemas aún cometen errores que en ocasiones son difíciles de detectar porque la frase traducida suena bien en español, por ejemplo, pero no refleja el significado de la original en inglés. Desde el enlace anterior puedes ir alternando entre la versión traducida automáticamente y el texto original con los botones de la parte superior; además, si consultas la versión traducida automáticamente y colocas el puntero del ratón sobre una frase, se mostrará un texto flotante con el original. Si lees la versión traducida automáticamente, ve consultando el original para asegurarte de que el sistema no ha alterado su significado. Ten especial cuidado con el código en JavaScript cuyas variables o palabras reservadas, por ejemplo, pueden haber sido traducidas erroneamente.
De cara a comprender mejor el capítulo sobre la vida secreta de los objetos, se muestran a continuación algunas notas que te pueden servir para terminar de comprender los conceptos allí introducidos.
Importante
En primer lugar, no hay que confundir la propiedad prototype
de una función con el prototipo de un objeto. El prototipo de un objeto, entendido como un segundo objeto que contiene el conjunto de funciones y atributos compartidos con otros objetos, se almacena en una propiedad interna llamada [[Prototype]]
que no es directamente accesible desde tu código. No obstante, consultar o modificar esta propiedad interna se puede hacer indirectamente de dos formas:
Usando el atributo
__proto__
de un objeto, atributo que muchos navegadores han venido soportando pero que el estándar de ECMAScript solo ha reconocido recientemente, señalándolo, eso sí, como un atributo en vías de extinción (deprecated). Significa esto que puedes usarlo para acceder rápidamente al prototipo de un objeto mientras haces pruebas desde la consola del navegador o en la fase de desarrollo de un programa, pero nunca deberías usarlo sobre aplicaciones en producción porque futuras versiones de los navegadores podrían no soportarlo.Mediante las funciones
Object.getPrototypeOf()
yObject.setPrototypeOf()
reconocidas por el estándar del lenguaje como la forma correcta de acceso al prototipo interno.
El prototipo (interno) por defecto de los objetos de JavaScript es Object.prototype
, es decir, el prototipo (visible) de la función Object
.
Por otro lado, el valor por defecto del prototipo (visible) de una función es un objeto que contiene una única propiedad llamada constructor
que apunta a la propia función.
Un prototipo no deja de ser un objeto con una serie de funciones como el siguiente:
1 2 3 | protoOkapi = {
habla() { console.log("¡Hola!"); }
}
|
La definición anterior no parece seguir la notación que habíamos estudiado para crear objetos literalmente, pero lo que ocurre aquí es simplemente que, si omitimos el nombre de la propiedad, se utilizará para ella el mismo nombre que el de la función correspondiente, por lo que lo anterior es más o menos equivalente a:
1 2 3 | protoOkapi = {
habla: function habla() { console.log("¡Hola!"); }
}
|
Y también es equivalente a:
1 2 3 | protoOkapi = {
habla: function() { console.log("¡Hola!"); }
}
|
La función habla
no tiene en principio nada especial; podemos invocarla para que imprima el saludo con protoOkapi.habla()
.
Una vez tenemos nuestro prototipo, podemos crear objetos que lo usen y hereden sus propiedades mediante la función Object.create()
. Vamos a usarla en una función que, además de crear un objeto con dicho prototipo, asigna a ese objeto en particular un atributo con la edad del animal:
1 2 3 4 5 | function creaOkapi(proto,edad) {
let okapi= Object.create(proto);
okapi.edad= edad;
return okapi;
}
|
Con estos ingredientes, podemos crear dos objetos que representen sendos okapis:
1 2 3 4 | let o1= creaOkapi(protoOkapi,2);
let o2= creaOkapi(protoOkapi,4);
o1.habla();
o2.habla();
|
Aunque cada okapi tiene su propio atributo edad
(puedes imprimir o1.edad
y o2.edad
), el código de la función habla
no está duplicado en memoria, porque solo existe una instancia de la función (recuerda que las funciones son objetos de clase Function
y podemos, por tanto, manejar referencias a ellas) dentro del prototipo. Cuando escribimos o1.edad
el atributo se encuentra directamente en el objeto, pero cuando escribimos o1.habla()
la función no es un atributo directo de o1
, sino que, al no encontrarlo en el objeto, el intérprete lo busca en su prototipo.
Como hemos comentado, podemos acceder al prototipo del objeto con Object.getPrototypeOf(o1)
y en muchos navegadores, aunque no recomendado, con o1.__proto__
. De hecho, en un navegador que soporte __proto__
la comparación Object.getPrototypeOf(o1) === o1.__proto__
devolverá true
porque el prototipo no deja de ser un único objeto en memoria. También devolverá cierto la expresión o1.__proto__ === protoOkapi
en nuestro caso. Ahora bien, si el prototipo de un objeto es a su vez un objeto, como hemos visto, ¿cuál es el resultado de o1.__proto__.__proto__
o, dicho de otra forma, cuál es el prototipo del objeto protoOkapi
? Si mostramos el contenido de la expresión o1.__proto__.__proto__
por la consola del navegador, veremos un objeto con una serie de métodos como toString
, entre otros. Estos métodos vienen del prototipo de Object
, es decir, Object.prototype
, que es el prototipo usado por defecto para los objetos para los que no se define un prototipo específico. La expresión Object.prototype === o1.__proto__.__proto__
se evalúa, por tanto, a cierto.
El método habla
definido anteriormente se limita a imprimir siempre la misma cadena. Los métodos, sin embargo, suelen utilizar los atributos del objeto sobre el que se invocan. Como el intérprete de JavaScript se encarga de vincular this
al objeto sobre el que se ejecuta un método, podemos hacer que la función habla
imprima la edad del okapi con otro prototipo:
1 2 3 4 5 | protoOkapi = {
habla() { console.log("¡Hola! Tengo "+this.edad+" años."); }
}
let o3= creaOkapi(protoOkapi,6);
o3.habla();
|
Obviamente, si invocamos habla
directamente haciendo protoOkapi.habla()
, la variable this.edad
valdrá undefined
. Observa que podemos asociar una nueva función al okapi o1
haciendo:
1 2 | o1.camina = function() { console.log("Caminando voy."); }
o1.camina();
|
Pero, al no pertenecer al prototipo, la función camina
solo existe para el objeto o1
.
Nota
La variable polimórifica this
se revincula cada vez que se llama a una función por lo que no podremos usarla desde una función interna para referirnos al objeto this
de la función externa, excepto si la función interna es una función flecha. Para ilustrarlo, considera el siguiente bloque HTML:
1 2 3 | <div id="w">
<div id="x">X</div>
</div>
|
Cuando se hace clic en la equis mayúscula, el siguiente código de JavaScript muestra por consola la cadena w
excepto en el segundo caso en el que se produce una excepción por tener this.parentNode
el valor undefined
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | document.querySelector("#x").addEventListener("click",
function () {
console.log(this.parentNode.id);
});
document.querySelector("#x").addEventListener("click",
function () {
setTimeout(function () {
console.log(this.parentNode.id)
}, 1000);
});
document.querySelector("#x").addEventListener("click",
function () {
var this2= this;
setTimeout(function () {
console.log(this2.parentNode.id)
}, 2000);
});
document.querySelector("#x").addEventListener("click",
function () {
setTimeout(() => {
console.log(this.parentNode.id)
}, 3000);
});
|
Observa cómo en el tercer caso salvaguardar el this
externo en la variable local this2
de la función externa garantiza una clausura adecuada entre la función interna y una copia de la referencia a this
.
Una representación gráfica de la disposición en memoria de los diferentes objetos resultantes del código anterior es la siguiente:
Representación en memoria de los objetos creados en el código de la actividad.¶
JavaScript tiene una sintaxis alternativa ligeramente más sencilla para crear objetos. Esta segunda forma se basa en definir una función constructora y utilizar la palabra reservada new
:
1 2 3 4 5 6 7 8 | function Okapi (edad) {
this.edad= edad;
}
Okapi.prototype.charla= function () {
console.log("¿Qué tal? Tengo "+this.edad+" años.");
}
let o4= new Okapi(8);
o4.charla();
|
Importante
El operador new
hace aquí muchas cosas:
crea un nuevo objeto;
asigna como prototipo de este nuevo objeto (es decir, como valor de la propiedad interna e inacesible
[[Prototype]]
, la misma a la que podemos acceder conObject.getPrototypeOf()
o en algunos navegadores con__proto__
) el mismo objeto que el referenciado por la propiedad externa y accesible de nombreprototype
del constructor (en este caso,Okapi.prototype
, objeto al que hemos añadido la funcióncharla
);hace que
this
apunte al nuevo objeto;ejecuta la función constructora;
devuelve el objeto creado (que en este caso será asignado a la variable
o4
).
Cada vez que creamos una función, el objeto que la representa recibe un atributo prototype
, que es un objeto con un atributo constructor
que referencia a la misma función. Además, como cualquier objeto de JavaScript, el objeto de la función tiene un prototipo interno que inicialmente se basa en el prototipo de la clase predefinida Object
, esto es, Object.prototype
, que contiene una serie de métodos básicos como toString
. Cuando sobre el objeto creado (o4
en nuestro caso) invocamos una función, esta se busca en primer lugar en los atributos del objeto; si no se encuentra allí, se busca en su prototipo (allí el intérprete encontraría, por ejemplo, la función charla
); si no se encuentra allí, se busca en el prototipo de su prototipo (allí el intérprete se encontraría con la función toString
) y así sucesivamente hasta llegar al prototipo de Object
. Observa el paralelismo de este comportamiento con la herencia de lenguajes de programación como Java o C++, aunque su implementación es muy diferente.
Nota
En cualquier momento durante la ejecución del programa, podemos añadir nuevos métodos a un prototipo y todos los objetos vinculados a él (tanto los ya existentes como los nuevos que se creen) recibirán dinámicamente el nuevo método.
Más recientemente, JavaScript ha incorporado la palabra reservada class
que permite crear objetos y asociarles prototipos de una forma más parecida a como otros lenguajes definen clases y crean objetos. En realidad, esta otra manera de definir los objetos es lo que se conoce como azúcar sintáctica, porque no añade nuevas características al lenguaje, sino que el intérprete transforma la nueva notación a la clásica basada en prototipos que hemos visto:
1 2 3 4 5 6 7 8 9 10 11 12 | class OkapiBis {
constructor(edad) {
this.edad= edad;
}
charla() {
console.log("¿Qué tal? Tengo "+this.edad+" años.");
}
}
let o5= new OkapiBis(10);
o5.charla();
o5.toString();
|
Finalmente, encadenando prototipos podemos definir relaciones de herencia, lo que con la nueva notación se puede hacer con la palabra reservada extends
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | class Empleado {
constructor(nombre,apellidos) {...}
getNombreCompleto() {...};
}
class Director extends Empleado {
constructor(nombre,apellidos) {
super(nombre,apellidos);
this._empleados = [];
}
añadeEmpleado(empleado) {
this._empleados.push(empleado);
}
}
|
3.6. Ámbitos y clausuras¶
En JavaScript es habitual definir una función dentro de otra de forma que la función interna utilice variables definidas en el ámbito de la externa y sobreviva a ella, en el sentido de que la función interna acabe ejecutándose cuando la externa ya finalizó:
1 2 3 4 5 6 7 8 9 10 11 | function creaLista() {
var list = document.querySelector("#list");
for (let i = 1; i <= 5; i++) {
let item = document.createElement("li");
item.appendChild(document.createTextNode("Elemento " + i));
item.addEventListener("click", function (e) {
console.log("Se ha hecho clic en el elemento "+i+".");
}, false);
list.appendChild(item);
}
}
|
Para que este código funcione es necesario que las variables locales de la función externa no se destruyan al finalizar la ejecución de la función como ocurriría en principio con todas sus variables locales cuando son eliminadas de la pila. Una clausura de JavaScript es la combinación de una función (como la función manejadora de evento del código anterior) junto con las variables locales del ámbito exterior que utiliza (la variable i
en este caso). Una clausura se implementa como un registro que almacena una función y un entorno de la pila.
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 (con el permiso de los prototipos, claro). 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 convar
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 conlet
dentro de un bucle, se reserva sitio en la pila para una variable distinta en cada iteración; el uso delet
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 hemos mencionado, como clausura);
Las declaraciones de funciones locales y globales también sufren el mecanismo de izado en sus ámbitos respectivos.
Hazlo tú ahora
Estudia el siguiente código y ejécutalo después de dedicar un rato a pensar qué valores imprime por la consola.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | 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();
|
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | 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();
|
Hazlo tú ahora
Las clausuras llevan fácilmente a confusión si no se entiende bien cómo funcionan. Intenta predecir la salida del siguiente código antes de ejecutarlo:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | function f () {
var funcs = [];
for (var i = 0; i < 3; i++) {
funcs[i] = function() {
console.log("My value: " + i);
};
funcs[i](); // Aquí se obtiene un resultado
}
return funcs;
}
var m= f();
for (var j = 0; j < 3; j++) {
m[j](); // Aquí otro diferente
}
|
Intenta predecir ahora la salida cuando las variables se declaran con let
en lugar de con var
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | function f () {
var funcs = [];
for (let i = 0; i < 3; i++) {
funcs[i] = function() {
console.log("My value: " + i);
};
funcs[i]();
}
return funcs;
}
var m= f();
for (var j = 0; j < 3; j++) {
m[j]();
}
|
Al usar let
se crea un nuevo enlace entre la función interior y la variable del bucle en cada iteración. Comprueba después si tus predicciones eran correctas ejecutando el código en un intérprete de JavaScript.
3.7. 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», «JavaScript for impatient programmers», «Eloquent JavaScript» o «Deep JavaScript: theory and techniques».