De afgelopen jaren hebben ons laten zien dat webontwikkeling aan het veranderen is. Omdat er veel functies en API's aan de browsers werden toegevoegd, moesten we ze op de juiste manier gebruiken. De taal waaraan we deze eer te danken hebben was JavaScript.
Aanvankelijk waren de ontwikkelaars niet overtuigd van het ontwerp en hadden ze vooral negatieve indrukken bij het gebruik van dit script. Na verloop van tijd bleek dat deze taal een groot potentieel heeft, en latere ECMAScript-standaarden maken sommige mechanica menselijker en simpelweg beter. In dit artikel bekijken we er een paar.
Soorten waarden in JS
De bekende waarheid over JavaScript is dat alles hier een object is. Echt alles: arrays, functies, strings, getallen en zelfs booleans. Alle soorten waarden worden weergegeven door objecten en hebben hun eigen methoden en velden. We kunnen ze echter in twee categorieën verdelen: primitieven en structurelen. De waarden van de eerste categorie zijn onveranderlijk, wat betekent dat we een variabele opnieuw kunnen toewijzen met de nieuwe waarde, maar de bestaande waarde zelf niet kunnen wijzigen. De tweede categorie vertegenwoordigt waarden die kunnen worden gewijzigd, dus moeten ze worden geïnterpreteerd als verzamelingen van eigenschappen die we kunnen vervangen of gewoon de methoden aanroepen die zijn ontworpen om dit te doen.
Bereik van gedeclareerde variabelen
Laten we, voordat we dieper ingaan, uitleggen wat de scope betekent. We kunnen zeggen dat de scope het enige gebied is waar we gedeclareerde variabelen kunnen gebruiken. Voor de ES6-standaard konden we variabelen declareren met de var-instructie en ze globale of lokale reikwijdte geven. De eerste is een gebied dat ons toegang geeft tot sommige variabelen op elke plaats van de toepassing, de tweede is alleen gewijd aan een specifiek gebied - voornamelijk een functie.
Sinds de standaard ES2015, JavaScript heeft drie manieren om variabelen aan te geven die verschillen met het sleutelwoord. De eerste is al eerder beschreven: variabelen die zijn gedeclareerd met het sleutelwoord var hebben een scoped naar de huidige functie-inhoud. ES6 standaard staat ons toe om variabelen te declareren op meer menselijke manieren - in tegenstelling tot var statements, zijn variabelen gedeclareerd door const en let statements alleen gescoped naar het blok. Echter, JS behandelt het const statement nogal ongebruikelijk in vergelijking met andere programmeertalen - in plaats van een persisted waarde, houdt het een persisted referentie naar de waarde. Kortom, we kunnen eigenschappen van een object dat is gedeclareerd met een const statement wijzigen, maar we kunnen de referentie van deze variabele niet overschrijven. Sommigen zeggen dat het var alternatief in ES6 eigenlijk een let statement is. Nee, dat is het niet, en het var statement is het niet en zal waarschijnlijk ook nooit worden ingetrokken. Een goede gewoonte is om het gebruik van var statements te vermijden, omdat ze ons meestal meer problemen geven. Op zijn beurt moeten we const statements misbruiken, totdat we de referentie moeten wijzigen - dan moeten we let gebruiken.
Voorbeeld van onverwacht scoopgedrag
Laten we beginnen met het volgende code:
(() => {
for (var i = 0; i {
console.log(`Waarde van "i": ${i}`);
}, 1000);
}
})();
Als we ernaar kijken, lijkt het erop dat de for-lus de i-waarde itereert en na één seconde de waarden van de iterator logt: 1, 2, 3, 4, 5. Nou, dat is niet zo. Zoals we hierboven hebben vermeld, gaat de var-instructie over het behouden van de waarde van een variabele voor het hele functiegedeelte; dit betekent dat in de tweede, derde enzovoort iteratie de waarde van de i-variabele wordt vervangen door een volgende waarde. Uiteindelijk eindigt de lus en de time-outtikken laten ons het volgende zien: 5, 5, 5, 5. De beste manier om een huidige waarde van de iterator te behouden, is door in plaats daarvan het let statement te gebruiken:
(() => {
for (laat i = 0; i {
console.log(`Waarde van "i": ${i}`);
}, 1000);
}
})();
In het bovenstaande voorbeeld houden we de reikwijdte van de i waarde in het huidige iteratieblok, het is het enige gebied waar we deze variabele kunnen gebruiken en niets kan het overschrijven van buiten dit gebied. Het resultaat in dit geval is zoals verwacht: 1 2 3 4 5. Laten we eens kijken hoe we deze situatie kunnen aanpakken met een var-instructie:
(() => {
for (var i = 0; i {
setTimeout() => {
console.log(`Waarde van "j": ${j}`);
}, 1000);
})(i);
}
})();
Omdat de var-instructie gaat over het binnen het functieblok houden van de waarde, moeten we een gedefinieerde functie aanroepen die een argument neemt - de waarde van de huidige status van de iterator - en dan gewoon iets doen. Niets buiten de gedeclareerde functie zal de j waarde overschrijven.
Voorbeelden van verkeerde verwachtingen van objectwaarden
De meest gepleegde misdaad die mij is opgevallen betreft het negeren van de kracht van structurals en het wijzigen van hun eigenschappen die ook in andere stukken code worden gewijzigd. Kijk maar eens snel:
const DEFAULT_VALUE = {
favorieteBand: '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);
Vanaf het begin: laten we aannemen dat we een model hebben met standaardeigenschappen, opgeslagen als een object. We willen een knop hebben die de invoerwaarden herstelt naar de standaardwaarden. Nadat we de invoer hebben ingevuld met enkele waarden, werken we het model bij. Na een moment denken we dat de standaardkeuze gewoon beter was, dus willen we die herstellen. We klikken op de knop... en er gebeurt niets. Waarom? Omdat we de kracht van gerefereerde waarden negeren.
Dit deel: const currentValue = DEFAULTVALUE vertelt JS het volgende: neem de verwijzing naar de DEFAULTVALUE waarde en wijst de variabele currentValue eraan toe. De echte waarde wordt slechts eenmaal in het geheugen opgeslagen en beide variabelen wijzen ernaar. Sommige eigenschappen op de ene plaats wijzigen, betekent ze op een andere plaats wijzigen. We hebben een paar manieren om dit soort situaties te vermijden. Een die voldoet aan onze behoeften is een spread operator. Laten we onze code aanpassen:
const DEFAULT_VALUE = {
favorieteBand: '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);
In dit geval werkt de spread operator als volgt: het neemt alle eigenschappen van een object en maakt er een nieuw object van. Hierdoor wijzen de waarden in currentValue en DEFAULT_VALUE niet langer naar dezelfde plaats in het geheugen en alle wijzigingen die worden toegepast op een van hen zullen de andere niet beïnvloeden.
Ok, dus de vraag is: draait het allemaal om het gebruik van de magische spreidingsoperator? In dit geval - ja, maar onze modellen kunnen complexer zijn dan dit voorbeeld. Als we geneste objecten, arrays of andere structuren gebruiken, heeft de spread operator van de waarde waarnaar op het hoogste niveau wordt verwezen alleen invloed op het hoogste niveau en zullen de eigenschappen waarnaar wordt verwezen nog steeds dezelfde plaats in het geheugen delen. Er zijn veel oplossingen om met dit probleem om te gaan, alles hangt af van je behoeften. We kunnen objecten klonen op elk diepteniveau of, bij complexere operaties, tools zoals immer gebruiken waarmee we bijna pijnloos onveranderlijke code kunnen schrijven.
Meng alles door elkaar
Is een mix van kennis over scopes en waarden bruikbaar? Natuurlijk! Laten we iets bouwen dat beide gebruikt:
const useValue = (defaultValue) => {
const value = [...defaultValue];
const setValue = (newValue) => {
value.length = 0; // lastige manier om array leeg te maken
newValue.forEach((item, index) => {
waarde[index] = item;
});
// doe wat andere dingen
};
return [waarde, setValue];
};
const [animals, setAnimals] = useValue(['kat', 'hond']);
console.log(animals); // ['kat', 'hond']
setAnimals(['paard', 'koe']);
console.log(animals); // ['paard', 'koe']);
Laten we regel voor regel uitleggen hoe deze code werkt. De functie useValue maakt een array op basis van het argument defaultValue; het maakt een variabele en een andere functie, de modificator. Deze modificator neemt een nieuwe waarde die op een lastige manier wordt toegepast op de bestaande. Aan het einde van de functie retourneren we de waarde en de modificator als arraywaarden. Vervolgens gebruiken we de aangemaakte functie - declare animals en setAnimals als geretourneerde waarden. Gebruik hun modificator om te controleren of de functie de variabele animal beïnvloedt - ja, het werkt!
Maar wacht, wat is er zo bijzonder aan deze code? De referentie bewaart alle nieuwe waarden en je kunt je eigen logica in deze modificator injecteren, zoals sommige API's of een deel van het ecosysteem die je dataflow moeiteloos voedt. Dat lastige patroon wordt vaak gebruikt in modernere JS bibliotheken, waar het functionele paradigma in programmeren ons toelaat om de code minder complex te houden en gemakkelijker leesbaar voor andere programmeurs.
Samenvatting
Door te begrijpen hoe taalmechanismen onder de motorkap werken, kunnen we bewustere en lichtere code schrijven. Zelfs als JS geen low-level taal is en ons dwingt om enige kennis te hebben over hoe geheugen wordt toegewezen en opgeslagen, moeten we nog steeds opletten voor onverwacht gedrag bij het wijzigen van objecten. Aan de andere kant is het misbruiken van klonen van waarden niet altijd de juiste manier en verkeerd gebruik heeft meer nadelen dan voordelen. De juiste manier om de dataflow te plannen is om te overwegen wat je nodig hebt en welke mogelijke obstakels je kunt tegenkomen bij het implementeren van de logica van de applicatie.
Lees meer: