De siste årene har vist oss at nettutvikling er i endring. Etter hvert som mange funksjoner og API-er ble lagt til i nettleserne, måtte vi bruke dem på riktig måte. Språket vi kan takke JavaScript for denne æren.
Til å begynne med var utviklerne ikke overbevist om hvordan det var designet, og de hadde stort sett negative inntrykk mens de brukte dette skriptet. Over tid viste det seg at dette språket har et stort potensial, og senere ECMAScript-standarder gjør noen av mekanikkene mer menneskelige og rett og slett bedre. I denne artikkelen tar vi en titt på noen av dem.
Typer av verdier i JS
Den velkjente sannheten om JavaScript er at alt her er et objekt. Virkelig alt: matriser, funksjoner, strenger, tall og til og med booleaner. Alle typer verdier representeres av objekter og har sine egne metoder og felt. Vi kan imidlertid dele dem inn i to kategorier: primitiver og strukturaler. Verdiene i den første kategorien er uforanderlige, noe som betyr at vi kan tilordne en variabel en ny verdi, men ikke endre selve den eksisterende verdien. Den andre kategorien representerer verdier som kan endres, så de bør tolkes som samlinger av egenskaper som vi kan erstatte eller bare kalle metodene som er utformet for å gjøre det.
Omfanget av deklarerte variabler
Før vi går dypere, la oss forklare hva scope betyr. Vi kan si at scope er det eneste området der vi kan bruke deklarerte variabler. Før ES6-standarden kunne vi deklarere variabler med var-setningen og gi dem globalt eller lokalt omfang. Det første er et område som gir oss tilgang til noen variabler hvor som helst i applikasjonen, det andre er bare dedikert til et bestemt område - hovedsakelig en funksjon.
Siden standarden ES2015, JavaScript har tre forskjellige måter å deklarere variabler på. Den første er beskrevet tidligere: Variabler deklarert med var-nøkkelordet er begrenset til den aktuelle funksjonskroppen. ES6-standarden tillater oss å deklarere variabler på mer menneskelige måter - i motsetning til var-setninger, er variabler deklarert med const- og let-setninger kun begrenset til blokken. JS behandler imidlertid const-setningen ganske uvanlig sammenlignet med andre programmeringsspråk - i stedet for en persistert verdi, beholder den en persistert referanse til verdien. Kort sagt kan vi endre egenskapene til et objekt som er deklarert med en const-setning, men vi kan ikke overskrive referansen til denne variabelen. Noen sier at var-alternativet i ES6 faktisk er en let-setning. Nei, det er det ikke, og var-setningen er ikke, og den vil sannsynligvis aldri bli trukket tilbake. En god praksis er å unngå å bruke var-setninger, fordi de for det meste gir oss mer trøbbel. Til gjengjeld må vi misbruke const-setninger, helt til vi må endre referansen - da bør vi bruke let.
Eksempel på uventet scope-oppførsel
La oss begynne med følgende kode:
(() => {
for (var i = 0; i {
console.log(`Værdi av "i": ${i}`);
}, 1000);
}
})();
Når vi ser på det, ser det ut til at for-løkken itererer i-verdien, og etter ett sekund vil den logge verdiene til iteratoren: 1, 2, 3, 4, 5. Men det gjør den ikke. Som vi nevnte ovenfor, handler var-setningen om å beholde verdien av en variabel for hele funksjonskroppen; det betyr at i den andre, tredje og så videre iterasjonen vil verdien av i-variabelen bli erstattet med en neste verdi. Til slutt avsluttes løkken, og timeout-ticksene viser oss følgende: 5, 5, 5, 5, 5, 5. Den beste måten å beholde en aktuell verdi for iteratoren på, er å bruke let-setningen i stedet:
(() => {
for (let i = 0; i {
console.log(`Værdi av "i": ${i}`);
}, 1000);
}
})();
I eksemplet ovenfor beholder vi scopet til i-verdien i den gjeldende iterasjonsblokken, det er det eneste området der vi kan bruke denne variabelen, og ingenting kan overstyre den fra utenfor dette området. Resultatet i dette tilfellet er som forventet: 1 2 3 4 5. La oss se på hvordan vi kan håndtere denne situasjonen med en var-setning:
(() => {
for (var i = 0; i {
setTimeout(() => {
console.log(`Værdi av "j": ${j}`);
}, 1000);
})(i);
}
})();
Siden var-setningen handler om å holde verdien inne i funksjonsblokken, må vi kalle en definert funksjon som tar et argument - verdien av iteratorens nåværende tilstand - og deretter bare gjøre noe. Ingenting utenfor den deklarerte funksjonen vil overstyre j-verdien.
Eksempler på feil forventninger til objektverdier
Den vanligste forbrytelsen jeg har lagt merke til, er at man ignorerer kraften til strukturaler og endrer egenskapene deres, som også endres i andre deler av koden. Ta en rask titt:
const DEFAULT_VALUE = {
favoriteBand: 'The Weeknd'
};
const currentValue = DEFAULT_VALUE;
const bandInput = document.querySelector('#favorittband');
const restoreDefaultButton = document.querySelector('#restore-button');
bandInput.addEventListener('input', () => {
currentValue.favoriteBand = bandInput.value;
}, false);
restoreDefaultButton.addEventListener('click', () => {
currentValue = DEFAULT_VALUE;
}, false);
Fra begynnelsen: La oss anta at vi har en modell med standardegenskaper, lagret som et objekt. Vi ønsker å ha en knapp som gjenoppretter inngangsverdiene til standardverdiene. Etter å ha fylt inn noen verdier i inndataene, oppdaterer vi modellen. Etter et øyeblikk tenker vi at standardvalget rett og slett var bedre, så vi ønsker å gjenopprette det. Vi klikker på knappen ... og ingenting skjer. Hvorfor ikke? Fordi vi ignorerer kraften i refererte verdier.
Denne delen: const currentValue = DEFAULTVALUE forteller JS følgende: ta referansen til DEFAULTVALUE-verdien og tilordner currentValue-variabelen med den. Den virkelige verdien lagres i minnet bare én gang, og begge variablene peker på den. Hvis du endrer noen egenskaper på ett sted, betyr det at du endrer dem på et annet. Vi har noen måter å unngå slike situasjoner på. En av dem er en spread-operator. La oss fikse koden vår:
const DEFAULT_VALUE = {
favoriteBand: 'The Weeknd'
};
const currentValue = { ...DEFAULT_VALUE };
const bandInput = document.querySelector('#favorittband');
const restoreDefaultButton = document.querySelector('#restore-button');
bandInput.addEventListener('input', () => {
currentValue.favoriteBand = bandInput.value;
}, false);
restoreDefaultButton.addEventListener('click', () => {
currentValue = { ...DEFAULT_VALUE };
}, false);
I dette tilfellet fungerer spread-operatoren slik: Den tar alle egenskapene fra et objekt og oppretter et nytt objekt fylt med dem. Dette gjør at verdiene i currentValue og DEFAULT_VALUE ikke lenger peker til samme sted i minnet, og alle endringer som gjøres i én av dem, vil ikke påvirke de andre.
Ok, så spørsmålet er: handler alt om å bruke den magiske spredningsoperatoren? I dette tilfellet - ja, men våre modeller kan kreve mer kompleksitet enn dette eksemplet. Hvis vi bruker nestede objekter, matriser eller andre strukturer, vil spredningsoperatoren for den refererte verdien på øverste nivå bare påvirke det øverste nivået, og refererte egenskaper vil fortsatt dele samme plass i minnet. Det finnes mange løsninger for å håndtere dette problemet, alt avhenger av dine behov. Vi kan klone objekter på alle dybdenivåer eller, i mer komplekse operasjoner, bruke verktøy som immer som lar oss skrive uforanderlig kode nesten smertefritt.
Bland det hele sammen
Er en blanding av kunnskap om scopes og verdityper nyttig? Selvsagt er det det! La oss bygge noe som bruker begge disse:
const useValue = (defaultValue) => {
const value = [...defaultValue];
const setValue = (newValue) => {
value.length = 0; // vanskelig måte å tømme matrisen på
newValue.forEach((element, indeks) => {
value[index] = item;
});
// gjør noen andre ting
};
return [value, setValue];
};
const [animals, setAnimals] = useValue(['cat', 'dog']);
console.log(animals); // ['cat', 'dog']
setAnimals(['hest', 'ku']);
console.log(animals); // ['hest', 'ku']);
La oss forklare hvordan denne koden fungerer linje for linje. UseValue-funksjonen oppretter en matrise basert på defaultValue-argumentet; den oppretter en variabel og en annen funksjon, dens modifikator. Denne modifikatoren tar en ny verdi som på en vanskelig måte brukes på den eksisterende. På slutten av funksjonen returnerer vi verdien og dens modifikator som matriseverdier. Deretter bruker vi den opprettede funksjonen - erklærer animals og setAnimals som returnerte verdier. Bruk modifikatoren deres for å sjekke om funksjonen påvirker dyrevariabelen - ja, det fungerer!
Men vent, hva er det egentlig som er så fancy i denne koden? Referansen beholder alle de nye verdiene, og du kan legge inn din egen logikk i denne modifikatoren, for eksempel noen API-er eller en del av økosystemet som driver dataflyten din uten innsats. Dette vanskelige mønsteret brukes ofte i mer moderne JS-biblioteker, der det funksjonelle programmeringsparadigmet gjør det mulig for oss å gjøre koden mindre kompleks og lettere å lese for andre programmerere.
Sammendrag
Forståelsen av hvordan språkmekanikken fungerer under panseret, gjør at vi kan skrive mer bevisst og lett kode. Selv om JS ikke er et lavnivåspråk og tvinger oss til å ha en viss kunnskap om hvordan minne tildeles og lagres, må vi likevel holde øye med uventet oppførsel når vi modifiserer objekter. På den annen side er det ikke alltid riktig å misbruke kloner av verdier, og feil bruk har flere ulemper enn fordeler. Den riktige måten å planlegge dataflyten på er å vurdere hva du trenger, og hvilke mulige hindringer du kan støte på når du implementerer logikken i applikasjonen.
Les mer om dette: