Link Cerca Menu Expand Document

Javascript modern

Javascript bàsic

Javascript és un llenguatge d’alt nivell que utilitza compilació just-in-time, o sigui, es compila durant la seva execució.

Javascript va començar com un llenguatge que permetia petites interaccions a les pàgines web, però esdevé important a partir de 2015 (versió ES6), quan comencen a fer-se revisions anuals de l’estàndard. ECMAScript (o ECMA-262) és el standard que fa interoperable Javascript als navegadors i al servidor (nodejs).

A la versió 2009 (ES5) es va introduir strict mode. Fins llavors, diem que utilitzàvem Javascript tradicional. El desenvolupament modern es fa exclusivament utilitzant aquest mode, ja que permet evitar molts bugs en ser més estricte: elimina errors silenciosos i prohibeix algunes sintaxis. Està activat per defecte en alguns casos (mòduls i objectes), però pot activar-se amb ‘use strict’ com a primera línia de codi d’un arxiu.

Variables

Javascript és un llenguatge de tipatge dinàmic: el tipus està associat al valor, no a la variable. Es pot canviar el valor d’una variable, i per tant el seu tipus.

A JS tradicional s’utilitza var per declarar variables. No cal inicialitzar, i llavors la variable té el valor undefined. La variable té l’àmbit de la funció on es declara.

A JS modern s’utilitzen let i const per a declarar variables i constants. La variable té l’àmbit del bloc on es declara.

Primitius

Els tipus de Javascript són els primitius i els objectes.

Els primitius essencials són:

  • boolean: true o false.
  • null: una variable assignada intencionalment a res.
  • undefined: una variable mai assignada.
  • numèrics: números de doble precisió. NaN és un valor numèric no representable.
  • strings.

Podem veure el tipus del valor d’una variable amb l’operador typeof.

Els tipus primitius són immutables: no es poden modificar un cop creats. En canvi, els objectes (inclosos els arrays) són mutables. També se’ls diu tipus referència. Quan es comparen, són iguals si fan referència al mateix objecte. I quan s’assignen a una variable estem fent una nova referència al mateix objecte. Les còpies d’objectes poden ser superficials (referència) o profundes.

Conversions i operadors

Conversions:

  • Podem fer conversions amb Boolean(valor), Number(valor) i String(valor)
  • A número: undefined és NaN, false i null és 0, true és 1
  • string a número: si buit, 0, si no, s’intenta llegir (NaN en cas d’error)
  • A boolean: son false els valors 0, string buit, null, undefined i NaN (falsy values) i la resta true (truthy values)

Operadors:

  • Els operador matemàtics són + - * / % i **
  • També hi ha l’operador concatenació per a strings +
  • L’operador + unari equival a Number(valor)
  • L’assignació = és també un operador, i retorna el valor de l’expressió
  • Operadors modify-in-place per a tots els operadors aritmètics += -= ...
  • Increment/decrement ++ --
  • Operadors de bits & | ^ ~ << >> >>>
  • Operador coma retorna l’últim valor avaluat
  • Operador condicional ? condition? value1 : value2 retorna value1 si la condició és true
  • Operadors lògics || && ! ?? (nulllish coaslescing)
  • || troba el primer valor truthy
  • && troba el primer valor falsy
  • ?? retorna el primer valor definit (diferent de null i undefined)

Comparacions:

  • Els operadors habituals > < >= <= != == (equals) === (strict equals)
  • “Strict equals” no fa conversió per a comparar

Objectes

Els objectes són col.leccions de propietats accessibles mitjançant claus. Les propietats poden tenir qualsevol tipus. És una organització de tipus mapa.

Els objectes es poden crear amb la notació literal { clau1: valor1, clau2: valor2...}.

Alguns objectes existents al llenguatge:

  • Dates: representació de dates.
  • Arrays: propietats accessibles amb un índex.
  • Basats en claus: Maps, Sets.

