Node.js

Conceptes previs de JavaScript

  

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:

1. Variables i estructures de dades

1.1. Declaració de variables i constants

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)

1.2. Estructures heterogènies de dades

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.

2. Funcions y arrow functions

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).

2.1. Les funcions tradicionals

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

2.2. Les funcions anònimes

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.

2.3. Les arrow functions

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

2.3.1. Ús directe d’arrow functions

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.

2.4. Arrow functions i funcions tradicionals

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.

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, thishereta 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 i esborrarPersona 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ètode push 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);

3. Programació asíncrona

En programació existeixen dues grans maneres d’invocar o cridar a les funcions:

3.1. Els callbacks

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.

3.2. Les promeses

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.

3.2.1. Crear una promesa. Elements a tindre en compte

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.

3.2.2. Consum de promeses

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:

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 font arrow_functions.js de l’exercici anterior.

El que faràs en aquest exercici és adaptar les dues funcions novaPersona i esborrarPersona perquè retornen una promesa.

En el cas de novaPersona, es retornarà amb resolve l’objecte persona inserit, si la inserció va ser satisfactòria, o amb reject 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 vector

En el cas d’esborrarPersona, es retornarà amb resolve l’objecte persona eliminat, si l’esborrat va ser satisfactori, o amb reject 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.

3.2.3. L’especificació async/await

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);
    }
}