Ces dernières années nous ont montré que le développement web est en train de changer. Alors que de nombreuses fonctionnalités et API étaient ajoutées aux navigateurs, nous devions les utiliser de la bonne manière. Le langage auquel nous devons cet honneur est le JavaScript.
Au départ, les développeurs n'étaient pas convaincus de la manière dont il était conçu et avaient des impressions plutôt négatives lorsqu'ils utilisaient ce script. Avec le temps, il s'est avéré que ce langage avait un grand potentiel, et les normes ECMAScript qui ont suivi ont rendu certains mécanismes plus humains et, tout simplement, meilleurs. Dans cet article, nous examinons certains d'entre eux.
Types de valeurs en JS
La vérité bien connue sur les JavaScript est que tout ici est un objet. Vraiment tout : les tableaux, les fonctions, les chaînes de caractères, les nombres et même les booléens. Tous les types de valeurs sont représentés par des objets et possèdent leurs propres méthodes et champs. Cependant, nous pouvons les diviser en deux catégories : les primitives et les structurelles. Les valeurs de la première catégorie sont immuables, ce qui signifie que nous pouvons réaffecter une variable avec la nouvelle valeur, mais que nous ne pouvons pas modifier la valeur existante elle-même. La seconde catégorie représente des valeurs qui peuvent être modifiées, elles doivent donc être interprétées comme des collections de propriétés que nous pouvons remplacer ou simplement appeler les méthodes conçues à cet effet.
Portée des variables déclarées
Avant d'aller plus loin, expliquons ce que signifie la portée. Nous pouvons dire que la portée est le seul endroit où nous pouvons utiliser les variables déclarées. Avant la norme ES6, nous pouvions déclarer des variables avec l'instruction var et leur donner une portée globale ou locale. La première est un domaine qui nous permet d'accéder à certaines variables à n'importe quel endroit de l'application, la seconde est dédiée à une zone spécifique - principalement une fonction.
Depuis la norme ES2015, JavaScript propose trois façons de déclarer des variables, qui diffèrent selon le mot-clé. La première est décrite plus haut : les variables déclarées par le mot-clé var sont limitées au corps de la fonction en cours. La norme ES6 nous a permis de déclarer des variables de manière plus humaine - contrairement aux déclarations var, les variables déclarées par les déclarations const et let ne s'appliquent qu'au bloc. Cependant, JS traite l'instruction const de manière assez inhabituelle si on la compare aux autres instructions const. les langages de programmation - au lieu d'une valeur persistante, il conserve une référence persistante à la valeur. En bref, nous pouvons modifier les propriétés d'un objet déclaré avec une déclaration const, mais nous ne pouvons pas écraser la référence de cette variable. Certains disent que l'alternative var dans ES6 est en fait une déclaration let. Non, ce n'est pas le cas, et l'instruction var n'est pas et ne sera probablement jamais retirée. Une bonne pratique consiste à éviter d'utiliser les déclarations var, parce qu'elles nous causent souvent plus de problèmes. En revanche, nous devons abuser des déclarations const, jusqu'à ce que nous devions modifier sa référence - nous devrions alors utiliser let.
Exemple de comportement inattendu de la portée
Commençons par ce qui suit code:
(() => {
for (var i = 0 ; i {
console.log(`Valeur de "i" : ${i}`) ;
}, 1000) ;
}
})() ;
Lorsque nous l'examinons, il semble que la boucle for itère la valeur i et, après une seconde, elle enregistre les valeurs de l'itérateur : 1, 2, 3, 4, 5. En fait, ce n'est pas le cas. Comme nous l'avons mentionné plus haut, l'instruction var permet de conserver la valeur d'une variable pour tout le corps de la fonction ; cela signifie qu'à la deuxième, troisième et ainsi de suite, la valeur de la variable i sera remplacée par la valeur suivante. Enfin, la boucle se termine et les ticks du timeout nous montrent la chose suivante : 5, 5, 5, 5, 5, 5. La meilleure façon de conserver la valeur actuelle de l'itérateur est d'utiliser l'instruction let à la place :
(() => {
for (let i = 0 ; i {
console.log(`Valeur de "i" : ${i}`) ;
}, 1000) ;
}
})() ;
Dans l'exemple ci-dessus, nous conservons la portée de la valeur i dans le bloc d'itération actuel, c'est le seul domaine où nous pouvons utiliser cette variable et rien ne peut la remplacer en dehors de cette zone. Le résultat dans ce cas est comme prévu : 1 2 3 4 5. Voyons comment gérer cette situation avec une instruction var :
(() => {
for (var i = 0 ; i {
setTimeout(() => {
console.log(`Valeur de "j" : ${j}`) ;
}, 1000) ;
})(i) ;
}
})() ;
Comme l'instruction var permet de conserver la valeur à l'intérieur du bloc fonctionnel, nous devons appeler une fonction définie qui prend un argument - la valeur de l'état actuel de l'itérateur - et ensuite faire quelque chose. Rien en dehors de la fonction déclarée ne remplacera la valeur j.
Exemples d'attentes erronées concernant la valeur des objets
Le crime le plus souvent commis que j'ai remarqué concerne le fait d'ignorer le pouvoir des structurales et de changer leurs propriétés qui sont également modifiées dans d'autres morceaux de code. Jetez un coup d'œil rapide :
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) ;
Pour commencer : supposons que nous disposons d'un modèle avec des propriétés par défaut, stockées sous la forme d'un objet. Nous souhaitons disposer d'un bouton qui rétablisse les valeurs par défaut des entrées. Après avoir rempli l'entrée avec quelques valeurs, nous mettons à jour le modèle. Au bout d'un moment, nous pensons que le choix par défaut était tout simplement meilleur, et nous voulons donc le rétablir. Nous cliquons sur le bouton... et rien ne se passe. Pourquoi ? Parce que nous avons ignoré le pouvoir des valeurs référencées.
Cette partie : const currentValue = DEFAULTVALUE indique à JS ce qui suit : prendre la référence à la valeur DEFAULTVALUE et l'affecte à la variable currentValue. La valeur réelle n'est stockée qu'une seule fois dans la mémoire et les deux variables pointent vers elle. Modifier certaines propriétés à un endroit signifie les modifier à un autre. Nous disposons de quelques moyens pour éviter ce genre de situation. L'un d'entre eux, qui répond à nos besoins, est l'opérateur d'étalement. Corrigeons notre code :
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) ;
Dans ce cas, l'opérateur d'étalement fonctionne comme suit : il prend toutes les propriétés d'un objet et crée un nouvel objet rempli de ces propriétés. Grâce à cela, les valeurs de currentValue et DEFAULT_VALUE ne pointent plus vers le même endroit dans la mémoire et toutes les modifications appliquées à l'une d'entre elles n'affecteront pas les autres.
La question qui se pose est donc la suivante : s'agit-il simplement d'utiliser l'opérateur magique d'étalement ? Dans ce cas, oui, mais nos modèles peuvent être plus complexes que cet exemple. Si nous utilisons des objets imbriqués, des tableaux ou toute autre structure, l'opérateur de dispersion de la valeur référencée au niveau supérieur n'affectera que le niveau supérieur et les propriétés référencées continueront à partager le même emplacement dans la mémoire. Il existe de nombreuses solutions pour résoudre ce problème, tout dépend de vos besoins. Nous pouvons cloner des objets à chaque niveau de profondeur ou, pour des opérations plus complexes, utiliser des outils tels qu'immer qui nous permet d'écrire du code immuable presque sans douleur.
Mélanger le tout
Est-il possible d'utiliser un mélange de connaissances sur les champs d'application et les types de valeurs ? Bien sûr que oui ! Construisons quelque chose qui utilise ces deux types de connaissances :
const useValue = (defaultValue) => {
const value = [...defaultValue] ;
const setValue = (newValue) => {
value.length = 0 ; // manière délicate d'effacer un tableau
newValue.forEach((item, index) => {
value[index] = item ;
}) ;
// fait d'autres choses
} ;
return [value, setValue] ;
} ;
const [animals, setAnimals] = useValue(['cat', 'dog']) ;
console.log(animals) ; // ['chat', 'chien']
setAnimals(['cheval', 'vache']) ;
console.log(animals) ; // ['cheval', 'vache']) ;
Expliquons le fonctionnement de ce code ligne par ligne. La fonction useValue crée un tableau basé sur l'argument defaultValue ; elle crée une variable et une autre fonction, son modificateur. Ce modificateur prend une nouvelle valeur qui est appliquée d'une manière délicate à la valeur existante. À la fin de la fonction, nous renvoyons la valeur et son modificateur sous forme de tableau. Ensuite, nous utilisons la fonction créée - déclarons les animaux et setAnimals comme valeurs retournées. Nous utilisons leur modificateur pour vérifier si la fonction affecte la variable animal - oui, ça marche !
Mais attendez, qu'est-ce qui est si sophistiqué dans ce code ? La référence conserve toutes les nouvelles valeurs et vous pouvez injecter votre propre logique dans ce modificateur, comme par exemple certaines API ou un élément de l'écosystème qui alimente votre flux de données sans aucun effort. Ce modèle délicat est souvent utilisé dans les bibliothèques JS plus modernes, où le paradigme fonctionnel de la programmation nous permet de garder le code moins complexe et plus facile à lire par d'autres programmeurs.
Résumé
La compréhension du fonctionnement des mécanismes du langage sous le capot nous permet d'écrire un code plus conscient et plus léger. Même si JS n'est pas un langage de bas niveau et nous oblige à avoir une certaine connaissance de la façon dont la mémoire est assignée et stockée, nous devons toujours garder un œil sur les comportements inattendus lors de la modification d'objets. D'autre part, l'utilisation abusive de clones de valeurs n'est pas toujours la bonne solution et une utilisation incorrecte présente plus d'inconvénients que d'avantages. La bonne façon de planifier le flux de données est de considérer ce dont vous avez besoin et les obstacles possibles que vous pouvez rencontrer lors de la mise en œuvre de la logique de l'application.
En savoir plus :