La paradoxa dels primitius string, number i boolean és que també permeten mètodes utilitzant embolcalls anomenats String, Number i Boolean. Només es creen quan es criden els mètodes.

Els strings tenen aquests mètodes essencials: trim(), includes(), indexOf(), toUpperCase(), toLowerCase(), replace(), slice(), split(), repeat(), match(), charAt(), charCodeAt().

Quant als números: parseInt(), toString(), toExponential(), toFixed(), toPrecision(), valueOf(), toLocaleString(), parseFloat(), isInteger().

Els arrays són un tipus d’objecte que poden canviar de mida i contenir diferents tipus de dades. Estan indexats per sencers i permeten diverses operacions:

  • push(e): afegeix un element al final de l’array.
  • shift() i pop(): extreu i retorna el primer / l’últim element de l’array, undefined si està buit.
  • concat(arr1, ...): crea un nou array amb els continguts de dos o més arrays.
  • slice(start, end): crea una còpia superficial d’una part d’un array (end, primer índex a excloure, és opcional).
  • splice(start, deleteCount, item1, ...): canvia el contingut d’un array esborrant o substituint.

Operacions iteratives:

  • map(fn(e){...}): crea un nou array amb els valors retornats per la funció.
  • find((testFn(e){...}): retorna el primer element que satisfà la funció de prova.
  • findIndex(testFn(e){...}): retorna l’índex del primer element que satisfà la funció de prova.
  • filter(testFn(e){...}): crea una còpia superficial del array amb els elements que satisfan la funció de prova.
  • reduce(reducerFn(prev,e){}, initial): retorna un valor únic iterant pels elements i amb un valor previ. El primer previ és initial.

Iteració

Podem iterar sobre els arrays i els objectes.

  • array.forEach(fn(item, index)) per a iterar sobre arrays.
  • for...of obté els valors per a iterables, com els arrays, strings, Map, Set i NodeList.
  • for...in per a propietats enumerables d’objectes.
  • Object.keys(obj) i Object.values() retornen arrays amb propietats i valors, respectivament.

Hoisting

El hoisting és un procés de l’intèrpret JS on sembla moure la declaració de variables, funcions i classes a dalt del seu àmbit abans d’executar el codi. Això permet, per exemple, utilitzar funcions abans de declarar-les. També es pot fer amb declaracions de variables i classes, però en general no es recomana.

Funcions

Les funcions són objectes que poden ser cridats. La declaració d’una funció té aquest aspecte:

function name(parameter1, parameter2, parameter3 = "some default value", ...) {
 // body
}

Els paràmetres es comporten com variables arguments. Quan cridem la funció li passem arguments. Els arguments no definits en la crida tenen el valor undefined. Els paràmetres per defecte s’avaluen només si no s’indica l’argument corresponent.

El valor de retorn d’una funció que no retorna res és undefined.

Les variables locals són visibles només dins la funció. Les variables externes (o globals) també són accessibles. Si es diuen igual que alguna local, llavors perden l’accessibilitat.

Expressions de funcions

Les funcions admeten expressions. Per exemple, dirHola és una expressió de funció:

let dirHola = function(nom) {
  console.log(`hola, ${nom}`);
};

dirHola('Joan');

function fesCrida(laMevaFuncio, nom) {
  laMevaFuncio(nom);
}

fesCrida(dirHola, 'Anna');

La crida dins de fesCrida es fa exactament a la mateixa funció dirHola. El paràmetre laMevaFuncio té un valor de tipus funció i s’anomena callback, ja que permet cridar de tornada un codi.

Les declaracions de funcions es poden cridar abans de ser creades (hoisting), i només són visibles al block a que pertanyen. Les expressions de funcions només es poden cridar un cop assignades.

Funcions fletxa

Les funcions fletxa són una alternativa a les declaracions de funció que hem vist.

let func1 = (arg1, arg2, ...) => expression;
let func2 = (arg1, arg2, ...) => sentence;
let func3 = (arg1, arg2, ...) => {
  // code with optional return sentence
}

Aquí func1 és una funció que retorna una expressió. Per exemple:

let suma = (a, b) => a + b;
suma(1,2);

En canvi func2 podria no retornar res, per exemple:

let logme = (a) => console.log(a);
logme(somevar);

Finalment func3 respon a la necessitat d’una funció multilínia.

Rest & Spread

Quan volem passar un nombre variable d’arguments podem utilitzar la sintaxi “resta de paràmetres”, que sempre va al final dels paràmetres d’una funció:

function someFunc(arg1, arg2, ...args) {
  // ... aquí args és un array: args[0], args[1]...
}

L’acció inversa ens permetria passar un array com a paràmetres individuals. Per exemple, si volem utilitzar Math.max(arg1, arg2, ... argN) podem fer-ho així:

let arr = [1, 4, 9];
console.log(Math.max(...arr));

La sintaxi “spread” permet fer còpies d’arrays i d’objectes. Per exemple:

let arr = [1, 4, 9];
let arrCopy = [...arr];
let obj = { a: 1, b: 4, c: 9 }
let objCopy = {...obj};
let ObjCopyChanged = {...obj, a: 3} // a canvia a 3 (després de copiar 1)

Execution contexts i event loop

Browser Runtime Environment

Les crides al Web API fan que hi hagi callbacks que s’hagin de cridar en certs moments (esdeveniments). Quan és així, la Web API afegeix aquestes crides a una cua de callbacks. L’event loop comprovarà quan està buit el stack per anar traient les callbacks i executant-les. En resum, JavaScript s’executa en un sol fil, i les callbacks ho fan quan el stack està buit.

Orientació a objecte

Un objecte es pot crear amb un inicialitzador:

const name = 'John';
const person = {
  name: name,
  getName: function() {
    return this.name;
  }
};

Una classe és una plantilla per a crear un objecte. Utilitzen internament funcions constructors i prototips.

class Person {
  constructor (name) {
    this.name = name;
  }
  getName() {
    return this.name;
  }
}
const person = new Person('Ann');
console.log(person.getName());

Aquesta seria l’alternativa amb funció constructor i prototips:

function Person(name) {
  this.name = name;
}
Person.prototype.getName = function() {
  return this.name;
};

Podem estendre una classe amb:

class JohnPerson extends Person {
  constructor() {
    super('John');
  }
}

this

Només ens referirem al mode estricte.

  • En el context global, this és undefined.
  • En el context d’una funció, depèn de com es va cridar la funció:
    1. Cap valor, quan s’executa una funció aïllada.
    2. Al constructor d’una classe, la nova instància.
    3. A un mètode objecte.metode(), l’objecte que el crida.
    4. A una funció fletxa, el this del context pare.

Per al primer cas (1), hi ha una forma d’associar una funció a una instància: utilitzant el mètode bind(instància) de la funció es retornarà una nova funció que pot cridar-se aïlladament i on this és la instància.

Aquí, la crida a boundMyFunc assignarà this = myObj dins la meva funció:


function myFunc() { 
  // ...
}
let boundMyFunc = myFunc.bind(myObj);
boundMyFunc();

JSON

JSON és una representació en cadena (string) d’una estructura de dades que permet intercanviar-les fàcilment. La representació arrel pot ser un objecte {} o bé un array [], que poden contenir altres objectes, altres arrays o bé els tipus essencials: string, number, true/false o null.

JavaScript té dos mètodes que permeten convertir un objecte o array a un string JSON i també l’operació inversa:

  • JSON.stringify(valor)
  • JSON.parse(string)

A diferència dels literals JavaScript, la representació JSON sempre utilitza dobles cometes per als strings. A més, totes les propietats també han de tenir dobles cometes.

JSON.stringify no converteix qualsevol valor. Per exemple, no ho fa amb funcions o altres objectes, com Map. De vegades, cal implementar el segon i tercer paràmetres opcionals, replacer and reviver, per soportar certs tipus.

Javascript asíncron

Callbacks

L’API de JavaScript al navegador permet programar accions asíncrones. Per exemple, coses a fer quan hi ha un esdeveniment, o temporitzadors que executen altres accions. Per realitzar-les s’utilitzen callbacks, o sigui, valors de tipus funció.

Per exemple, setTimeout(callback, milliseconds) permet cridar una funció després d’un cert temps. Aquesta callback pot ser, per exemple, una funció fletxa:

setTimeout(() => {
  console.log("ha passat 1 segon");
}, "1000")

Si volem executar una callback dins d’una altra es pot produir la “piràmide de la perdició”: una sèrie de funcions a dins d’altres funcions imbricades que fan el codi poc llegible. Per això van aparèixer les promeses.

Promises

El codi asíncron té habitualment dues parts:

  • Un codi de producció que triga un cert temps a executar-se.
  • Un codi de consumició que necessitarà allò produït un cop estigui llest.

Productor

Una promesa ajunta aquestes dues parts.

let promesa = new Promise(function(resolve, reject) {
  // codi de producció
});

El codi de producció ha de fer una d’aquestes dues crides un cop acabi:

  • resolve(value) per indicar que tot va anar bé i el resultat és value.
  • reject(error) per indicar que no va anar bé i l’error és error.

Per exemple, una promesa que acaba quan passa 1 segon amb el resultat “fet”:

let promise = new Promise(function(resolve, reject) {
  setTimeout(() => resolve("fet"), 1000);
  // error podria ser: reject(new Error("hi ha un error"))
});

Consumidor

Podem utilitzar el mètode then d’una promesa per a consumir-la:

promise.then(
  function(result) { /* consumeix el resultat correcte */ },
  function(error) { /* consumeix l'error */ }
);
// per exemple, per cridar alert amb el resultat (callback error no utilitzada):
promise.then(alert);
// es poden encadenar then, catch i finally:
promise.then(consumidorResultat).catch(consumidorError).finally(cridaSempre);

Si el consumidor del resultat retorna una promesa, es poden encadenar thens:

promise.then(value => {
  return value.anotherPromise();
}).then(anotherValue => {
  // use anotherValue
});

Si el que es retorna no és una promesa, l’API la converteix automàticament a promesa que resol al valor retornat:

const promise1 = new Promise((resolve, reject) => resolve(1));
promise1.then(v1 => v1 + 1).then(v2 => v2 *2).then(v3 => console.log(v3));

Es poden afegir diversos consumidors a un productor fent thens a la mateixa promesa:

promise.then(consumidorResultat1);
promise.then(consumidorResultat2);

Async/Await

Les promeses es poden utilitzar amb una sintaxi alternativa més amigable. Per exemple, per dir que una funció retorna una promesa només cal prefixar-la amb async:

async function myAsyncFunc() {
  return 1; // el mateix que Promise.resolve(1)
}
myAsyncFunc().then(alert); // mostra 1

Compte, perquè myAsyncFunc s’executarà de forma asíncrona ja que retorna una promesa. Per tant, si fem:

async function myAsyncFunc() {
  return new Promise((resolve, reject) => {
    setTimeout(() => resolve("done!"), 3000)
  });
}
myAsyncFunc(); // retorna la promesa, que no utilitzem
console.log('crida feta!');

primer es mostrarà “crida feta!” i als tres segons es resoldrà la promesa, que ningú espera (no hi ha then).

Per altra banda, await espera fins que una promesa es resol i retorna el seu resultat. Només es pot utilitzar dins d’una funció prefixada amb async (també a mòduls):

async function myAsyncFunc() {
  const promise = new Promise((resolve, reject) => {
    setTimeout(() => resolve("done!"), 3000)
  });
  const value = await promise;
  return "well " + value;
}
myAsyncFunc().then(value => alert(value));

Modern things

Modules

Els mòduls a JavaScript són una forma de trossejar el codi per a trossejar-lo i fer-lo més fàcil de gestionar. Inicialment, el patró IIFE permetia crear mòduls. Altres frameworks han implementat aquest patró, com per exemple CommonJS en el cas de NodeJS. A partir d’ES6 tenim una implementació a la especificació.

Per crear un mòdul cal exportar des del mòdul A i importar-lo des del mòdul B. Hi ha dos tipus d’exports:

  • default: export default ...;. S’importen amb import someNameOfYourChoice from './path/to/file.js';.
  • named: export const someData = ...;. S’importen amb import { someData } from './path/to/file.js';.

Un arxiu només pot contenir un mòdul default i un nombre il.limitat d’anomenats. També es poden importar els anomenats amb import * as upToYou from './path/to/file.js'; i llavors pots accedir als elements exportats com upToYou.someData.

Template literals

També anomenada “string interpolation”, una cadena amb backticks permet incloure el resultat d’expressions:

let cadena = `bon dia, ${name}!`;

Shorthand syntax

La declaració shorthand permet assignar propietats d’un objecte on la clau i el valor tenen el mateix identificador, evitant haver de repetir ident: ident.

const name = 'Pere'
const age = 20
const location = 'Sabadell'  
const user = {
    name,      
    age,
    location
}

Computed property names

Podem avaluar els noms de les propietats d’un objecte de forma dinàmica. Per exemple:

const obj = {
  [someExpression]: value
}

Array & Object destructuring

El “destructuring” permet desempaquetar arrays i objectes en variables.

let arr = ["John", "Smith"];
let [firstName, surname] = arr;

A la dreta pot haver qualsevol dada iterable. A l’esquerra pot haver qualsevol assignable, per exemple:

let user = {};
[user.name, user.surname] = "John Smith".split(' ');

Si l’array és més llarg que la llista a l’esquerra, els ítems extra s’ignoren. Si l’array és més curt, els ítems de l’esquerra seran undefined. També podem ignorar ítems de la dreta escrivint comes seguides. I també podem utilitzar spread per a recollir la resta:

let [name1, name2, ...rest] = ["Rosa", "Joan", "Anna", "Pere"];
[user.name, user.surname] = "John Smith".split(' ');

Amb els objectes també ho podem fer:

let obj = {var1: "valor1", var2: "valor2"};
let {var1, var2} = obj;
// no hi ha ordre a les propietats!

Podem reanomenar una propietat i utilitzar valors per defecte. Seguint l’exemple anterior:

let obj = {var1: "valor1", var2: "valor2"};
let {var1: valor1, var2: valor2, var3 = "valor3"} = obj;
// "valor3" podria ser qualsevol expressió

Podem només extreure la part que ens interessa:

let obj = {var1: "valor1", var2: "valor2"};
let {var1} = obj;

Podem utilitzar la sintaxi “resta”:

let obj = {var1: "valor1", var2: "valor2", var3: "valor3"};
let {var1, ...rest} = obj;
// rest serà un objecte amb dos propietats, var2 i var3

Podem fer destructuring anidat:

let obj1 = {
  var1: {
    opt1: 1,
    opt2: 2
  },
  var2: "valor2",
  var3: ["first", "second", "third"]
};
let {
  var1: { opt1, opt2 },
  var3: [first, ...rest] } = obj1;

Podem utilitzar-ho per a passar paràmetres de funcions:

let obj = {var1: "valor1", var2: "valor2", var3: "valor3"};
function myFunc1({var1, var2}) {
  // ...
}
myFunc1(obj);
let arr = ["first", "second"];
function myFunc2([var1, var2]) {
  // ...
}
myFunc2(arr);

Referències