En aquest document donarem un breu repàs a alguns conceptes de JavaScript que utilitzarem al llarg del curs, i amb els quals convé que ens comencem a familiaritzar des de ja, si no els hem utilitzats encara. En concret, tractarem:
En JavaScript existixen diverses maneres de declarar variables, però no totes són igual de segures ni recomanables.
La forma clàssica, que encara trobaràs en molts tutorials i exemples obsolets en Internet, és amb la paraula reservada var
:
var nom = "Nacho";
var edat = 41;
Encara que funciona, var
té un problema important: el seu àmbit no es limita al bloc ({ … }) en el qual es declara, sinó a tota la funció o fins i tot al context global. Això pot provocar resultats inesperats:
if (2 > 1)
{
var nom = "Nacho";
console.log("Nom dins:", nom);
}
console.log("Nom fora:", nom); // "Nacho" <-- continua existint
En este exemple, la variable nom
continua disponible fora del if, quan el lògic seria que s’haguera “perdut” en eixir del bloc.
Per a evitar este comportament, es recomana utilitzar la paraula reservada let
, en lloc de var
, per a declarar variables:
if (2 > 1)
{
let nom = "Nacho";
console.log("Nom dins:", nom);
}
console.log("Nom fora:", nom); // Error: nom no està definit
D’esta manera, l’àmbit de cada variable queda restringit al bloc on es declara, evitant accessos accidentals des de fora i fent que el codi siga més predictible i fàcil de mantindre.
També que podem emprar la paraula const
per a definir constants en el codi. Això serà particularment útil tant per a definir constants convencionals (com un text o número fix, per exemple) com per a carregar llibreries, com veurem en sessions posteriors.
const pi = 3.1416;
És important destacar que const
impedix la reassignació de la variable, però no fa immutable el contingut d’un objecte o un array:
const persona = { nom: "Ana" };
persona.nom = "Laura"; // permés (modificació interna)
persona = {}; // error (no es pot reassignar la referència)
Les estructures de dades o objectes literals en JavaScript són col·leccions dinàmiques de parells propietat-valor, on:
Per exemple:
let persona = {
nom: "Maria",
edat: 41,
telefon: "666555444"
};
Podem accedir a una propietat usant el punt .
o la notació de claudàtors [ ]
.
let nom = persona.nom; // Maria
let edat = persona["edat"]; // 41
També és possible extraure propietats d’un objecte directament en variables o constants amb el mateix nom que la propietat de l’objecte (desestructuració):
const { nom, edat, telefon } = persona;
console.log(nom); // "Maria"
console.log(edat); // 41
console.log(telefon); // "666555444"
Objectes niats
Un objecte pot contindre com a valor altre objecte, formant estructures més complexes. En el següent exemple, el valor de la propietat direccio
és un nou objecte:
let persona = {
nom: "Maria",
edat: 41,
telefon: "666555444",
direccio: {
via: "Avinguda",
nom: "Miguel Hernández",
numero: 62
}
};
Per a accedir a propietats niades podem fer-ho de la forma tradicional:
let via = persona.direccio.via; // Avinguda
let numero = persona["direccio"]["numero"]; // 62
O bé, usant desestructuració niada:
const { direccio: { via, numero } } = persona;
console.log(via); // "Avinguda"
console.log(numero); // "62"
Ací, direccio
ha de coincidir amb la clau de l’objecte. No obstant això, en este exemple no es crea una variable anomenada direccio, només les internes via
i numero
.
En JavaScript podem definir funcions de diferents maneres. La forma més tradicional és utilitzant la paraula reservada function
, però existix una notació més compacta i molt usada hui en dia: les arrow functions (funcions fletxa o funcions lambda).
Les funcions tradicionals són definides amb la paraula reservada function
. Es coneixen com a declaracions de funció i tenen una característica especial: el motor de JavaScript les “eleva” a l’inici del programa (hoisting), de manera que es poden utilitzar fins i tot abans d’aparéixer en el codi.
Suposem aquesta funció tradicional que retorna la suma dels dos paràmetres que se li passen:
function sumar(num1, num2) {
return num1 + num2;
}
A l’hora d’utilitzar aquesta funció, n’hi ha prou amb cridar-la en el lloc desitjat, passant-li els paràmetres adequats. Per exemple:
console.log(sumar(3, 2)); // Mostrarà 5
Una altra manera de definir funcions és mitjançant funcions anònimes, dites així perquè no tenen un nom propi.Estes funcions es declaren “sobre la marxa”, i normalment s’assignen a una variable o constant per a poder utilitzar-les després a través d’eixa referència:
let sumarAnonim = function(num1, num2) {
return num1 + num2;
};
console.log(sumarAnonim(3, 2));
A diferència de les declaracions de funció tradicionals, estes no es “eleven” amb el hoisting, per la qual cosa només poden usar-se després d’haver sigut definides en el codi.
Les funciones fletxa (arrow functions) són una altra manera de definir funcions més compacta. S’empra una expressió lambda per a especificar els paràmetres d’una banda (entre parèntesis) i el codi de la funció per un altre entre claus, separats per una fletxa (=>
). Es prescindix de la paraula reservada function
per a definir-les.
La mateixa funció anterior, expressada com arrow function, quedaria així:
let sumar = (num1, num2) => {
return num1 + num2;
};
Igual que les funcions anònimes, es poden assignar a una variable o constant per a reutilitzar-les més avant, o bé escriure’s directament en el lloc on es necessiten.
Quan la funció només retorna un valor, es pot simplificar eliminant les claus i la paraula return
, quedant així:
let sumar = (num1, num2) => num1 + num2;
A més, si la funció té un únic paràmetre, es poden ometre els parèntesis.
Per exemple, esta funció retorna el doble del número que rep com a paràmetre:
let doble = num => 2 * num;
console.log(doble(3)); // Mostrarà 6
Com comentàvem abans, les arrow functions (igual que les funcions anònimes) poden escriure’s directament en el lloc on es necessiten. Això les fa especialment útils com a funcions de callback, per exemple en els mètodes de arrays.
Per exemple, donat el següent llistat de dades personals:
let dades = [
{nom: "Nacho", telefon: "966112233", edat: 41},
{nom: "Ana", telefon: "911223344", edat: 36},
{nom: "Mario", telefon: "611998877", edat: 15},
{nom: "Laura", telefon: "633663366", edat: 17}
];
Si volem filtrar les persones majors d’edat, podem fer-ho amb una funció anònima combinada amb la funció filter
:
let majorsEdat = dades.filter(function(persona) {
return persona.edat >= 18;
})
console.log(majorsEdat);
I també podem emprar una arrow function en el seu lloc:
let majorsEdat = dades.filter(persona => persona.edat >= 18);
console.log(majorsEdat);
Notar que, en aquests casos, no assignem la funció a una variable per a usar-la més tard, sinó que s’empren en el mateix punt on es defineixen. Notar també que el codi queda més compacte emprant una arrow function.
La diferència entre les arrow *functions i les funcions tradicionals o anònimes és que les primeres no permeten accedir directament a this
ni a l’objecte especial arguments
, que sí que estan disponibles en les funcions normals. Per tant, quan necessitem usar this
o arguments
, és més senzill optar per una funció tradicional o anònima.
this
depén de com s’anomene a la funció.this
apunta a eixe objecte.this
apunta al context global (window en navegador, undefined en mode estricte).this
no es redefinix sinó que hereta el valor del context on va ser creada. Per esta raó no és recomanable usar-les com a mètodes d’objectes, ja que no “veuen” a l’objecte com this
.arguments
; si es necessita accedir a tots els paràmetres, ha d’usar-se la sintaxi de paràmetres rest (…args), o recórrer a una funció normal o anònima.let persona = {
nom: "Ana",
saludar: function() {
console.log("Hola, soc " + this.nombre);
}
};
persona.saludar(); // "Hola, soc Ana" -> this apunta a l'objete persona
let salutacio = persona.saludar;
salutacio(); // "Hola, soc undefined" -> this ja no apunta a persona, sinó al context global
Amb arrow function, this
hereta de l’exterior (no l’objecte)
let persona2 = {
nom: "Ana",
/* Definim "saludar" com arrow function. Això fa que no compartisca
el context de l'objecte en què està (hereta this de l'exterior, no del propi objecte) */
saludar: () => {
console.log("Hola, soc " + this.nom);
}
};
persona2.saludar(); // "Hola, soc undefined" -> this no apunta a persona2, sinó al context exterior.
En canvi, les arrow functions són molt útils quan volem mantindre el this
d’un mètode en funcions internes, com en callbacks:
let persona3 = {
nom: "Ana",
saludar: function() {
setTimeout(() => {
console.log("Hola, soc " + this.nom);
}, 500);
}
};
persona3.saludar(); // Després de 0.*5s: "Hola, soc Ana"
En l’exemple anterior, la arrow function hereta this
del cos de la funció saludar, que el seu this
és persona3.
En resum, usa funcions tradicionals per a definir mètodes d’objectes que necessiten el seu propi this
, i arrow functions per a callbacks o funcions internes on es necessite mantindre el this
exterior.
Exercici 1:
Crea una carpeta anomenada “ArrowFunctions” en el teu espai de treball, en la carpeta de “Exercicis”. Crea un arxiu font dins anomenat
arrow_functions.js
amb el següent codi:let dades = [ {nom: "Nacho", telefon: "966112233", edat: 41}, {nom: "Ana", telefon: "911223344", edat: 36}, {nom: "Mario", telefon: "611998877", edat: 15}, {nom: "Laura", telefon: "633663366", edat: 17} ]; novaPersona({nom: "Juan", telefon:"965661564", edat: 60}); novaPersona({nom: "Rodolfo", telefon:"910011001", edat: 20}); esborrarPersona("910011001"); console.log(dades);
Hem definit un vector amb dades de persones, i un programa principal que anomena dues vegades a una funció
novaPersona
, passant-li com a paràmetres els objectes amb les dades de les persones a afegir. Després, cridem a una funcióesborrarPersona
, passant-li com a paràmetre un número de telèfon, i vam mostrar el vector de persones amb les dades que hi haja.Has d’implementar les funcions
novaPersona
iesborrarPersona
perquè facen la seua comesa. La primera rebrà la persona com a paràmetre i, si el telèfon no existeix en el vector de persones, l’afegirà. Per a això, pots utilitzar el mètodepush
del vector:dades.push(persona);
Quant a
esborrarPersona
, eliminarà del vector a la persona que tinga aquest telèfon, en cas que existisca. Per a eliminar a la persona del vector, pots simplement filtrar les persones el telèfon de les quals no siga l’indicat, i assignar el resultat al propi vector de persones:dades = dades.filter(persona => persona.telefon != telefonABuscar);
En programació existeixen dues grans maneres d’invocar o cridar a les funcions:
Un dels pilars en els quals se sustenta la programació asíncrona en JavaScript ho conformen els callbacks. Un callback és una funció Al fet que es passa com a paràmetre a una altra B, i que serà anomenada en algun moment durant l’execució de B (normalment quan B finalitza la seua tasca). Aquest concepte és fonamental per a dotar a Node.js (i a JavaScript en general) d’un comportament asíncron: es diu a una funció, i se li deixa indicat el que ha de fer quan acabe, i mentrestant el programa pot dedicar-se a altres coses.
Un exemple el tenim amb la funció setTimeout
de JavaScript. A aquesta funció li podem indicar una funció a la qual anomenar, i un temps (en mil·lisegons) que esperar abans de cridar-la. Executada la línia de l’anomenada a setTimeout
, el programa segueix el seu curs i quan el temps expira, es diu a la funció callback indicada.
Provem d’escriure aquest exemple en un arxiu anomenat callback.js
en nostra subcarpeta “ProjectesNode/Proves/ProvesSimples”:
setTimeout(function() {console.log("Finalitzat callback");}, 2000);
console.log("Hola");
Si executem l’exemple, veurem que el primer missatge que apareix és el de “Hola”, i passats dos segons, apareix el missatge de “Finalitzat callback”. És a dir, hem anomenat a setTimeout
i el programa ha seguit el seu curs després, ha escrit “Hola” per pantalla i, una vegada ha passat el temps estipulat, s’ha anomenat al callback per a fer el seu treball.
Utilitzarem callbacks àmpliament durant aquest curs. De manera especial per a processar el resultat d’algunes promeses que emprarem (ara veurem què són les promeses), o el tractament d’algunes peticions de serveis.
Les promeses són un altre mecanisme important per a dotar d’asincronia a JavaScript. S’empren per a definir la finalització (reeixida o no) d’una operació asíncrona. En el nostre codi, podem definir promeses per a realitzar operacions asíncrones, o bé (més habitual) utilitzar les promeses definides per uns altres en l’ús de les seues llibreries.
Al llarg d’aquest curs utilitzarem promeses per a, per exemple, enviar operacions a una base de dades i recollir el resultat de les mateixes quan finalitzen, sense bloquejar el programa principal. Però per a entendre millor què és el que farem, arribat el moment, convé tindre clara l’estructura d’una promesa i les possibles respostes que ofereix.
En el cas que vulguem o necessitem crear una promesa, es crearà un objecte de tipus Promise
. A aquest objecte se li passa com a paràmetre una funció amb dos paràmetres:
Aquests dos paràmetres se solen cridar, respectivament, resolve
i reject
. Per tant, un esquelet bàsic de promesa, emprant arrow functions per a definir la funció a executar, seria així:
let nombVariable = new Promise((resolve, reject) => {
// Codi a executar
// Si tot va bé, cridem a "resolve"
// Si alguna cosa falla, cridem a "reject"
});
Internament, la funció farà el seu treball i cridarà als seus dos paràmetres en l’un o l’altre cas. En el cas de resolve
, se li sol passar com a paràmetre el resultat de l’operació, i en el cas de reject
se li sol passar l’error produït.
Vegem-ho amb un exemple. La següent promesa busca els majors d’edat de la llista de persones vista en un exemple anterior. Si es troben resultats, es retornen amb la funció resolve
. En cas contrari, es genera un error que s’envia amb reject
. Còpia l’exemple en un arxiu anomenat prova_promesa.js
en la carpeta “ProjectesNode/Proves/ProvesSimples” del teu espai de treball:
let dades = [
{nom: "Nacho", telefon: "966112233", edat: 41},
{nom: "Ana", telefon: "911223344", edat: 36},
{nom: "Mario", telefon: "611998877", edat: 15},
{nom: "Laura", telefon: "633663366", edat: 17}
];
let promesaMajorsEdat = new Promise((resolve, reject) => {
let resultat = dades.filter(persona => persona.edat >= 18);
if (resultat.length > 0)
resolve(resultat);
else
reject("No hi ha resultats");
});
La funció que defineix la promesa també es podria definir d’aquesta altra forma:
let promesaMajorsEdat = llistat => {
return new Promise((resolve, reject) => {
let resultat = llistat.filter(persona => persona.edat >= 18);
if (resultat.length > 0)
resolve(resultat);
else
reject("No hi ha resultats");
});
};
Així no fem ús de variables globals, i l’array queda passat com a paràmetre a la pròpia funció, que retorna l’objecte Promise
una vegada concloga. Deixa definida la promesa d’aquesta segona forma en l’arxiu font de prova.
En el cas de voler utilitzar una promesa prèviament definida (o creada per uns altres en alguna llibreria), simplement cridarem a la funció o objecte que desencadena la promesa, i recollim el resultat. En aquest cas:
resolve
) emprem la clàusula then
.reject
) emprem la clàusula catch
.Així, la promesa anterior es pot emprar d’aquesta manera (novament, emprem arrow functions per a processar la clàusula then
amb el seu resultat, o el catch
amb el seu error):
promesaMajorsEdat(dades).then(resultat => {
// Si entrem ací, la promesa s'ha processat bé
// En "resultat" podem accedir al resultat obtingut
console.log("Coincidències trobades:");
console.log(resultat);
}).catch(error => {
// Si entrem ací, hi ha hagut un error en processar la promesa
// En "error" el podem consultar
console.log("Error:", error);
});
Còpia aquest codi sota el codi anterior en l’arxiu prova_promesa.js
creat anteriorment, per a comprovar el funcionament i el que mostra la promesa.
Notar que, en definir la promesa, es defineix també l’estructura que tindrà el resultat o l’error. En aquest cas, el resultat és un vector de persones coincidents amb els criteris de cerca, i l’error és una cadena de text. Però poden ser el tipus de dada que vulguem.
Exercici 2:
Crea una carpeta anomenada “Promeses” en el teu espai de treball, en la carpeta de “Exercicis”. Crea dins un arxiu font anomenat
promeses.js
, que siga una còpia de l’arxiu fontarrow_functions.js
de l’exercici anterior.El que faràs en aquest exercici és adaptar les dues funcions
novaPersona
iesborrarPersona
perquè retornen una promesa.En el cas de
novaPersona
, es retornarà ambresolve
l’objecte persona inserit, si la inserció va ser satisfactòria, o ambreject
el missatge “Error: el telèfon ja existeix” si no es va poder inserir la persona perquè ja existia el seu telèfon en el vectorEn el cas d’
esborrarPersona
, es retornarà ambresolve
l’objecte persona eliminat, si l’esborrat va ser satisfactori, o ambreject
un missatge “Error: no es van trobar coincidències” si no existia cap persona amb aqueix telèfon en el vector.Modifica el codi del programa principal perquè intente afegir una persona correcta i una altra equivocada (telèfon ja existent en el vector), i esborrar una persona correcta i una altra equivocada (telèfon no existent en el vector). Comprova que el resultat en executar és el que esperaves.
Des de ES8 es té disponible una nova manera de treballar amb crides asíncrones, a través de l’especificació async/await. És una forma més còmoda de cridar a funcions asíncrones i recollir el seu resultat abans de cridar a una altra, sense necessitat d’anar niant clàusules then
per a enllaçar el resultat d’una promesa amb la següent.
No entrarem en els detalls sobre com utilitzar-la de moment. Ho farem més endavant, quan estiguem familiaritzats amb les promeses.Però, perquè puguem fer-nos una idea del que implica, reescriurem un exemple anterior fet amb promeses usant aquesta especificació. Partim del mateix vector de persones:
let dades = [
{nom: "Nacho", telefon: "966112233", edat: 41},
{nom: "Ana", telefon: "911223344", edat: 36},
{nom: "Mario", telefon: "611998877", edat: 15},
{nom: "Laura", telefon: "633663366", edat: 17}
];
Construïm ara la nostra funció per a buscar persones majors d’edat. És similar a l’anterior, però li afegim la partícula async
per a indicar que és una funció asíncrona. Això fa que la funció retorne sempre una promesa.
let promesaMajorsDeEdat = async llistat => {
return new Promise((resolve, reject) => {
let resultat = llistat.filter(persona => persona.edat >= 18);
if (resultat.length > 0)
resolve(resultat);
else
reject("No hi ha resultats");
});
};
NOTA: en realitat, en aquest cas no fa falta afegir la partícula
async
perquè la funció, en retornar una promesa, ja és automàticament asíncrona. Però es pot seguir aquest costum en programar.
A l’hora d’invocar a aquesta funció podem fer-ho de la mateixa manera que abans (amb then/catch) o usant la partícula await
. Aquesta partícula fa que el codi del programa s’espere que la funció finalitze per a després continuar:
let adults = await promesaMajorsDeEdat(dades);
// En arribar ací ja tenim el llistat
console.log(adults);
No obstant això, un dels requisits que estableix l’especificació és que no podem utilitzar la clàusula await
fora d’un bloc asíncron. Per tant, és habitual definir una funció asíncrona que invoque a la resta, i cridar a aquesta des del programa principal:
let promesaMajorsDeEdat = async llistat => {
return new Promise((resolve, reject) => {
let resultat = llistat.filter(persona => persona.edat >= 18);
if (resultat.length > 0)
resolve(resultat);
else
reject("No hi ha resultats");
});
};
async function principal()
{
let adults = await promesaMajorsDeEdat(dades);
console.log(adults);
}
En el cas que la invocació siga reeixida es retornarà el llistat, que recollim en la variable adults
. Però, què ocorre si alguna cosa falla? En aquest cas podem utilitzar un bloc try..catch
per a capturar l’excepció i mostrar el missatge d’error que es produïsca. A més, podem enllaçar aquests blocs un darrere l’altre per a assegurar-nos que una cosa s’execute quan acabe l’anterior.
async function principal()
{
try
{
let adults = await promesaMajorsDeEdat(dades);
console.log("Resultats:", adults);
} catch(e) {
// Error
console.log(e);
}
// Una altra crida sincronitzada...
try
{
let variable = await ...;
} catch(e) {
console.log(e);
}
}