JavaScript modern
- Javascript bàsic
- Funcions
- Execution contexts i event loop
- Orientació a objecte
- Javascript asíncron
- Modern things
- Referències
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
ofalse
.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.
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)
iString(valor)
- A número:
undefined
ésNaN
,false
inull
és0
,true
és1
string
a número: si buit,0
, si no, s'intenta llegir (NaN
en cas d'error)- A
boolean
: sonfalse
els valors0
,string
buit, els nullish (null
iundefined
) iNaN
(falsy values) i la restatrue
(truthy values)
Operadors:
- Els operador matemàtics són
+ - * / % i **
- També hi ha l'operador concatenació per a strings
+
- L'operador
+
unari equival aNumber(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ó éstrue
||
troba el primer valor truthy&&
troba el primer valor falsy??
retorna el primer valor definit (nulllish coaslescing)?.
retornaundefined
si la propietat o mètode a continuació és nullish (optional chaining)??=
només assigna a si l'operand a l'esquerra és nullish (nullish coalescing assignment)
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()
ipop()
: 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 primerprevi
ésinitial
.
Comprovacions de tipus
L'operador typeof s'utilitza per determinar el tipus primitiu d'una variable. Retorna una cadena amb el nom del tipus, com ara string
, number
, boolean
, object
, undefined
, function
, or symbol
.
console.log(typeof "hello"); // "string"
console.log(typeof 42); // "number"
console.log(typeof true); // "boolean"
console.log(typeof {}); // "object"
console.log(typeof undefined); // "undefined"
console.log(typeof function(){}); // "function"
L'operador instanceof comprova si un objecte és una instància d'una classe o funció constructora específica, retornant vertader o fals. És especialment útil per comprovar classes personalitzades, instàncies d'arrays i objectes de llibreria.
class Dog {}
const myDog = new Dog();
console.log(myDog instanceof Dog); // true
console.log([] instanceof Array); // true
console.log([] instanceof Object); // true (arrays are objects in JavaScript)
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)
iObject.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
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ó:
- Cap valor, quan s'executa una funció aïllada.
- Al constructor d'una classe, la nova instància.
- A un mètode
objecte.metode()
, l'objecte que el crida. - 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 ésvalue
.reject(error)
per indicar que no va anar bé i l'error éserror
.
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 ambimport someNameOfYourChoice from './path/to/file.js';
. - named:
export const someData = ...;
. S'importen ambimport { 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);