De senaste åren har visat oss att webbutvecklingen håller på att förändras. Eftersom många funktioner och API:er lades till i webbläsarna var vi tvungna att använda dem på rätt sätt. Språket som vi är skyldiga denna ära var JavaScript.
Till en början var utvecklarna inte övertygade om hur det var utformat och hade mestadels negativa intryck när de använde detta skript. Med tiden visade det sig att detta språk har stor potential, och efterföljande ECMAScript-standarder gör några av mekanikerna mer mänskliga och helt enkelt bättre. I den här artikeln tar vi en titt på några av dem.
Olika typer av värden i JS
Den välkända sanningen om JavaScript är att allt här är ett objekt. Verkligen allt: arrayer, funktioner, strängar, siffror och till och med booleaner. Alla typer av värden representeras av objekt och har sina egna metoder och fält. Vi kan dock dela in dem i två kategorier: primitiva och strukturella. Värdena i den första kategorin är oföränderliga, vilket innebär att vi kan tilldela en variabel ett nytt värde men inte ändra det befintliga värdet. Den andra kategorin representerar värden som kan ändras, så de bör tolkas som samlingar av egenskaper som vi kan ersätta eller bara anropa de metoder som är utformade för att göra det.
Omfattning av deklarerade variabler
Innan vi går djupare, låt oss förklara vad scope betyder. Vi kan säga att scope är det enda område där vi kan använda deklarerade variabler. Före ES6-standarden kunde vi deklarera variabler med var-satsen och ge dem globalt eller lokalt scope. Den första är en sfär som gör att vi kan komma åt vissa variabler var som helst i applikationen, den andra är bara avsedd för ett specifikt område - främst en funktion.
Sedan standarden ES2015, JavaScript har tre sätt att deklarera variabler som skiljer sig åt med nyckelord. Det första beskrivs tidigare: variabler som deklareras med var-nyckelordet är begränsade till den aktuella funktionskroppen. ES6-standarden gjorde det möjligt för oss att deklarera variabler på mer mänskliga sätt - i motsats till var-satser är variabler som deklareras med const- och let-satser endast begränsade till blocket. JS behandlar dock const-satsen på ett ganska ovanligt sätt jämfört med andra programmeringsspråk - istället för ett persisterat värde, behåller den en persisterad referens till värdet. Kort sagt, vi kan ändra egenskaper hos ett objekt som deklarerats med en const-sats, men vi kan inte skriva över referensen för denna variabel. Vissa säger, att var-alternativet i ES6 faktiskt är let-satsen. Nej, det är det inte, och var-satsen är inte och kommer förmodligen aldrig att dras tillbaka. En god praxis är att undvika att använda var-satser, eftersom de oftast ger oss mer problem. I sin tur måste vi missbruka const-satser, tills vi måste ändra dess referens - då bör vi använda let.
Exempel på oväntat scope-beteende
Låt oss börja med följande kod:
(() => {
for (var i = 0; i {
console.log(`Värdet av "i": ${i}`);
}, 1000);
}
})();
När vi tittar på det verkar det som om for-slingan itererar i-värdet och efter en sekund kommer den att logga värdena för iteratorn: 1, 2, 3, 4, 5. Men det gör det inte. Som vi nämnde ovan handlar var-satsen om att behålla värdet på en variabel för hela funktionskroppen; det betyder att i den andra, tredje och så vidare iterationen kommer värdet på i-variabeln att ersättas med ett nästa värde. Slutligen avslutas slingan och timeout-tickarna visar oss följande: 5, 5, 5, 5, 5, 5. Det bästa sättet att behålla ett aktuellt värde på iteratorn är att använda let-satsen istället:
(() => {
for (let i = 0; i {
console.log(`Värdet av "i": ${i}`);
}, 1000);
}
})();
I exemplet ovan behåller vi omfattningen av i-värdet i det aktuella iterationsblocket, det är den enda sfär där vi kan använda den här variabeln och ingenting kan åsidosätta den utanför detta område. Resultatet i det här fallet är som förväntat: 1 2 3 4 5. Låt oss ta en titt på hur man hanterar den här situationen med en var-sats:
(() => {
for (var i = 0; i {
setTimeout(() => {
console.log(`Värdet av "j": ${j}`);
}, 1000);
})(i);
}
})();
Eftersom var-satsen handlar om att hålla värdet inom funktionsblocket måste vi anropa en definierad funktion som tar ett argument - värdet för iteratorns aktuella tillstånd - och sedan bara göra något. Ingenting utanför den deklarerade funktionen kommer att åsidosätta j-värdet.
Exempel på felaktiga förväntningar på objektvärden
Det vanligaste brottet som jag har märkt är att man ignorerar kraften i strukturella element och ändrar deras egenskaper som också ändras i andra delar av koden. Ta en snabb titt:
const DEFAULT_VALUE = {
favoritBand: '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);
Från början: Låt oss anta att vi har en modell med standardegenskaper, lagrade som ett objekt. Vi vill ha en knapp som återställer dess inmatningsvärden till standardvärdena. Efter att ha fyllt i inmatningen med några värden uppdaterar vi modellen. Efter en stund tycker vi att standardvalet helt enkelt var bättre, så vi vill återställa det. Vi klickar på knappen ... och ingenting händer. Varför händer ingenting? På grund av att vi ignorerar kraften i referensvärdena.
Denna del: const currentValue = DEFAULTVALUE säger följande till JS: ta referensen till DEFAULTVALUE-värde och tilldelar variabeln currentValue det. Det verkliga värdet lagras bara en gång i minnet och båda variablerna pekar på det. Om man ändrar vissa egenskaper på ett ställe innebär det att man ändrar dem på ett annat. Vi har några sätt att undvika sådana situationer. Ett som uppfyller våra behov är en spread-operator. Låt oss fixa vår kod:
const DEFAULT_VALUE = {
favoritBand: '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 det här fallet fungerar spread-operatorn på följande sätt: den tar alla egenskaper från ett objekt och skapar ett nytt objekt som fylls med dem. Tack vare detta pekar värdena i currentValue och DEFAULT_VALUE inte längre på samma plats i minnet och alla ändringar som tillämpas på ett av dem påverkar inte de andra.
Ok, så frågan är: handlar det bara om att använda den magiska spridningsoperatorn? I det här fallet - ja, men våra modeller kan kräva mer komplexitet än detta exempel. Om vi använder kapslade objekt, matriser eller andra strukturer kommer spridningsoperatören för det refererade värdet på toppnivå endast att påverka toppnivån och refererade egenskaper kommer fortfarande att dela samma plats i minnet. Det finns många lösningar för att hantera detta problem, allt beror på dina behov. Vi kan klona objekt på varje djupnivå eller, i mer komplexa operationer, använda verktyg som immer som gör att vi kan skriva oföränderlig kod nästan smärtfritt.
Blanda ihop allt
Är en blandning av kunskap om scopes och värdetyper användbar? Naturligtvis är det det! Låt oss bygga något som använder båda dessa:
const useValue = (defaultValue) => {
const värde = [...defaultValue];
const setValue = (newValue) => {
value.length = 0; // knepigt sätt att rensa array
newValue.forEach((item, index) => {
värde[index] = objekt;
});
// gör lite andra saker
};
return [värde, setValue];
};
const [animals, setAnimals] = useValue(['katt', 'hund']);
console.log(djur); // ['katt', 'hund']
setAnimals(['häst', 'ko']);
console.log(djur); // ['häst', 'ko']);
Låt oss förklara hur den här koden fungerar rad för rad. Funktionen useValue skapar en array baserat på argumentet defaultValue; den skapar en variabel och en annan funktion, dess modifierare. Denna modifierare tar ett nytt värde som på ett knepigt sätt tillämpas på det befintliga. I slutet av funktionen returnerar vi värdet och dess modifierare som matrisvärden. Därefter använder vi den skapade funktionen - deklarerar djur och setAnimals som returnerade värden. Använd deras modifikator för att kontrollera om funktionen påverkar djurvariabeln - ja, det fungerar!
Men vänta, vad exakt är så snyggt i den här koden? Referensen behåller alla nya värden och du kan lägga in din egen logik i den här modifieraren, till exempel vissa API:er eller en del av ekosystemet som driver ditt dataflöde utan ansträngning. Detta knepiga mönster används ofta i mer moderna JS-bibliotek, där det funktionella paradigmet i programmering gör att vi kan hålla koden mindre komplex och lättare att läsa för andra programmerare.
Sammanfattning
Förståelsen för hur språkmekaniken fungerar under huven gör att vi kan skriva mer medveten och lättviktig kod. Även om JS inte är ett lågnivåspråk och tvingar oss att ha viss kunskap om hur minne tilldelas och lagras, måste vi fortfarande hålla utkik efter oväntade beteenden när vi modifierar objekt. Å andra sidan är det inte alltid rätt att missbruka kloner av värden och felaktig användning har fler nackdelar än fördelar. Rätt sätt att planera dataflödet är att fundera över vad du behöver och vilka eventuella hinder du kan stöta på när du implementerar logiken i applikationen.
Läs mer om detta: