De sidste par år har vist os, at webudvikling er under forandring. Da der blev tilføjet mange funktioner og API'er til browserne, var vi nødt til at bruge dem på den rigtige måde. Sproget, som vi skylder denne ære, var JavaScript.
I starten var udviklerne ikke overbeviste om, hvordan det var designet, og de havde mest negative indtryk, når de brugte dette script. Med tiden viste det sig, at dette sprog har et stort potentiale, og efterfølgende ECMAScript-standarder gør nogle af mekanikkerne mere menneskelige og ganske enkelt bedre. I denne artikel ser vi på nogle af dem.
Typer af værdier i JS
Den velkendte sandhed om JavaScript er, at alt her er et objekt. Virkelig alt: arrays, funktioner, strenge, tal og endda booleaner. Alle typer af værdier repræsenteres af objekter og har deres egne metoder og felter. Vi kan dog inddele dem i to kategorier: primitive og strukturelle. Værdierne i den første kategori er uforanderlige, hvilket betyder, at vi kan tildele en variabel en ny værdi, men ikke ændre selve den eksisterende værdi. Den anden repræsenterer værdier, der kan ændres, så de skal fortolkes som samlinger af egenskaber, som vi kan erstatte eller bare kalde de metoder, der er designet til at gøre det.
Omfanget af deklarerede variabler
Lad os forklare, hvad scope betyder, før vi går i dybden med det. Vi kan sige, at scope er det eneste område, hvor vi kan bruge erklærede variabler. Før ES6-standarden kunne vi erklære variabler med var-sætningen og give dem global eller lokal rækkevidde. Det første er et område, der giver os adgang til nogle variabler alle steder i applikationen, det andet er kun dedikeret til et bestemt område - hovedsageligt en funktion.
Siden standarden ES2015, JavaScript har tre måder at erklære variabler på, som adskiller sig fra nøgleordet. Den første er beskrevet før: Variabler erklæret med var-nøgleordet er begrænset til den aktuelle funktionskrop. ES6-standarden gav os mulighed for at erklære variabler på mere menneskelige måder - i modsætning til var-sætninger er variabler, der er erklæret med const- og let-sætninger, kun omfattet af blokken. JS behandler dog const-sætningen ret usædvanligt sammenlignet med andre programmeringssprog - i stedet for en persisteret værdi, beholder den en persisteret reference til værdien. Kort sagt kan vi ændre egenskaber for et objekt, der er erklæret med en const-sætning, men vi kan ikke overskrive referencen til denne variabel. Nogle siger, at var-alternativet i ES6 faktisk er en let-sætning. Nej, det er det ikke, og var-sætningen er ikke, og den vil sandsynligvis aldrig blive trukket tilbage. En god praksis er at undgå at bruge var-sætninger, fordi de for det meste giver os flere problemer. Til gengæld er vi nødt til at misbruge const-sætninger, indtil vi skal ændre dens reference - så bør vi bruge let.
Eksempel på uventet scope-adfærd
Lad os starte med følgende Kode:
(() => {
for (var i = 0; i {
console.log(`Værdi af "i": ${i}`);
}, 1000);
}
})();
Når vi ser på det, ser det ud til, at for-løkken itererer i-værdien, og efter et sekund logger den iteratorens værdier: 1, 2, 3, 4, 5. Men det gør den ikke. Som vi nævnte ovenfor, handler var-sætningen om at beholde værdien af en variabel i hele funktionens krop; det betyder, at i den anden, tredje og så videre iteration vil værdien af i-variablen blive erstattet med en næste værdi. Til sidst slutter løkken, og timeout-ticksene viser os følgende: 5, 5, 5, 5, 5. Den bedste måde at beholde en aktuel værdi af iteratoren på er at bruge let statement i stedet:
(() => {
for (let i = 0; i {
console.log(`Værdien af "i": ${i}`);
}, 1000);
}
})();
I eksemplet ovenfor holder vi omfanget af i-værdien i den aktuelle iterationsblok, det er det eneste område, hvor vi kan bruge denne variabel, og intet kan tilsidesætte den uden for dette område. Resultatet i dette tilfælde er som forventet: 1 2 3 4 5. Lad os se på, hvordan man håndterer denne situation med en var-sætning:
(() => {
for (var i = 0; i {
setTimeout(() => {
console.log(`Værdi af "j": ${j}`);
}, 1000);
})(i);
}
})();
Da var-sætningen handler om at holde værdien inde i funktionsblokken, er vi nødt til at kalde en defineret funktion, som tager et argument - værdien af iteratorens aktuelle tilstand - og så bare gøre noget. Intet uden for den erklærede funktion vil tilsidesætte j-værdien.
Eksempler på forkerte forventninger til objektværdier
Den hyppigst begåede forbrydelse, som jeg har bemærket, er at ignorere styrken ved structurals og ændre deres egenskaber, som også ændres i andre kodestykker. Tag et hurtigt kig:
const DEFAULT_VALUE = {
favoriteBand: 'The Weeknd'
};
const currentValue = DEFAULT_VALUE;
const bandInput = document.querySelector('#favorite-band');
const restoreDefaultButton = document.querySelector('#restore-button');
bandInput.addEventListener('input', () => {
currentValue.favoriteBand = bandInput.value;
}, false);
restoreDefaultButton.addEventListener('click', () => {
currentValue = DEFAULT_VALUE;
}, false);
Fra begyndelsen: Lad os antage, at vi har en model med standardegenskaber, der er gemt som et objekt. Vi vil gerne have en knap, der gendanner inputværdierne til standardværdierne. Når vi har udfyldt input med nogle værdier, opdaterer vi modellen. Efter et øjeblik tænker vi, at standardvalget simpelthen var bedre, så vi vil gendanne det. Vi klikker på knappen ... og der sker ikke noget. Hvorfor ikke? Fordi vi ignorerer kraften i de refererede værdier.
Denne del: const currentValue = DEFAULTVALUE fortæller JS følgende: tag referencen til DEFAULTVALUE-værdi og tildeler variablen currentValue den. Den virkelige værdi gemmes kun én gang i hukommelsen, og begge variabler peger på den. Hvis man ændrer nogle egenskaber ét sted, betyder det, at man ændrer dem et andet sted. Vi har et par måder at undgå den slags situationer på. En af dem, der opfylder vores behov, er en spread operator. Lad os rette vores kode:
const DEFAULT_VALUE = {
favoriteBand: 'The Weeknd'
};
const currentValue = { ...DEFAULT_VALUE };
const bandInput = document.querySelector('#favorite-band');
const restoreDefaultButton = document.querySelector('#restore-button');
bandInput.addEventListener('input', () => {
currentValue.favoriteBand = bandInput.value;
}, false);
restoreDefaultButton.addEventListener('click', () => {
currentValue = { ...DEFAULT_VALUE };
}, false);
I dette tilfælde fungerer spread-operatoren på følgende måde: Den tager alle egenskaber fra et objekt og opretter et nyt objekt fyldt med dem. Takket være dette peger værdierne i currentValue og DEFAULT_VALUE ikke længere på det samme sted i hukommelsen, og alle ændringer i en af dem vil ikke påvirke de andre.
Ok, så spørgsmålet er: Handler det hele om at bruge den magiske spredningsoperator? I dette tilfælde - ja, men vores modeller kan kræve mere kompleksitet end dette eksempel. Hvis vi bruger indlejrede objekter, arrays eller andre strukturer, vil spredningsoperatoren for den refererede værdi på øverste niveau kun påvirke det øverste niveau, og de refererede egenskaber vil stadig dele det samme sted i hukommelsen. Der er mange løsninger til at håndtere dette problem, alt afhænger af dine behov. Vi kan klone objekter på alle dybdeniveauer eller, i mere komplekse operationer, bruge værktøjer som immer, der giver os mulighed for at skrive uforanderlig kode næsten smertefrit.
Bland det hele sammen
Er en blanding af viden om scopes og værdityper brugbar? Selvfølgelig er det det! Lad os bygge noget, der bruger begge dele:
const useValue = (defaultValue) => {
const value = [...defaultValue];
const setValue = (newValue) => {
value.length = 0; // vanskelig måde at rydde array på
newValue.forEach((item, index) => {
value[index] = item;
});
// gør nogle andre ting
};
return [value, setValue];
};
const [animals, setAnimals] = useValue(['cat', 'dog']);
console.log(animals); // ['cat', 'dog']
setAnimals(['horse', 'cow']);
console.log(animals); // ['horse', 'cow']);
Lad os forklare, hvordan denne kode fungerer linje for linje. Funktionen useValue opretter et array baseret på argumentet defaultValue; den opretter en variabel og en anden funktion, dens modificator. Denne modificator tager en ny værdi, som på en vanskelig måde anvendes på den eksisterende. I slutningen af funktionen returnerer vi værdien og dens modificator som array-værdier. Dernæst bruger vi den oprettede funktion - erklærer animals og setAnimals som returnerede værdier. Brug deres modificator til at tjekke, om funktionen påvirker dyrevariablen - ja, det virker!
Men vent, hvad er det egentlig, der er så smart ved denne kode? Referencen beholder alle de nye værdier, og du kan indsætte din egen logik i denne modificator, f.eks. nogle API'er eller en del af økosystemet der driver dit dataflow uden anstrengelse. Det vanskelige mønster bruges ofte i mere moderne JS-biblioteker, hvor det funktionelle programmeringsparadigme giver os mulighed for at holde koden mindre kompleks og lettere at læse for andre programmører.
Sammenfatning
Forståelsen af, hvordan sprogmekanikken fungerer under motorhjelmen, giver os mulighed for at skrive mere bevidst og let kode. Selv om JS ikke er et lavniveausprog og tvinger os til at have en vis viden om, hvordan hukommelse tildeles og lagres, skal vi stadig holde øje med uventet adfærd, når vi ændrer objekter. På den anden side er misbrug af kloner af værdier ikke altid den rigtige måde, og forkert brug har flere ulemper end fordele. Den rigtige måde at planlægge dataflowet på er at overveje, hvad du har brug for, og hvilke mulige forhindringer du kan støde på, når du implementerer applikationens logik.
Læs mere om det: