ÉLOQUENT JAVASCRIPT

3e Édition

Marijn Haverbeke

Traducteurs

| CptNome | Setni | Xmarabout | Elariondakta |

Voici un livre qui traite du contrôle des ordinateurs. Aujourd’hui, les ordinateurs sont aussi communs que le sont les tournevis, mais ils sont hautement plus complexes, et faire de sorte qu’ils puissent exécuter ce que vous voudriez qu’ils fassent n’est pas toujours si facile.

Si la tâche que vous souhaitez que l’ordinateur exécute est très commune, une tâche bien définie, comme afficher vos courriels ou agir à titre de calculatrice, vous pouvez simplement ouvrir l’application appropriée et l’utiliser. Mais pour les tâches uniques ou tâches sans fin, il n’existe probablement pas d’application.

Commentez

- Titre : ÉLOQUENT JAVASCRIPT

- Auteur : Marijn Haverbeke

- Traducteurs : CptNome, Setni, Xmarabout, Elariondakta

- Parution : 19 juillet 2019

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2019 Marijn Haverbeke. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Introduction

- I-A. La programmation

- I-B. Pourquoi les langages sont importants

- I-C. Qu’est-ce que JavaScript

- I-D. Quoi faire avec le code ?

- I-E. Vue d’ensemble de ce livre

- I-F. Conventions typographiques

- II. Valeurs, Types et Opérateurs

- II-A. Valeurs

- II-B. Nombres

- II-B-1. Arithmétique

- II-B-2. Nombres spéciaux

- II-C. Chaînes

- II-D. Opérateurs unaires

- II-E. Valeurs booléennes

- II-E-1. Comparaison

- II-E-2. Opérateurs logiques

- II-F. Valeurs vides

- II-G. Conversion de type automatique

- II-G-1. Court-circuiter les opérateurs logiques

- II-H. Résumé

- III. Structure du Programme

- III-A. Expressions et déclarations

- III-B. Variables

- III-C. Noms de variables

- III-D. Environnement

- III-E. Fonctions

- III-F. Fonction console.log

- III-G. Valeurs de retour

- III-H. Flux de contrôle

- III-I. Exécution conditionnelle

- III-J. Boucles while et do

- III-K. Indenter le code

- III-L. Boucles for

- III-M. Sortir d'une boucle

- III-N. Mettre à jour des variables de manière succincte

- III-O. Répartir sur la base d'une valeur avec switch

- III-P. Capitalisation

- III-Q. Commentaires

- III-R. Résumé

- III-S. Exercices

- III-S-1. Mettre un triangle en boucle

- III-S-2. FizzBuzz

- III-S-3. Plateau d'échecs

- IV. Les fonctions

- IV-A. Définir une fonction

- IV-B. Fixations et portées

- IV-C. Portée imbriquée

- IV-D. Les fonctions en tant que valeur

- IV-E. Autres déclarations

- IV-F. Fonctions fléchées

- IV-G. La pile d'exécution

- IV-H. Arguments optionnels

- IV-I. Fermeture (Closure)

- IV-J. Récursion

- IV-K. Fonctions réutilisables

- IV-L. Synthèse

- IV-M. Exercices

- IV-M-1. Le minimum

- IV-M-2. Récursion

- IV-M-3. Comptage des majuscules

- V. Chapitre 4 : structure des données - Objets et Tableaux

- V-A. L’écureuil-garou

- V-B. Ensemble de données

- V-C. Propriétés

- V-D. Méthodes

- V-E. Objets

- V-F. Mutabilité

- V-F-1. Le journal du lycanthrope

- V-G. Calcul des corrélations

- V-H. Boucle dans un tableau

- V-I. L’analyse finale

- V-J. Plus loin avec les tableaux

- V-K. Chaînes de caractères et leurs propriétés

- V-L. Le paramètre Rest

- V-M. L’objet Math

- V-N. Déstructuration

- V-O. JSON

- V-P. Résumé

- V-Q. Exercices

- V-Q-1. La somme d’un intervalle

- V-Q-2. Renverser un tableau

- V-Q-3. Une liste

- V-Q-4. Comparaison profonde

- VI. Les fonctions d'ordre supérieur

- VI-A. Abstraction

- VI-B. Abstraire la répétition

- VI-C. Les fonctions d'ordre supérieur

- VI-D. Jeu de données de scripts

- VI-E. Filtrer les tableaux

- VI-F. Transformer avec map

- VI-G. Résumer par réduction à une seule valeur

- VI-H. Composition de fonctions

- VI-I. Chaînes de caractères et codage des caractères

- VI-J. Reconnaître du texte

- VI-K. Résumé

- VI-L. Exercices

- VI-L-1. Aplatissement

- VI-L-2. Votre propre boucle

- VI-L-3. Tout

- VI-L-4. Direction d’écriture dominante

- VII. La vie secrète des objets

- VII-A. Introduction

- VII-B. Encapsulation

- VII-C. Méthodes

- VII-D. Prototypes

- VII-E. Classes

- VII-F. La notation de classe

- VII-G. Réécrire des propriétés dérivées

- VII-H. Maps

- VII-I. Polymorphisme

- VII-J. Symboles

- VII-K. L’interface Iterator

- VII-L. Accesseurs(getters), mutateurs(setters) et statiques

- VII-M. Héritage

- VII-N. L’opérateur instanceof

- VII-O. Résumé

- VII-P. Exercices

- VII-P-1. Un type Vector

- VII-P-2. Des groupes

- VII-P-3. Des groupes itérables

- VII-P-4. Emprunter une méthode

I - Introduction

« Nous pensons que nous créons le système pour nos propres usages. Nous croyons que nous le construisons à notre image… Mais l’ordinateur n’est pas vraiment comme nous. Il s’agit d’une projection d’une très petite partie de nous-même : cette partie dévouée à la logique, l’ordre, les règles et la clarté. »
- Ellen Ullman, Close to the Machine : Technophilia and its Discontents

Micro-ordinateur et tournevis

C’est ici que la programmation peut entrer en jeu. Programmer est l’acte de construire un programme – un ensemble précis d’instructions indiquant à un ordinateur quoi faire. Parce que les ordinateurs sont des bêtes stupides et pédantes, la programmation est fondamentalement fastidieuse et frustrante.

Heureusement, si vous arrivez à faire abstraction de ce désagrément et peut-être même apprécier la rigueur de penser dans les termes que ces machines stupides peuvent comprendre, la programmation peut devenir gratifiante. Cela vous permet de réaliser en quelques secondes des tâches impossibles à réaliser à la main. C’est un moyen pour que votre ordinateur réalise des choses qu’il ne pouvait faire auparavant. Et tout cela procure un merveilleux exercice de pensée abstraite.

La plupart des programmes sont réalisés avec des langages de programmation. Un langage de programmation est un langage artificiel inventé pour fournir des instructions aux ordinateurs. Il est intéressant de remarquer que la manière la plus efficace que nous ayons trouvée pour communiquer avec l’ordinateur soit aussi similaire à la façon dont on communique entre nous. Comme les langages humains, les langages informatiques permettent à des mots et des phrases d’être combinés de façon nouvelle, rendant possible l’expression de tout nouveaux concepts.

Au début, les interfaces basées sur le texte telles que les invites de commandes BASIC et DOS des années 80 et 90 étaient le moyen principal d’interagir avec les ordinateurs. Elles ont largement été remplacées par des interfaces graphiques, qui sont plus faciles d’approche, mais qui offrent moins de liberté. Les langages de programmation sont toujours présents si vous savez où regarder. Un de ces langages, JavaScript, est intégré dans tous les navigateurs web modernes et de ce fait, est disponible sur la plupart des appareils.

Ce livre va tenter de vous le rendre suffisamment familier pour vous permettre de faire des choses utiles et amusantes avec ce langage.

I-A - La programmation

Parallèlement à l’enseignement du JavaScript, je vais vous initier aux principes de base de la programmation. Il s’avère que la programmation est difficile. Les règles fondamentales sont claires et simples, mais les programmes construits sur ces règles tendent à devenir assez complexes pour introduire leurs propres règles et de nouvelles complexités. D’une certaine manière, vous construisez votre propre labyrinthe et vous pourriez bien vous y perdre.

Pendant la lecture de ce livre, il y aura des moments terriblement frustrants. Si vous êtes nouveau dans la programmation, il y aura beaucoup de nouvelle matière à digérer. La plupart des éléments de cette matière seront alors combinés de manière à approfondir votre compréhension.

Libre à vous de faire les efforts nécessaires. Lorsque vous avez du mal à suivre le livre, ne partez sur aucune conclusion hâtive concernant vos propres capacités. Vous êtes capable – vous avez juste besoin de persévérer. Prenez une pause, relisez certains passages et assurez-vous de lire et de comprendre les exemples de programmes et les exercices. Apprendre est un travail difficile, mais chaque chose que vous apprendrez vous appartiendra et rendra l’apprentissage subséquent plus facile.

« Quand les actions deviennent non productives, ramassez de l’information ; quand les informations deviennent indigestes, dormez. »

- Ursula L. Le Guin, The Left Hand of Darkness

Un programme est l’ensemble de plusieurs éléments. C’est un morceau de texte rédigé par un programmeur, c’est la force dirigeante qui fait que l’ordinateur fait ce qu’il fait, ce sont les données dans la mémoire de l’ordinateur, d’ailleurs il contrôle les actions effectuées sur cette même mémoire. Les analogies qui tentent de comparer un programme à des objets familiers sont souvent incomplètes. Celle qui convient le mieux est celle d’une machine – beaucoup de parties différentes sont généralement impliquées et pour que l’ensemble se tienne, nous devons considérer les façons dont ces parties sont interconnectées et contribuent au fonctionnement de l’ensemble.

Un ordinateur est une machine physique qui agit comme l’hôte de ces machines immatérielles. Les ordinateurs en soi ne peuvent seulement effectuer que des actions simples et stupides. La raison pour laquelle ils sont si utiles est qu’ils exécutent ces actions à une vitesse incroyablement élevée. Un programme peut ingénieusement combiner une quantité gigantesque de ces actions simplissimes afin de réaliser des choses beaucoup plus complexes.

Un programme est une construction de pensées. Ça ne coûte rien à construire, ça ne pèse rien, et ça grandit rapidement au rythme de vos doigts sur le clavier.

Mais sans attention, la taille d’un programme et sa complexité vont s’étendre hors de contrôle, mystifiant même la personne qui l’a créé. Garder un programme sous contrôle est le principal problème en programmation. Quand un programme fonctionne, c’est merveilleux. L’art de la programmation est l’aptitude de contrôler la complexité. L’enjeu principal est subtil – faire simple à travers la complexité.

Certains programmeurs croient que cette complexité est mieux contrôlée en utilisant seulement de petits ensembles de techniques très bien comprises dans leurs programmes. Ils ont établi des règles strictes (« les bonnes pratiques ») prescrivant la forme qu’un programme devrait avoir et ils restent prudemment à l’intérieur de leur petite zone de confort.

Ceci n’est pas seulement ennuyant, c’est inefficace. Les nouveaux problèmes nécessitent souvent de nouvelles solutions. Le monde de la programmation est très jeune et continue de se développer rapidement, et est assez varié pour laisser largement de place à différentes approches. Il y a de nombreuses et terribles méprises à faire dans le design d’un programme, et vous devriez progresser de manière à comprendre ce que vous faites. L’instinct de savoir à quoi ressemble un bon programme s’acquiert avec la pratique, non en suivant une liste de règles.

I-B - Pourquoi les langages sont importants

Au tout début de l’informatique, il n’y avait pas de langages. Les programmes ressemblaient à ceci :

00110001 00000000 00000000

00110001 00000001 00000001

00110011 00000001 00000010

01010001 00001011 00000010

00100010 00000010 00001000

01000011 00000001 00000000

01000001 00000001 00000001

00010000 00000010 00000000

01100010 00000000 00000000

Voici un programme qui ajoute ensemble tous les nombres de 1 à 10 et imprime le résultat : 1 + 2 + … + 10 = 55. Il pourrait s’exécuter sur une simple et hypothétique machine. Pour programmer sur les premiers ordinateurs, il était nécessaire de configurer de grands ensembles de commutateurs dans la bonne position ou encore perforer des trous dans un ruban ou une carte et les insérer dans l’ordinateur. Vous pouvez probablement imaginer comment cette procédure était fastidieuse et sujette aux erreurs. Même écrire de simples programmes demandait une grande intelligence et de la discipline. Les programmes plus complexes étaient pratiquement inconcevables.

Évidemment, entrer manuellement ces mystérieux schémas de bits (des uns et des zéros) procurait au programmeur le sentiment profond d’être un puissant sorcier. Et cela devait en valoir la peine en termes de satisfaction professionnelle.

Chacune des lignes du précédent programme contient une instruction unique. On pourrait le traduire en français ainsi :

1. Écrire le nombre 0 à l’emplacement mémoire 0.

2. Écrire le nombre 1 à l’emplacement mémoire 1.

3. Écrire la valeur située à l’emplacement mémoire 1 dans l’emplacement à l’adresse 2.

4. Soustraire le nombre 11 de la valeur située à l’emplacement mémoire 2.

5. Si la valeur à l’emplacement mémoire 2 est le nombre 0, continuer avec l’instruction 9.

6. Ajouter la valeur située à l’emplacement mémoire 1 à la valeur située à l’emplacement mémoire 0.

7. Ajouter le nombre 1 à la valeur située à l’emplacement mémoire 1.

8. Continuer avec l’instruction 3.

9. Afficher la valeur située à l’emplacement mémoire 0.

Bien que cela soit déjà plus lisible que la soupe de bits, cela reste sensiblement obscur. Il est beaucoup plus aisé d’utiliser des noms à la place des nombres pour les instructions et les adresses mémoires.

1. Affecter la variable total à 0.

2. Affecter la variable compteur à 1.

3. [boucle]

4. Affecter la variable comparer à la variable compteur.

5. Soustraire 11 de la variable comparer.

6. Si comparer est égal à zéro, continuer à [fin].

7. Ajouter compteur à total.

8. Ajouter 1 à compteur.

9. Continuer à [boucle].

10. [fin]

11. Afficher total.

À ce moment, pouvez-vous comprendre comment le programme fonctionne ? Les deux premières lignes affectent leur valeur initiale à deux emplacements mémoire : total sera utilisé pour compiler le résultat du calcul, et compteur servira à faire le suivi du nombre auquel nous nous sommes rendus. Les lignes utilisant comparer sont probablement les plus étranges. Le programme veut savoir si compteur est égal à 11 pour décider s’il peut arrêter son exécution. Puisque notre machine hypothétique est plutôt primitive, elle peut seulement comparer si un nombre est zéro, puis prendre une décision en conséquence. Donc, le programme utilise l’emplacement mémoire nommé comparer pour calculer la valeur de compteur - 11 et prendre une décision basée sur cette valeur. Les deux lignes suivantes ajoutent la valeur de compteur au résultat et augmentent compteur de 1 chaque fois que le programme décide que compteur n’est pas encore égal à 11.

Voici le même programme en JavaScript :

1. let total = 0, count = 1;

2. while (count <= 10) {

3. total += count;

4. count += 1;

5. }

6. console.log(total);

7. // → 55

Cette version apporte quelques améliorations. Le plus important, il n’est plus nécessaire de spécifier la manière dont nous voulons que le programme saute en arrière et en avant. La boucle while s’en charge. Elle continue d’exécuter le bloc (entouré par les accolades) en dessous aussi longtemps que la condition fournie est respectée. Cette condition est « count <= 10 », ce qui veut dire « count est plus petit ou égal à 10 ». Nous n’avons plus besoin de créer une valeur temporaire pour la comparer à zéro, détail qui était tout simplement insignifiant. Une partie de la puissance des langages de programmation est qu’ils peuvent s’occuper des détails insignifiants pour nous.

À la fin du programme, après que la boucle while est terminée, l’instruction console.log est utilisée pour afficher le résultat.

Finalement, voici ce à quoi le programme aurait pu ressembler si nous avions eu les commandes d’instructions range et sum disponibles qui, respectivement, créent une collection de nombres à l’intérieur d’un intervalle donné et calcule la somme de la collection de nombres :

1. console.log(sum(range(1, 10))) ;

2. // → 55

La morale de cette histoire est qu’un même programme peut être rédigé d’une manière courte ou longue, de façon peu lisible ou facile à comprendre. La première version du programme était extrêmement obscure, alors que la version finale est presque un langage humain (anglais) : « log the sum of the range of numbers from 1 to 10 ». (« Afficher la somme des nombres dans l’intervalle de 1 à 10 ».) (Nous verrons dans les chapitres suivants comment définir des opérations comme sum et range.)

Un bon langage de programmation aide le programmeur en lui permettant de parler à un niveau plus élevé des actions que l’ordinateur doit accomplir. Il aide à éviter d’avoir à tenir compte des détails, il fournit des blocs d’instructions pratiques (tel que while et console.log), il permet de définir ses propres blocs d’instructions (tel que sum et range), et rend ces blocs faciles à construire.

I-C - Qu’est-ce que JavaScript

JavaScript fut introduit en 1995 comme une manière d’ajouter des programmes dans les pages web sur le navigateur Netscape. Le langage est désormais intégré dans tous les grands navigateurs web graphiques. Il rend possibles les applications web modernes – applications avec lesquelles vous interagissez directement sans devoir recharger la page à chaque action. JavaScript est utile également dans les sites web plus traditionnels en fournissant différentes formes d’interactivités et de fonctionnalités.

Il est important de noter que JavaScript n’a pratiquement rien à voir avec le langage de programmation nommé Java. Les similarités de nom furent inspirées par des considérations marketing plutôt que par bon jugement. Quand JavaScript fut introduit, le langage Java était fort et gagnait en popularité. Quelqu’un pensa qu’il serait judicieux de surfer sur ce succès. Maintenant nous sommes pris avec ce nom.

Après son adoption en dehors de Netscape, un standard fut défini pour décrire la façon dont le langage JavaScript devait fonctionner de manière à ce que les nombreux logiciels qui revendiquaient supporter JavaScript puissent parler du même langage. Il se nomme le standard ECMAScript, d’après l’organisation internationale Ecma qui fut à l’origine de la standardisation.

Certaines personnes disent des choses horribles à propos de JavaScript. Certaines de ces choses sont vraies. Quand je dus écrire quelque chose en JavaScript pour la première fois, j’en suis venu rapidement à mépriser ce langage. Il pouvait accepter pratiquement tout ce que j’écrivais, mais l’interprétait d’une manière complètement différente de ma volonté. Cela avait beaucoup à voir avec le fait que je n’avais aucune idée de ce que je faisais, bien sûr, mais il y a un véritable problème là : JavaScript est ridiculement permissif. L’idée derrière cette conception était que cela rendrait la programmation JavaScript plus facile pour les débutants. Dans les faits, cela rend plus ardu de trouver les problèmes dans vos programmes parce que le système ne vous les indique pas expressément.

Par ailleurs, cette flexibilité a aussi ses avantages. Cela laisse place à beaucoup de techniques qui sont impossibles à réaliser avec les langages plus stricts ; et comme vous pourrez le découvrir par exemple au chapitre 10, cela permet de surmonter certaines lacunes de JavaScript. Après avoir appris le langage convenablement et travaillé avec lui un certain temps, j’ai commencé à aimer JavaScript.

Il y a eu plusieurs versions de JavaScript. ECMAScript version 3 fut la plus largement supportée des versions pendant la grande ascension de JavaScript aux alentours de 2000 à 2010. À cette époque, les travaux sur l’ambitieuse version 4 étaient en cours, et planifiaient un nombre important d’améliorations et d’extensions au langage. Changer radicalement un langage vivant et largement utilisé s’est avéré politiquement difficile et les travaux sur la version 4 furent abandonnés en 2008 menant, en 2009, à une version 5 beaucoup moins ambitieuse qui apporta seulement quelques améliorations non controversées. Puis en 2015 vint la version 6, une mise à jour majeure incluant certaines des idées planifiées pour la version 4. Depuis, nous avons eu de nouvelles petites mises à jour chaque année.

Le fait que le langage évolue signifie que les navigateurs doivent constamment se mettre à jour, et si vous utilisez un vieux navigateur, il est possible qu’il ne supporte pas toutes les fonctionnalités. Les concepteurs du langage font attention pour n’apporter aucun changement qui puisse casser les programmes existants, cela fait en sorte que les nouveaux navigateurs peuvent quand même exécuter les vieux programmes. Dans ce livre, j’utilise la version 2017 de JavaScript.

Les navigateurs web ne sont pas les seules plateformes où JavaScript est utilisé. Certaines bases de données telles que MongoDB et CouchDB utilisent JavaScript comme langage de scripts et de requêtes. Plusieurs plateformes de programmation pour postes de travail et pour serveurs, comme le notable projet Node.js (le sujet du chapitre 20), fournissent un environnement pour programmer en JavaScript en dehors du navigateur.

I-D - Quoi faire avec le code ?

Le code est le texte qui constitue les programmes. La plupart des chapitres de ce livre contiennent beaucoup de code. Je crois que lire et écrire du code est une partie indispensable de l’apprentissage de la programmation. Essayez de ne pas simplement survoler les exemples – lisez-les attentivement et comprenez-les. Cela pourra sembler lent et confus au début, mais je vous promets que vous allez vous y habituer. La même chose vaut pour les exercices. Ne présumez pas que vous les comprenez tant que vous n’avez pas encore écrit une solution valide.

Je vous recommande de tester vos solutions aux exercices dans un interpréteur JavaScript. De cette façon, vous saurez immédiatement si ce que vous faites fonctionne, et, je le souhaite, serez tenté d’expérimenter et d’aller au-delà des exercices.

Lorsque vous lisez ce livre dans votre navigateur, vous pouvez éditer (et exécuter) tous les exemples de programmes en cliquant dessus.

Si vous désirez exécuter les programmes inscrits dans ce livre en dehors du site web du livre, certaines attentions seront requises. Plusieurs exemples sont complets et devraient fonctionner dans n’importe quel environnement JavaScript. Mais le code de certains des derniers chapitres est souvent écrit pour un environnement spécifique (le navigateur ou Node.js) et ne peut pas être exécuté ailleurs. De plus, certains chapitres décrivent de plus gros programmes, et les morceaux de code inclus sont dépendants d’autres ou de fichiers externes. Le bac à sable (SandBox) du site web fournit des liens vers des fichiers zip contenant tous les scripts et données nécessaires à l’exécution du code pour un chapitre donné.

I-E - Vue d’ensemble de ce livre

Ce livre contient essentiellement trois parties. Les douze premiers chapitres parlent du langage JavaScript. Les sept chapitres suivants concernent les navigateurs web et la manière dont JavaScript est utilisé pour les programmer. Finalement, deux chapitres sont dédiés à Node.js, un autre environnement dans lequel on peut programmer en JavaScript.

Dans le livre, il y a cinq projets de chapitres qui décrivent de plus grands exemples de programmes qui vous donneront un goût de ce qu’est la programmation. Par ordre d’apparition, nous travaillerons à construire un « robot de livraison », un « langage de programmation », un « jeu de plateforme », un « programme de dessin digital » et un « site web dynamique ».

La partie du livre traitant du langage commence avec quatre chapitres traitant de la structure de base du langage JavaScript. Ils introduisent les « structures de contrôle » (tel que le mot réservé while que vous avez vu dans cette introduction), les « fonctions » (écrire vos propres blocs de code), et les « structures de données ». Après cela, vous serez en mesure d’écrire de petits programmes. Ensuite, les chapitres 5 et 6 traitent des techniques pour utiliser les fonctions et les objets pour écrire du code abstrait et garder la complexité sous contrôle.

Après un premier chapitre du projet, la partie du livre dédiée au langage continue avec des chapitres sur la « gestion des erreurs et la résolution de bogues », les « expressions régulières » (un outil important pour travailler avec du texte), la « modularité » (une autre défense face à la complexité), et la « programmation asynchrone » (gérer les événements qui ont une durée). Le second chapitre du projet conclut cette première partie du livre.

La deuxième partie, les chapitres 13 à 19, décrit les outils auxquels le navigateur JavaScript a accès. Vous apprendrez à afficher des choses à l’écran (chapitres 14 et 17), répondre aux interactions de l’utilisateur (chapitre 15), et communiquer à travers le réseau (chapitre 18). On retrouve encore deux chapitres du projet dans cette partie.

Après cela, le chapitre 20 décrit Node.js, et le chapitre 21 construit un petit site web utilisant cet outil.

I-F - Conventions typographiques

Dans ce livre, le texte écrit dans une police monospace va représenter des éléments de programmes – parfois il s’agit d’extraits de code autonome, et parfois ils réfèrent à une partie d’un programme connexe. Les programmes (dont vous en avez déjà vu quelques-uns) sont écrits de la façon suivante :

1. function factorial(n) {

2. if (n == 0) {

3. return 1;

4. } else {

5. return factorial(n - 1) * n;

6. }

7. }

Parfois, pour montrer la sortie qu’un programme produit, la sortie attendue est écrite à la suite, avec deux barres obliques et une flèche en avant.

console.log(factorial(8));
// → 40320

Bonne lecture !

II - Valeurs, Types et Opérateurs

« Sous la surface de la machine, le programme bouge. Sans effort, il se dilate et se contracte. En grande harmonie, les électrons se dispersent et se regroupent. Les formes sur l’écran ne sont que des ondulations sur l'eau. L'essence reste invisible en dessous. »
— Maître Yuan-Ma, Le Livre de la Programmation

À l’intérieur du monde de l'ordinateur, ce ne sont que des données. Vous pouvez lire des données, modifier des données, créer de nouvelles données — mais ce qui n'est pas une donnée ne peut pas être mentionné. Toutes ces données sont stockées comme de longues séquences de bits et sont donc fondamentalement identiques.

Les bits représentent n’importe quelle donnée et ont deux valeurs, habituellement décrits comme des zéros et des uns. À l’intérieur de l'ordinateur, ils prennent des formes telles qu'une charge électrique haute ou basse, un signal fort ou faible, ou un point brillant ou terne sur la surface d'un CD. N'importe quelle information discrète peut être réduite à une séquence de zéros et de uns, et donc représentée en bits.

Par exemple, nous pouvons exprimer le nombre 13 en bits. Cela fonctionne de la même façon qu'un nombre décimal, mais au lieu de dix chiffres différents, vous n'en avez que deux, et le poids de chacun s’accroît par un facteur 2 de droite à gauche. Voici les bits qui font le nombre 13, avec le poids des chiffres affiché en dessous d'eux :

1. 0   0   0   0   1   1   0   1

2. 128  64  32  16   8   4   2   1

C'est donc le nombre binaire 00001101. Ses chiffres non nuls représentent 8, 4 et 1, et totalisent 13.

II-A - Valeurs

Imaginez une mer de bits — un océan de bits. Un ordinateur moderne typique a plus de 30 milliards de bits dans son stockage de données volatiles (mémoire de travail). Le stockage non volatil (le disque dur ou équivalent) tend à en avoir plus par plusieurs ordres de magnitude.

Pour être capable de travailler avec de telles quantités de bits sans être perdu, nous devons les séparer en morceaux qui représentent des éléments d’information. Dans un environnement JavaScript, ces morceaux sont appelés valeurs. Bien que toutes ces valeurs soient faites de bits, elles jouent des rôles différents. Toute valeur a un type qui détermine son rôle. Certaines valeurs sont des nombres, certaines sont des morceaux de texte, certaines sont des fonctions, et ainsi de suite.

Pour créer une valeur, vous devez juste invoquer son nom. C'est pratique. Vous n'avez pas besoin de collecter des matériaux de construction pour vos valeurs ou payer pour elles. Vous en appelez une et vous l’avez immédiatement. Bien sûr, elles ne sont pas vraiment créées à partir de rien. Chaque valeur doit être stockée quelque part, et si vous voulez utiliser une gigantesque quantité d'entre elles au même moment, vous pouvez être à court de mémoire. Heureusement, cela n'est un problème que si vous avez besoin de toutes simultanément. Dès que vous n'utilisez plus une valeur, elle disparaîtra, laissant derrière elle ses bits pour être recyclés comme matériel de construction pour la prochaine génération de valeurs.

Ce chapitre introduit les éléments atomiques des programmes JavaScript, qui sont les types simples de valeurs et les opérateurs qui peuvent agir sur de telles valeurs.

II-B - Nombres

Les valeurs du type nombre sont, sans surprise, des valeurs numériques. Dans un programme JavaScript, elles sont écrites ainsi :

1. 13

Utilisez cela dans un programme, et cela entraînera la création du motif de bit pour le nombre 13 dans la mémoire de l’ordinateur.

JavaScript utilise un nombre fixe de bits, 64 d'entre eux, pour stocker valeur numérique unique. Il existe seulement un certain nombre de motifs que vous pouvez faire avec 64 bits, ce qui signifie que le nombre de différents nombres pouvant être représentés est limité. Avec N nombres décimaux, vous pouvez représenter nombres. De façon similaire, avec 64 chiffres binaires, vous pouvez représenter nombres différents, ce qui vaut environ 18 quintillions (un 18 suivi de 18 zéros). C'est beaucoup.

Auparavant, la mémoire de l’ordinateur était beaucoup plus petite, et les gens tendaient à utiliser des groupes de 8 ou 16 bits pour représenter leurs nombres. Il était facile de déborder (overflow) de si petits nombres par accident — pour finir avec un nombre qui ne peut pas être contenu dans le nombre donné de bits. Aujourd'hui, même les ordinateurs qui rentrent dans votre poche ont plein de mémoire, donc vous êtes libre d'utiliser des morceaux de 64 bits, et vous devez seulement vous inquiéter quand vous traitez avec des nombres réellement astronomiques.

Cependant, les nombres de moins de 18 quintillions ne peuvent pas tous être contenus dans un nombre JavaScript. Ces bits stockent aussi des nombres négatifs, donc un bit indique le signe du nombre. Un problème plus important est que les nombres non entiers doivent aussi être représentés. Pour faire cela, certains bits sont utilisés pour stocker la position du séparateur décimal. Le vrai nombre entier maximal qui peut être stocké est plus autour de 9 quadrillions (15 zéros) — ce qui est toujours plaisant.

Les nombres fractionnaires sont écrits avec un point.

1. 9.81

Pour de très grands ou très petits nombres, vous pouvez aussi utiliser la notation scientifique en ajoutant un e (pour exposant), suivi par l'exposant du nombre.

1. 2.998e8

C'est 2.998 × 10^8 = 299 800 000.

Les calculs avec les nombres entiers (aussi appelés entiers) plus petits que le susmentionné 9 quadrillions sont garantis d’être toujours précis. Malheureusement, les calculs avec les nombres fractionnaires ne le sont généralement pas. Tout comme π (pi) ne peut pas être exprimé précisément par un nombre fini de chiffres décimaux, beaucoup de nombres perdent en précision quand seulement 64 bits sont disponibles pour les stocker. C'est dommage, mais cela ne cause des problèmes pratiques que dans des situations spécifiques. L’important est d’en prendre conscience et de traiter les nombres numériques fractionnaires comme approximations, et non pas comme des valeurs précises.

II-B-1 - Arithmétique

La principale chose à faire avec les nombres c'est de l’arithmétique. Les opérations arithmétiques comme l'addition ou la multiplication prennent deux valeurs numériques et produisent un nouveau nombre à partir d'eux. Voici à quoi cela ressemble en JavaScript :

1. 100 + 4 * 11

Les symboles + et * sont appelés opérateurs. Le premier correspond à l'addition, et le second correspond à la multiplication. Mettre un opérateur entre deux valeurs l'appliquera à ces valeurs et produira une nouvelle valeur.

Mais est-ce que l'exemple signifie « ajouter 4 et 100, et multiplier le résultat par 11 », ou est-ce que la multiplication est réalisée avant l'addition ? Comme vous avez dû le deviner, la multiplication est faite en premier. Mais comme en arithmétique, vous pouvez changer cela en encadrant l'addition avec des parenthèses.

1. (100 + 4) * 11

Pour la soustraction, il y a l’opérateur -, et la division peut être faite avec l’opérateur /.

Quand les opérateurs apparaissent ensemble sans parenthèses, l'ordre dans lequel ils sont appliqués est déterminé par la précédence des opérateurs. L'exemple montre que la multiplication vient avant l'addition. L’opérateur / à la même précédence que *. De même pour + et -. Quand plusieurs opérateurs avec la même précédence apparaissent les uns après les autres, comme dans 1 - 2 + 1, ils sont appliqués de gauche à droite : (1 - 2) + 1.

Ces règles de précédence ne sont pas quelque chose dont vous devez vous inquiéter. En cas de doute, ajouter simplement des parenthèses.

Il y a un autre opérateur arithmétique, que vous ne reconnaîtrez peut-être pas immédiatement. Le symbole % est utilisé pour représenter l’opération de reste. X % Y est le reste de la division de X par Y. Par exemple, 314 % 100 produit 14, et 144 % 12 retourne 0. La précédence de l’opérateur de reste est le même que pour la multiplication et la division. Vous verrez aussi souvent cet opérateur être appelé modulo.

II-B-2 - Nombres spéciaux

Il y a trois valeurs spéciales en JavaScript qui sont considérées comme des nombres, mais ne se comportent pas comme des nombres normaux.

Les deux premières sont Infinity et -Infinity qui représentent les infinis positif et négatif. Infinity - 1 est toujours Infinity, et ainsi de suite. Cependant, ne faites pas trop confiance aux calculs basés sur l’infini. Cela n'est pas mathématiquement correct, et cela mènera rapidement au prochain nombre spécial : NaN.

NaN signifie « not a number » (pas un nombre), cependant c'est une valeur de type nombre. Vous obtiendrez ce résultat quand, par exemple, vous essayez de calculer 0 / 0 (zéro divisé par zéro), Infinity - Infinity, ou n'importe quelle autre opération numérique qui ne retourne pas de résultat significatif.

II-C - Chaînes

Le type de donnée de base suivant est la chaîne (string). Les chaînes sont utilisées pour représenter du texte. Elles sont écrites en encadrant leur contenu avec des guillemets.

1. `Down on the sea`

2. "Lie on the ocean"

3. 'Float on the ocean'

Vous pouvez utiliser des guillemets simples, des guillemets doubles, ou des guillemets obliques pour marquer les chaînes, du moment que le début et la fin de la chaîne concordent.

Presque tout peut être mis entre guillemets, et JavaScript en fera une chaîne. Mais quelques caractères sont plus difficiles. Vous pouvez imaginer comment mettre des guillemets entre guillemets peut être compliqué. Les sauts de ligne (les caractères que vous avez quand vous pressez ENTER) peuvent être inclus sans échappement seulement si la chaîne est entre guillemets obliques (``).

Pour rendre possible d'inclure de tels caractères dans une chaîne, la notation suivante est utilisée : dès qu'un antislash (\) est trouvé dans un texte entre guillemets, il indique que le caractère suivant a une signification spéciale. Ceci est appelé échapper le caractère. Un guillemet qui est précédé par un antislash ne terminera pas la chaîne, mais en fera partie. Quand un caractère n apparaît après un antislash, il est interprété comme un saut de ligne. De façon similaire, un t après un antislash signifie un caractère tabulation. Prenez la chaîne suivante :

1. "This is the first line\nAnd this is the second"

Le texte réel contenu est :

1. This is the first line

2. And this is the second

Il y a, bien sûr, des situations où vous voulez qu'un antislash dans une chaîne soit juste un antislash, pas un code spécial. Si deux antislashs se succèdent, elles s'effondrent ensemble, et il n'en restera qu'une seule dans la valeur de la chaîne résultante. C'est ainsi que la chaîne de caractères "Un caractère de nouvelle ligne est écrit comme "\n"." peut être exprimée :

1. "Un caractère de nouvelle ligne est écrit comme \"\\n\"."

Les chaînes, aussi, doivent être modelées comme des séries de bits pour pouvoir exister dans l'ordinateur. La façon dont JavaScript le fait est basée sur la norme Unicode. Cette norme assigne virtuellement un nombre à chaque caractère dont vous pourrez avoir besoin, incluant les caractères du grec, arabe, japonais, arménien, et ainsi de suite. Si nous avons un nombre pour chaque caractère, une chaîne peut être décrite comme une séquence de nombres.

Et c'est ce que JavaScript fait. Mais il y a une complication : la représentation JavaScript utilise 16 bits par élément de chaîne, ce qui décrit jusqu'à caractères différents. Mais Unicode définit plus de caractères que cela — à peu près deux fois plus, à ce stade. Donc certains caractères, tels que beaucoup d’émoticônes, prennent jusqu'à deux « positions de caractère » dans les chaînes JavaScript. Nous y reviendrons au Chapitre 5.

Les chaînes ne peuvent pas être divisées, multipliées, ou soustraites, mais l’opérateur + peut être utilisé sur elles. Il n'additionne pas, mais fait plutôt une concaténation — il colle deux chaînes ensemble. La ligne suivante produira la chaîne "concatène" :

1. "con" + "cat" + "è" + "ne"

Les valeurs chaîne ont un nombre de fonctions (méthodes) qui leur sont associées et qui peuvent être utilisées pour réaliser des opérations sur elles. J'en dirai plus à propos de celles-ci dans le Chapitre 4.

Les chaînes écrites avec des guillemets simples ou doubles se comportent de façon très similaire — la seule différence est le type de guillemet que vous avez besoin d’échapper à l’intérieur. Les chaînes avec guillemets obliques, habituellement appelées littéraux de gabarits, peuvent faire quelques trucs supplémentaires. En dehors de pouvoir étendre des lignes, elles peuvent aussi intégrer d'autres valeurs.

1. console.log(`la moitié de 100 est ${100 / 2}`)

Quand vous écrivez quelque chose à l’intérieur de ${} dans un littéral de gabarit, son résultat sera calculé, converti en chaîne, et inclus à cette position. Cet exemple produit "la moitié de 100 est 50".

II-D - Opérateurs unaires

Tous les opérateurs unaires ne sont pas des symboles. Certains sont écrits avec des mots. Un exemple est l’opérateur typeof qui produit une valeur chaîne nommant le type de la valeur que vous lui avez donnée.

1. console.log(typeof 4.5)

2. // → number

3. console.log(typeof "x")

4. // → string

Nous utiliserons console.log dans le code de l'exemple pour indiquer que nous voulons voir le résultat de quelque chose qui a été évalué. Plus d'information dans le chapitre suivant.

Les opérateurs qui ont été présentés opèrent tous avec deux valeurs, mais typeof n'en prend qu'une seule. Les opérateurs qui utilisent deux valeurs sont appelés opérateurs binaires, alors que ceux qui en prennent une sont appelés opérateurs unaires. L’opérateur moins peut être utilisé à la fois comme un opérateur binaire et comme un opérateur unaire.

1. console.log(- (10 - 2))

2. // → -8

II-E - Valeurs booléennes

Il est souvent utile d'avoir une valeur qui distingue entre deux possibilités telles que « oui » et « non », ou « on » et « off ». Pour ce faire, JavaScript a un type booléen qui a seulement deux valeurs, true (vrai) et false (faux) qui sont écrites telles quelles.

II-E-1 - Comparaison

Voici une façon de produire des valeurs booléennes :

1. console.log(3 > 2)

2. // → true

3. console.log(3 < 2)

4. // → false

Les signes > et < sont respectivement les symboles traditionnels pour « plus grand que » et « plus petit que ». Ce sont des opérateurs binaires. De leur application résulte une valeur booléenne indiquant s'ils sont vrais ou non.

Les chaînes peuvent être comparées de la même façon.

1. console.log("Aardvark" < "Zoroaster")

2. // → true

La manière dont les chaînes sont ordonnées est grosso modo alphabétique, mais pas vraiment ce que vous vous attendriez à voir dans un dictionnaire : les lettres majuscules sont toujours « moins » que les minuscules, donc "Z" < "a", et les caractères non alphabétiques (!,-, et autres) sont aussi inclus dans l'ordre. Lors de la comparaison de chaînes, JavaScript parcourt les caractères de gauche à droite, comparant les codes Unicode un par un.

Les autres opérateurs similaires sont >= (plus grand ou égal à), <= (plus petit ou égal à), == (égal à), et != ( différent de ).

1. console.log("Itchy" != "Scratchy")

2. // → true

3. console.log("Pomme" == "Orange")

4. // → false

Il y a seulement une valeur en JavaScript qui n'est pas égale à elle-même, et c'est NaN (« not a number » qui signifie en français « pas un nombre »).

1. console.log(NaN == NaN)

2. // → false

NaN est supposé représenter le résultat d'un calcul absurde, et en tant que tel, il n'est pas égal au résultat de quel qu'autre calcul absurde.

II-E-2 - Opérateurs logiques

Il y a aussi quelques opérations qui peuvent être appliquées aux valeurs booléennes elles-mêmes. JavaScript supporte trois opérateurs logiques : et, ou et non. Ils peuvent être utilisés pour « raisonner » à propos des booléens.

L’opérateur && représente la logique et. C'est un opérateur binaire, et son résultat est vrai (true) seulement si les deux valeurs qui lui sont données sont vraies.

1. console.log(true && false)

2. // → false

3. console.log(true && true)

4. // → true

L’opérateur || représente la logique ou. Il retourne vrai (true) si l'une ou l'autre des valeurs qui lui sont données est vraie.

1. console.log(false || true)

2. // → true

3. console.log(false || false)

4. // → false

Non est écrit avec un point d'exclamation (!). C'est un opérateur unaire qui inverse la valeur qui lui est passée —!true retourne false et !false retourne true.

Lorsque l'on mélange ces opérateurs booléens avec les opérateurs arithmétiques et les autres, il n'est pas toujours évident de savoir quand les parenthèses sont nécessaires. En pratique, vous devez connaître l’ordre de priorité des opérateurs que nous avons vu jusque-là, || a la précédence la plus basse, puis vient &&, et puis les opérateurs de comparaison (>,==, ainsi de suite), et puis le reste. Cet ordre a été choisi de telle manière que, dans une expression typique comme la suivante, aussi peu de parenthèses que possible sont nécessaires :

1. 1 + 1 == 2 && 10 * 10 > 50

Le dernier opérateur logique dont je vais discuter n'est ni unaire, ni binaire, mais ternaire, opérant sur trois valeurs. Il est écrit avec un point d'interrogation et un deux-points, comme suit :

1. console.log(true ? 1 : 2);

2. // → 1

3. console.log(false ? 1 : 2);

4. // → 2

Celui-ci est appelé opérateur conditionnel (ou quelquefois juste opérateur ternaire puisque c'est le seul opérateur de ce type dans le langage). La valeur à gauche du point d'interrogation « choisit » laquelle des deux autres valeurs sera retournée. Quand elle est vraie, elle choisit la valeur du milieu, et quand elle est fausse, elle choisit la valeur de droite.

II-F - Valeurs vides

Il y a deux valeurs spéciales, écrites null et undefined, qui sont utilisées pour dénoter l'absence de valeur significative. Elles sont elles-mêmes des valeurs, mais elles ne comportent pas d'information.

Beaucoup d’opérations du langage qui ne produisent pas de valeur significative (vous verrez cela plus tard) retournent undefined simplement parce qu'elles doivent retourner une valeur.

La différence de sens entre undefined et null est un accident de conception de JavaScript, et cela n'a pas d'importance la plupart du temps. Dans les cas où vous êtes effectivement concerné par ces valeurs, je recommande de les considérer comme interchangeables.

II-G - Conversion de type automatique

Dans l'Introduction, j'ai mentionné que JavaScript se met en quatre pour accepter presque tous les programmes que vous lui donnez, même ceux qui font des choses bizarres. Ceci est bien démontré avec les expressions suivantes :

1. console.log(8 * null)

2. // → 0

3. console.log("5" - 1)

4. // → 4

5. console.log("5" + 1)

6. // → 51

7. console.log("five" * 2)

8. // → NaN

9. console.log(false == 0)

10. // → true

Quand un opérateur est appliqué sur le « mauvais » type de valeur, JavaScript va discrètement convertir cette valeur dans le type dont il a besoin, utilisant un jeu de règles qui souvent ne sont pas celles que vous voulez ou attendez. C'est appelé « conversion de type ». Le null de la première expression devient 0, et le "5" de la seconde expression devient 5 (d'une chaîne à un nombre). Mais dans la troisième expression, + essaye de concaténer avant de faire l’opération numérique, donc le 1 est converti en "1" (d'un nombre à une chaîne).

La conversion d’une valeur qui ne correspond pas à un nombre de façon évidente (tel que "five" ou undefined) en un nombre, donne la valeur NaN. De plus, les opérations arithmétiques sur NaN continuent de produire un NaN, donc si vous vous retrouvez à obtenir une valeur NaN à un endroit inattendu, regardez au niveau des conversions de type accidentelles.

Lors de la comparaison de valeurs de même type utilisant ==, le résultat est facile à prédire : vous devriez obtenir true quand les deux valeurs sont les mêmes, sauf dans le cas de NaN. Mais si les types diffèrent, JavaScript utilise un jeu de règles compliqué et confus pour déterminer quoi faire. Dans la plupart des cas, il essaie juste de convertir une des valeurs dans le type de l'autre valeur. Cependant, quand null ou undefined apparaissent d'un côté ou de l'autre de l’opérateur, il produit true seulement si les deux côtés sont soit null ou undefined.

1. console.log(null == undefined);

2. // → true

3. console.log(null == 0);

4. // → false

Ce comportement est souvent utile. Quand vous voulez tester si une valeur a une vraie valeur plutôt que null ou undefined, vous pouvez la comparer à null avec l’opérateur == (ou !=).

Mais que faire si vous voulez tester si quelque chose réfère précisément à la valeur false ? Des expressions comme 0 == false et "" == false sont aussi vraies à cause de la conversion de type automatique. Quand vous ne voulez aucune conversion de type se produire, il y a deux opérateurs additionnels : === et !===. Le premier teste si une valeur est précisément égale à l'autre, et le second teste si elle est précisément non égale. Donc "" === false est faux comme attendu.

Je recommande d'utiliser les opérateurs de comparaison à trois caractères de manière défensive pour éviter d’être piégé par les conversions de type inattendues. Mais quand vous êtes certain que les types de chaque côté sont les mêmes, il n'y a pas de problème à utiliser les opérateurs plus courts.

II-G-1 - Court-circuiter les opérateurs logiques

Les opérateurs && et || gèrent les valeurs de types différents d'une façon particulière. Ils convertiront la valeur à leur gauche en type booléen afin de décider quoi faire, mais en fonction de l’opérateur et du résultat de cette conversion, ils retourneront soit la valeur d’origine de gauche ou la valeur de droite.

Par exemple, l’opérateur || retournera la valeur à sa gauche quand elle peut être convertie en true, sinon, il retournera la valeur de droite. Cela a l'effet attendu quand les valeurs sont booléennes et fait quelque chose d'analogue pour les valeurs d'autres types.

1. console.log(null || "user")

2. // → user

3. console.log("Agnes" || "user")

4. // → Agnes

Nous pouvons utiliser cette fonctionnalité comme un moyen de revenir à une valeur par défaut. Si vous avez une valeur qui peut être vide, vous pouvez mettre || après avec sa valeur de remplacement. Si la valeur initiale peut être convertie en false, vous aurez la valeur de remplacement. Les règles pour convertir les chaînes et les nombres en booléens définissent que 0, NaN, et la chaîne vide ("") comptent comme false, alors que toutes les autres valeurs comptent comme true. Donc 0 || -1 produit -1, et "" || "!?" retourne "!?".

L’opérateur && fonctionne de la même façon, mais dans l'autre sens. Quand la valeur à sa gauche est quelque chose qui se convertit en false, il retourne cette valeur, sinon il retourne la valeur à sa droite.

Une autre propriété importante de ces deux opérateurs est que la partie à leur droite est évaluée seulement quand cela est nécessaire. Dans le cas de true || X quel que soit X — même si c'est une partie de programme qui fait quelque chose de terrible — le résultat sera vrai, et donc X n'est jamais évalué. De même pour false && X qui est faux et qui ignore X. Ceci est appelé évaluation en court-circuit.

L’opérateur conditionnel fonctionne d'une manière similaire. Parmi la seconde et la troisième valeur, seule celle qui est sélectionnée est évaluée.

II-H - Résumé

Nous avons vu quatre types de valeur JavaScript dans ce chapitre : nombres, chaînes, booléens, et valeurs non définies.

De telles valeurs sont créées en tapant leur nom (true, null) ou valeur (13, "abc"). Vous pouvez combiner et transformer des valeurs avec des opérateurs. Nous avons vu les opérateurs binaires pour l’arithmétique (+, -, *, / et %), la concaténation de chaîne (+), la comparaison (==, !=, ===, !==, <, >, <=, >=), et la logique (&&, || ) , de même que plusieurs opérateurs unaires (- pour opposer un nombre, ! pour opposer logiquement, et typeof pour trouver le type d'une valeur) et un opérateur ternaire (?:) pour choisir une valeur parmi deux à partir d'une troisième valeur.

Cela vous donne assez d'information pour utiliser JavaScript comme une calculatrice de poche, mais pas beaucoup plus. Le prochain chapitre commencera à intégrer ces expressions ensemble pour faire des programmes de base.

III - Structure du Programme

« Et mon cœur brille d'une lumière rouge vif sous ma peau translucide et ils doivent m'administrer 10cc de JavaScript pour me faire revenir. (Je réagis bien aux toxines dans le sang) Mec, ce truc ferait bien sauter les pêches sur vos branchies ! »
— why, Why's (Poignant) Guide to Ruby

Dans ce chapitre, nous allons commencer à faire des trucs qui peuvent être réellement appelés programmation. Nous allons élargir notre maîtrise du langage JavaScript au-delà des noms et fragments de phrases que nous avons vus jusqu'à présent, au point de pouvoir exprimer une prose significative.

III-A - Expressions et déclarations

Dans le Chapitre 1, nous avons créé des valeurs et leur avons appliqué des opérateurs pour obtenir de nouvelles valeurs. Créer des valeurs de cette façon est la substance principale de n'importe quel programme JavaScript. Mais cette substance doit être encadrée d'une structure plus large pour être utile. Donc, c'est ce que nous allons couvrir dans la suite.

Un fragment de code qui produit une valeur est appelé une expression. Toute valeur qui est écrite littéralement (telle que 22 ou "psychanalyse") est une expression. Une expression entre parenthèses est aussi une expression, tout comme l'est un opérateur binaire appliqué à deux expressions, ou un opérateur unitaire appliqué à une seule.

Cela montre une partie de la beauté d'une interface basée sur le langage. Des expressions peuvent contenir d'autres expressions d'une manière similaire à l'imbrication des sous-phrases dans les langages humains — une sous-phrase peut contenir elle-même des sous-phrases et ainsi de suite. Cela nous permet de construire des expressions qui décrivent de manière arbitraire des calculs complexes.

Si une expression correspond à un fragment de phrase, une instruction JavaScript correspond à une phrase complète. Un programme est une liste d'instructions.

Le type le plus simple d'instruction est une expression terminée par un point-virgule. Ceci est un programme :

1. 1;

2. !false;

Cependant, c'est un programme inutile. Une expression peut se contenter de produire une valeur, qui pourra ensuite être utilisée par le code englobant l'expression. Une instruction est autonome, elle ne compte pour quelque chose que si elle affecte le monde. Elle peut afficher quelque chose sur l’écran — cela compte pour changer le monde — ou elle peut changer l’état interne de la machine d'une manière qui affectera les instructions qui viennent après elle. Ces changements sont appelés effets secondaires. Les instructions de l'exemple précédent produisent juste les valeurs 1 et true et s'en débarrassent immédiatement après. Cela ne laisse absolument aucune trace dans le monde. Quand vous exécutez ce programme, rien d'observable ne se produit.

Dans certains cas, JavaScript vous permet d'omettre le point-virgule à la fin d'une instruction ; dans les autres cas, il doit être présent, sinon la prochaine ligne sera traitée comme faisant partie de la même instruction. Les règles permettant d’omettre le point-virgule en toute sécurité sont quelque peu complexes et sources d'erreurs. Donc, dans ce livre, chaque instruction qui a besoin d'un point-virgule en aura toujours un. Je vous recommande de faire de même, au moins jusqu'à ce que vous en sachiez plus à propos des subtilités des points-virgules absents.

III-B - Variables

Comment est-ce qu'un programme conserve un état interne ? Comment est-ce qu'il se souvient de quelque chose ? Nous avons vu comment produire de nouvelles valeurs à partir d'anciennes valeurs, mais cela ne change pas les anciennes valeurs, et la nouvelle valeur doit être immédiatement utilisée ou elle va à nouveau disparaître. Pour attribuer et mémoriser des valeurs, JavaScript fournit les instructions var, let et const. Elles permettent d’affecter une valeur à une variable.

1. let caught = 5 * 5;

C'est un second type d'instruction. Le mot spécial (mot-clef) let indique que cette phrase va définir une variable. Il est suivi par le nom de la variable et, si on veut immédiatement lui donner une valeur, par un opérateur = et une expression.

L'instruction précédente crée une variable appelée caught et l'utilise pour capturer le nombre qui est produit en multipliant 5 par 5.

Après avoir défini l’affectation, son nom peut être utilisé comme une expression. La valeur d'une telle expression est la valeur que la variable contient à ce moment. Voici un exemple :

1. let ten = 10;

2. console.log(ten * ten);

3. // → 100

Quand une variable pointe vers une valeur, cela ne signifie pas qu'elle est liée à cette valeur pour toujours. L’opérateur = peut être utilisé à n'importe quel moment sur les variables existantes pour les déconnecter de leur valeur courante et les faire pointer vers une nouvelle.

1. let mood = "light";

2. console.log(mood);

3. // → light

4. mood = "dark";

5. console.log(mood);

6. // → dark

Vous pouvez imaginer les variables comme des tentacules, plutôt que des boîtes. Les tentacules ne contiennent pas de valeurs ; ils s'en saisissent — deux variables peuvent référer à la même valeur. Un programme accède uniquement aux valeurs pour lesquelles il a une référence. Quand vous avez besoin de vous souvenir de quelque chose, vous faites pousser un tentacule pour le saisir ou vous y réattachez un de vos tentacules existants.

Regardons un autre exemple. Pour vous souvenir du nombre d'euros que Luigi vous doit, vous créez une variable. Et, quand il vous rembourse 35 €, vous donnez une nouvelle valeur à cette variable.

1. let luigisDebt = 140;

2. luigisDebt = luigisDebt - 35;

3. console.log(luigisDebt);

4. // → 105

Quand vous définissez une variable sans lui donner de valeur, le tentacule n'a rien dont il puisse se saisir donc il finit dans le vide. Si vous demandez la valeur d’une variable vide, vous obtiendrez la valeur undefined.

Une seule instruction let peut définir plusieurs variables. Les définitions doivent être séparées par des virgules.

1. let one = 1, two = 2;

2. console.log(one + two);

3. // → 3

Les mots var et const peuvent aussi être utilisés pour créer des variables, d'une manière similaire à let.

1. var name = "Ayda";

2. const greeting = "Hello ";

3. console.log(greeting + name);

4. // → Hello Ayda

Le premier, var (raccourci pour « variable »), est la manière de déclarer les variables en JavaScript avant 2015. Je reviendrai sur la façon précise dont il diffère de let dans le prochain chapitre. Pour le moment, souvenez-vous qu'il fait presque la même chose, mais nous allons rarement l'utiliser dans ce livre, car il a quelques propriétés qui prêtent à confusion.

Le mot const signifie constante. Il définit une variable constante qui pointe vers la même valeur aussi longtemps qu'il est en vie. C'est utile pour les variables qui donnent un nom à une valeur afin que vous puissiez vous y référer aisément plus tard.

III-C - Noms de variables

Les noms de variables peuvent être n'importe quel mot. Des chiffres peuvent faire partie des noms de variables — par exemple, catch22 est un nom valide — mais le nom ne doit pas commencer par un chiffre. Un nom de variable peut inclure des signes dollars ($) ou des tirets bas (_), mais pas d'autres caractères de ponctuation ou caractères spéciaux.

Les mots avec une signification particulière, tels que let, sont des mots-clefs (keywords) et ils ne doivent pas être utilisés comme nom de variable. Il y a aussi un certain nombre de mots qui sont « réservés pour être utilisés » dans de futures versions de JavaScript et qui ne peuvent pas être utilisés comme nom de variable. La liste complète des mots-clefs et des mots réservés est plutôt longue.

1. break case catch class const continue debugger default

2. delete do else enum export extends false finally for

3. function if implements import interface in instanceof let

4. new package private protected public return static super

5. switch this throw true try typeof var void while with yield

Ne vous souciez pas de mémoriser cette liste. Lorsque la création d'une variable génère une erreur de syntaxe inattendue, vérifiez si vous essayez de définir un mot réservé.

III-D - Environnement

La collection des variables qui existent à un moment donné et de leur valeur est appelée l'environnement. Quand un programme démarre, cet environnement n'est pas vide. Il contient toujours des variables qui font partie du langage et, la plupart du temps, il a aussi des variables qui fournissent des moyens d'interagir avec le système environnant. Par exemple, dans un navigateur, il y a des fonctions pour interagir avec le site web actuellement chargé et pour lire les entrées de la souris et du clavier.

III-E - Fonctions

Un grand nombre des valeurs fournies dans l'environnement par défaut sont de type fonction. Une fonction est un morceau de programme encapsulé dans une valeur. De telles valeurs peuvent être appliquées afin d’exécuter le programme encapsulé. Par exemple, dans un environnement de navigateur, la variable prompt contient une fonction qui affiche une petite boîte de dialogue demandant une entrée utilisateur. Il est utilisé ainsi :

1. prompt("Entrer le mot de passe");

Le fait d’exécuter une fonction est désigné par les mots : invoquer, appeler ou appliquer une fonction. Vous pouvez appeler une fonction en mettant des parenthèses après une expression qui produit une valeur de fonction. Habituellement, vous utiliserez directement le nom de la variable qui contient la fonction. Les valeurs entre les parenthèses sont données au programme à l'intérieur de la fonction. Dans l'exemple, la fonction prompt utilise la chaîne que nous lui donnons en tant que texte à afficher. Les valeurs passées à des fonctions sont appelées arguments. Différentes fonctions peuvent avoir besoin d'un nombre différent ou de différents types d'arguments.

La fonction prompt n'est pas très utilisée dans la programmation web moderne, principalement parce que vous n'avez pas de contrôle sur l'apparence des boîtes de dialogue, mais cela peut être utile pour de courts programmes ou des expérimentations.

III-F - Fonction console.log

Dans les exemples, j'ai utilisé console.log pour afficher des valeurs. La plupart des systèmes JavaScript (incluant tous les navigateurs web modernes et Node.js) fournissent une fonction console.log qui retranscrit ses arguments sur un périphérique de sortie texte. Dans les navigateurs, la sortie arrive dans la console JavaScript. Cette partie de l'interface du navigateur est masquée par défaut, mais la plupart des navigateurs l'ouvrent lorsque vous pressez F12 ou, sur un Mac, COMMAND-OPTION-I. Si cela ne fonctionne pas, cherchez dans les menus un item nommé « Developer Tools » ou portant un nom similaire.

Lors de l'exécution des exemples (ou de votre propre code) dans les pages de ce livre, la sortie console.log sera affichée après l'exemple, au lieu de la console JavaScript du navigateur.

1. let x = 30;

2. console.log("la valeur de x est", x);

3. // → la valeur de x est 30

Bien que les noms de fonctions ne puissent pas contenir de caractères point (.), console.log en a un. Ceci est dû au fait que console.log n'est pas une simple fonction. C'est en fait une expression qui retrouve la propriété log de la valeur contenue par la fonction console. Nous verrons exactement de quoi il en retourne au Chapitre 4.

III-G - Valeurs de retour

Afficher une boîte de dialogue ou afficher du texte à l'écran est un effet secondaire. Beaucoup de fonctions sont utiles à cause des effets secondaires qu'elles produisent. Des fonctions peuvent aussi produire des valeurs, auquel cas elles n'ont pas besoin d'avoir un effet secondaire pour être utiles. Par exemple, la fonction Math.max prend n'importe quelle quantité de nombres en argument et renvoie le plus grand.

1. console.log(Matx.max(2,4));

2. // → 4

Quand une fonction produit une valeur, on dit qu'elle retourne cette valeur. N'importe quoi produisant une valeur est une expression en JavaScript, ce qui signifie que les appels aux fonctions peuvent être utilisés dans des expressions plus grandes. Ici un appel à Math.min, qui est l'opposé de Math.max, est utilisé comme une composante d'une expression plus (+).

1. console.log(Math.min(2, 4) + 100);

2. // → 102

Le prochain chapitre explique comment écrire vos propres fonctions.

III-H - Flux de contrôle

Quand votre programme contient plusieurs instructions, elles sont exécutées comme s'il s'agissait d'une histoire, de haut en bas. Cet exemple de programme a deux instructions. La première demande un nombre à l'utilisateur, et la seconde, qui est exécutée après la première, affiche le carré de ce nombre.

1. let theNumber = Number(prompt("Choissez un nombre"));

2. console.log("Votre nombre est la racine carrée de " +

3. theNumber * theNumber);

La fonction Number convertit une valeur en un nombre. Nous avons besoin de cette conversion, car le résultat de prompt est une chaîne et nous voulons un nombre. Il y a des fonctions similaires appelées String et Boolean qui convertissent dans ces types.

Voici la représentation schématique, plutôt triviale, du flux de contrôle linéaire :

III-I - Exécution conditionnelle

Tous les programmes ne sont pas des routes droites. Nous pouvons, par exemple, vouloir créer un embranchement, où le programme prend la branche adéquate en fonction de la situation. Ceci est appelé exécution conditionnelle.

Une exécution conditionnelle est créée avec le mot-clef if en JavaScript. Dans le cas simple, nous voulons que du code soit exécuté si et seulement si, une certaine condition est satisfaite. Nous pouvons, par exemple, vouloir afficher le carré de l'entrée seulement si l'entrée est effectivement un nombre.

1. let theNumber = Number(prompt("Choissez un nombre"));

2. if (!Number.isNaN(theNumber)) {

3. console.log("Votre nombre est la racine carrée de " +

4. theNumber * theNumber);

5. }

Avec cette modification, si vous saisissez « perroquet », rien n'est affiché.

Le mot-clef if exécute ou saute une instruction en fonction de la valeur d'une expression booléenne. L'expression décisive est écrite après le mot-clef, entre parenthèses, suivi de l'instruction à exécuter.

La fonction Number.isNaN est une fonction JavaScript standard qui retourne true uniquement si l'argument qui lui est donné est NaN. La fonction Number retourne NaN quand vous lui donnez une chaîne qui ne représente pas un nombre valide. Et donc, la condition se traduit en « à moins que leNombre ne soit pas un nombre, fait ceci ».

L'instruction après le if est encadrée par des accolades ({ et }) dans cet exemple. Les accolades peuvent être utilisées pour grouper n'importe quel nombre d'instructions en une unique instruction, appelée bloc. Vous pourriez aussi les avoir omises dans ce cas, puisqu'elles ne contiennent qu'une seule instruction, mais pour éviter d'avoir à penser si elles sont nécessaires, la plupart des programmeurs JavaScript les utilisent avec chaque instruction encapsulée comme celle-ci. La plupart du temps, nous suivrons cette convention dans ce livre, excepté pour les occasionnelles lignes uniques.

1. if (1 + 1 == 2) console.log("C'est vrai");

2. // → C'est vrai

Souvent, vous n'avez pas seulement le code qui s'exécute quand une condition est vraie, mais aussi le code qui prend en charge l'autre cas. Cet autre chemin est représenté par la seconde flèche dans le diagramme. Vous pouvez utiliser le mot-clef else avec if, pour créer deux chemins d'exécution séparés.

1. let theNumber = Number(prompt("Choissez un nombre"));

2. if (!Number.isNaN(theNumber)) {

3. console.log("Votre nombre est la racine carrée de " +

4. theNumber * theNumber);

5. } else {

6. console.log("Hé. Pourquoi ne m'avez-vous pas donné un nombre ?");

7. }

Si vous devez choisir parmi plus de deux chemins, vous pouvez chaîner plusieurs paires if\else ensemble. Voici un exemple :

1. let num = Number(prompt("Choissez un nombre"));

3. if (num < 10) {

4. console.log("Petit");

5. } else if (num < 100) {

6. console.log("Moyen");

7. } else {

8. console.log("Grand");

9. }

Le programme va d'abord vérifier si num est plus petit que 10. Si c'est le cas, il choisit cette branche, affiche « Petit » et c'est fini. Si ce n'est pas le cas, il prend la branche else, qui elle même contient un second if. Si la seconde condition (< 100) est satisfaite, cela signifie que le nombre est compris entre 10 et 100 et « Moyen » est affiché. Sinon, la deuxième et dernière branche else est choisie.

Le schéma pour ce programme ressemble à ceci :

III-J - Boucles while et do

Prenez un programme qui affiche tous les nombres pairs de 0 à 12. Une façon de l'écrire est la suivante :

1. console.log(0);

2. console.log(2);

3. console.log(4);

4. console.log(6);

5. console.log(8);

6. console.log(10);

7. console.log(12);

Ça fonctionne, mais l'idée d'écrire un programme est de faire quelque chose avec moins de travail et non pas plus. Si nous avons besoin de tous les nombres inférieurs à 1000, cette approche serait impraticable. Ce dont nous avons besoin, c'est une façon d'exécuter un morceau de code plusieurs fois. Cette forme de flux de contrôle est appelée une boucle.

La mise en boucle du flux de contrôle nous permet de revenir à un point donné dans le programme où nous étions avant et de le répéter avec l'état courant de notre programme. Si nous combinons cela avec une variable qui compte, nous pouvons faire quelque chose comme :

1. let number = 0;

2. while (number <= 12) {

3. console.log(number);

4. number = number + 2;

5. }

6. // → 0

7. // → 2

8. //   … etcetera

Une instruction avec le mot-clef while crée une boucle. Le mot while est suivi d'une expression entre parenthèses et puis d'une instruction, un peu comme if. La boucle continue d'entrer dans cette instruction aussi longtemps que l'expression produit une valeur qui vaut true quand elle est convertie en booléen.

La variable nombre démontre la manière selon laquelle une variable peut suivre la progression d'un programme. À chaque fois que la boucle se répète, nombre reçoit une valeur qui est 2 de plus que sa précédente valeur. Au début de chaque répétition, il est comparé avec le nombre 12 pour décider si le travail du programme est terminé.

En tant qu'exemple faisant effectivement quelque chose d'utile, nous pouvons maintenant écrire un programme qui calcule et affiche la valeur de (2 à la puissance de 10). Nous utilisons deux variables : une pour garder une trace de notre résultat et une pour compter combien de fois nous avons multiplié ce résultat par 2. La boucle vérifie si la seconde variable a déjà atteint 10, sinon, elle met à jour les deux variables.

1. let result = 1;

2. let counter = 0;

3. while (counter < 10) {

4. result = result * 2;

5. counter = counter + 1;

6. }

7. console.log(result);

8. // → 1024

Le compteur aurait aussi pu commencer à 1 et vérifier <=10, mais pour des raisons qui deviendront plus apparentes au Chapitre 4, c'est une bonne idée de s'habituer à compter à partir de 0.

Une boucle do est une structure de contrôle similaire à une boucle while. Elle diffère seulement sur un point : une boucle do exécute toujours son contenu au moins une fois et elle commencer à vérifier si elle doit s'arrêter seulement après la première exécution. Pour refléter cela, la vérification apparaît après le contenu de la boucle.

1. let yourName;

2. do {

3. yourName = prompt("Qui êtes-vous ?");

4. } while (!yourName);

5. console.log(yourName);

Ce programme vous force à saisir votre nom. Il va demander encore et encore jusqu'à ce qu'il reçoive quelque chose qui n'est pas une chaîne vide. Appliquer l'opérateur ! à une valeur la convertira en type booléen avant de l'opposer, et toute chaîne, sauf "", est convertie en true. Cela signifie que la boucle continue de tourner en rond jusqu'à ce que vous fournissiez un nom non-vide.

III-K - Indenter le code

Dans les exemples, j'ai ajouté des espaces devant les instructions qui font partie d'une instruction plus large. Ces espaces ne sont pas nécessaires — l'ordinateur acceptera le programme très bien sans eux. En fait, même les sauts de lignes sont optionnels. Vous pouvez écrire un programme sur une unique longue ligne si vous le sentez ainsi.

Le rôle de cette indentation dans les blocs est de faire apparaître la structure du code. Dans le code, où de nouveaux blocs sont ouverts dans d'autres blocs, il peut devenir difficile de voir où un bloc se termine et un autre commence. Avec une indentation correcte, la forme visuelle d'un programme correspond à la forme des blocs à l'intérieur. J'aime utiliser deux espaces pour chaque bloc ouvert, mais les goûts varient — certaines personnes utilisent quatre espaces, d'autres personnes utilisent des caractères de tabulation. Ce qui est important est que chaque nouveau bloc ajoute le même nombre d'espaces.

1. if (false != true) {

2. console.log("Cela fait sens.");

3. if (1 < 2) {

4. console.log("Pas de surprise ici.");

5. }

6. }

La plupart des éditeurs de code (incluant celui de ce livre) aideront à indenter automatiquement les nouvelles lignes avec le nombre correct d'espaces.

III-L - Boucles for

Beaucoup de boucles suivent le modèle présenté dans les exemples while. D'abord une variable « compteur » est créée pour suivre la progression de la boucle. Puis vient une boucle while, habituellement avec une expression de test qui vérifie si le compteur à atteint sa valeur de fin. À la fin du corps de la boucle, le compteur est mis à jour pour suivre la progression.

Parce que ce modèle est si commun, JavaScript et les langages similaires fournissent un format légèrement plus court et plus compréhensible : la boucle for.

1. for (let number = 0; number <= 12; number = number + 2) {

2. console.log(number);

3. }

4. // → 0

5. // → 2

6. //   … etcetera

Ce programme est exactement équivalent à l'exemple précédent d'affichage des nombres pairs. Le seul changement est que toutes les instructions liées à l'état de la boucle sont groupées après for.

Les parenthèses après un mot-clef for doivent contenir deux points-virgules. La partie avant le premier point-virgule initialise la boucle, habituellement en définissant une variable. La seconde partie est l'expression qui vérifie si la boucle doit continuer. La dernière partie met à jour l'état de la boucle après chaque itération. Dans la plupart des cas, c'est plus court et plus clair qu'une construction while.

Voici le code qui calcule utilisant for à la place de while :

1. let result = 1;

2. for (let counter = 0; counter < 10; counter = counter + 1) {

3. result = result * 2;

4. }

5. console.log(result);

6. // → 1024

III-M - Sortir d'une boucle

Avoir la condition de bouclage produisant false n'est pas la seule manière de terminer une boucle. Il y a une instruction spéciale appelée break qui a pour effet d'immédiatement sortir de la boucle.

Ce programme illustre l'instruction break. Il cherche le premier nombre qui est à la fois plus grand ou égal à 20 et divisible par 7.

1. for (let current = 20; ; current = current + 1) {

2. if (current % 7 == 0) {

3. console.log(current);

4. break;

5. }

6. }

7. // → 21

Utiliser l'opérateur de reste (%) est une façon simple de tester si un nombre est divisible par un autre nombre. Si tel est le cas, le reste de la division vaut zéro.

La construction for dans l'exemple n'a pas la partie qui vérifie si la fin de la boucle est atteinte. Cela signifie que la boucle ne s'arrêtera jamais à moins que l'instruction break, à l'intérieur, soit exécutée.

Si vous deviez enlever cette instruction break ou accidentellement écrire une condition de fin qui produit toujours true, votre programme serait bloqué dans une boucle infinie. Un programme coincé dans une boucle infinie ne finira jamais son exécution, ce qui est généralement une mauvaise chose.

Si vous créez une boucle infinie dans l'un des exemples de ces pages, on vous demandera généralement si vous voulez arrêter le programme après quelques secondes. Si cela échoue, vous devrez fermer l'onglet dans lequel vous travaillez ou, sur certains navigateurs, fermer complètement le navigateur pour reprendre.

Le mot-clef continue est similaire à break, dans le fait qu'il influence la progression d'une boucle. Quand continue est rencontré dans le contenu d'une boucle, le contrôle saute le corps de la boucle et continue avec l'itération suivante.

III-N - Mettre à jour des variables de manière succincte

Lors de la mise en boucle, un programme doit souvent « mettre à jour » une variable pour conserver une valeur basée sur la valeur précédente de cette variable.

1. counter = counter + 1;

JavaScript fournit un raccourci pour cela.

1. counter += 1;

Des raccourcis similaires fonctionnent pour de nombreux autres opérateurs, tels que result *= 2 pour doubler result ou counter -=1 pour décrémenter.

Cela nous permet de raccourcir un peu plus notre exemple de comptage.

1. for (let number = 0; number <= 12; number += 2) {

2. console.log(number);

3. }

Pour counter += 1 et counter -=1, il y a même des équivalents plus courts : counter++ et counter--.

III-O - Répartir sur la base d'une valeur avec switch

Il n'est pas rare que du code ressemble à cela :

1. if (x == "valeur1") action1();

2. else if (x == "valeur2") action2();

3. else if (x == "valeur3") action3();

4. else actionDefaut();

Il y a une construction appelée switch qui est prévue pour exprimer une telle « répartition » d'une façon plus directe. Malheureusement, la syntaxe JavaScript utilisée pour cela (qui est héritée de la lignée des langages de programmation C/Java) n'est pas très pratique — une chaîne d'instructions if peut sembler meilleure. En voici un exemple :

1. switch (prompt("Quel temps fait-il ?")) {

2. case "pluvieux":

3. console.log("Pensez à prendre un parapluie.");

4. break;

5. case "ensoleillé":

6. console.log("Habillez-vous légèrement.");

7. case "nuageux":

8. console.log("Allez dehors.");

9. break;

10. default:

11. console.log("Type de temps inconnu !");

12. break;

13. }

Vous pouvez mettre n'importe quel nombre de labels case dans le bloc ouvert part switch. Le programme commencera l'exécution à partir du label correspondant à la valeur qui a été passée à switch ou à partir de default si aucune valeur correspondante n'a été trouvée. Il continuera l'exécution, même au travers d'autres labels, jusqu'à ce qu'il atteigne une instruction break. Dans certains cas, tels que le cas « ensoleillé » de cet exemple, cela peut être utilisé pour partager du code entre les options (il recommande d'aller dehors pour le temps ensoleillé et le temps nuageux). Mais faites attention, il est facile d'oublier un tel break, ce qui obligera le programme à exécuter du code que vous ne voulez pas exécuter.

III-P - Capitalisation

Les noms de variables ne peuvent pas contenir d'espace, cependant, il est souvent pratique d'utiliser plusieurs mots pour décrire clairement ce que la variable représente. Voici à peu près vos choix pour écrire un nom de variable avec plusieurs mots :

1. petitetortuefloue

2. petite_tortue_floue

3. PetiteTortueFloue

4. petiteTortueFloue

Le premier style peut être difficile à lire. Je préfère plutôt l'aspect des tirets-bas, bien que ce style soit un peu pénible à taper. Les fonctions JavaScript standard et la plupart des développeurs JavaScript suivent le style du bas : ils mettent en majuscule la première lettre de tous les mots sauf le premier. Il n'est pas difficile de s'habituer à de petites choses comme ça, et du code avec des styles de nommage différents peut être perturbant à lire, donc nous suivons cette convention.

Dans quelques cas, tels que la fonction Number, la première lettre d'une variable est aussi en majuscule. Cela a été fait pour marquer cette fonction comme étant un constructeur. Ce qu'est un constructeur deviendra plus clair dans le Chapitre 6. Pour le moment, l'important est de ne pas être dérangé par le manque apparent de cohérence.

III-Q - Commentaires

Souvent, le code brut n'exprime pas toute l'information que vous voulez qu'un programme transmette aux lecteurs humains ou bien il l'exprime d'une manière tellement cryptique que les gens ne pourront pas comprendre. À d'autres moments, vous voudrez peut-être juste inclure des idées connexes dans votre programme. C'est ce pour quoi les commentaires sont faits.

Un commentaire est un morceau de texte qui fait partie d'un programme, mais il est complètement ignoré par l'ordinateur. JavaScript a deux manières pour écrire les commentaires. Pour écrire un commentaire en une seule ligne, vous pouvez utiliser deux caractères barre oblique (//) puis le texte de commentaire.

1. let accountBalance = calculateBalance(account);

2. // C'est un trou de verdure où chante une rivière

3. accountBalance.adjust();

4. // Accrochant follement aux herbes des haillons d'argent

5. let report = new Report();

6. // Où le soleil, de la montagne fière, luit

7. addToReport(accountBalance, report);

8. // C'est un petit val qui mousse de rayons.

Un commentaire // va uniquement jusqu'à la fin de la ligne. Une section de texte comprise entre /* et */ sera ignorée complètement, quand bien même elle contiendrait des sauts de ligne. C'est utile pour ajouter des blocs d'information à propos d'un fichier ou d'un bout de programme.

1. /*

2. J'ai d'abord trouvé ce numéro griffonné au dos d'un vieux carnet de notes.

3. Depuis lors, il est souvent revenu, apparaissant parmi les numéros de téléphone

4. et les numéros de série des produits que j'ai achetés. Il est évident qu'il m'aime,

5. donc j'ai décidé de le garder.

6. */

7. const myNumber = 11213;

III-R - Résumé

Vous savez maintenant qu'un programme est fait à partir d'instructions, qui elles-mêmes contiennent quelques fois d'autres instructions. Des instructions tendent à contenir des expressions, qui elles-mêmes peuvent être construites à partir d'expressions plus petites.

Mettre des instructions les unes derrière les autres vous donne un programme qui est exécuté de haut en bas. Vous pouvez introduire des perturbations dans le flux de contrôle en utilisant des instructions conditionnelles (if, else et switch) et des instructions de boucle (while, do et for).

Des variables peuvent être utilisées pour mettre un nom sur des morceaux de données et elles sont utiles pour suivre l'état de votre programme. L'environnement est l'ensemble des variables qui sont définies. Les systèmes JavaScript ajoutent toujours un certain nombre de variables standard utiles dans votre environnement.

Les fonctions sont des valeurs spéciales qui encapsulent un morceau de programme. Vous pouvez les invoquer en écrivant nomFonction(argument1, argument2). Un tel appel de fonction est une expression et peut produire une valeur.

III-S - Exercices

Si vous n'êtes par sûr de comment tester vos solutions aux exercices, référez-vous à l' Introduction.

Chaque exercice commence avec une description du problème. Lisez cette description et essayez de résoudre l'exercice. Si vous rencontrez des difficultés, envisagez de lire les astuces après l’exercice. Les solutions complètes ne sont pas incluses dans ce livre, mais vous pouvez les trouver en ligne à l'adresse https://eloquentjavascript.net/code. Si vous voulez apprendre quelque chose des exercices, je recommande de regarder les solutions seulement après que vous aurez résolu l'exercice, ou au moins après que vous l'aurez attaqué assez longtemps et avec assez d'insistance pour avoir un léger mal de tête.

III-S-1 - Mettre un triangle en boucle

Écrivez un programme qui effectue sept appels à console.log pour afficher le triangle suivant :

1. #

2. ##

3. ###

4. ####

5. #####

6. ######

7. #######

Il sera utile de savoir que vous pouvez trouver la longueur d'une chaîne en écrivant .length après celle-ci.

1. let abc = "abc";

2. console.log(abc.length);

3. // → 3

La plupart des exercices contiennent un morceau de code que vous pouvez modifier pour résoudre l'exercice. Souvenez-vous que vous pouvez cliquer sur les blocs de code pour les éditer.

1. // Votre code ici

Vous pouvez commencer avec un programme qui affiche les nombres 1 à 7, que vous pouvez obtenir en faisant quelques modifications à l'exemple d'affichage des nombres pairs donné plus tôt dans ce chapitre, là où la boucle for a été introduite. Maintenant, considérez l'équivalence entre nombres et chaînes de caractères dièse. Vous pouvez aller de 1 à 2 en ajoutant 1 (+= 1). Vous pouvez aller de « # » à « ## » en ajoutant un caractère (+= "#") . Et donc, votre solution peut suivre de près le programme d'affichage de nombres.

III-S-2 - FizzBuzz

Écrivez un programme qui utilise console.log pour afficher tous les nombres de 1 à 100, avec deux exceptions. Pour les nombres divisibles par 3, affichez « Fizz » au lieu du nombre et pour les nombres divisibles par 5 (et non par 3), affichez « Buzz » à la place.

Quand avez fait fonctionner ça, modifiez votre programme pour afficher « FizzBuzz » pour les nombres qui sont à la fois divisibles par 3 et 5 (et toujours afficher « Fizz » ou « Buzz » pour les nombres divisibles uniquement par un de ces nombres).

C'est en fait un entretien d’embauche censé éliminer un large pourcentage de candidats programmeurs. Donc, si vous l'avez résolu, votre valeur sur le marché du travail vient juste d'augmenter.

1. // Votre code ici

Passer en revue les nombres est clairement un travail de boucle et sélectionner quoi afficher est une question d'exécution conditionnelle. Souvenez-vous du truc utilisant l'opérateur de reste (%) pour vérifier si un nombre est divisible par un autre (il a un reste à zéro).Dans la première version, il y trois possibilités pour chaque nombre, donc vous allez devoir créer une chaîne de if/else if else.

La seconde version du programme a une solution simple et une intelligente. La solution simple c'est d'ajouter une autre « branche » conditionnelle pour précisément tester la condition donnée. Pour la solution intelligente, construisez une chaîne contenant le ou les mots à afficher et affichez soit ce mot soit le nombre s'il n'y a pas de mot, potentiellement en faisant bon usage de l'opérateur ||

III-S-3 - Plateau d'échecs

Écrivez un programme qui crée une chaîne qui représente une grille de 8×8, utilisant des caractères nouvelle ligne pour séparer les lignes. À chaque position de la grille, il y a soit un espace, soit un caractère « # ». Les caractères doivent former un plateau d'échecs.

Passer cette chaîne à console.log devrait afficher quelque chose comme :

1. # # # #

2. # # # #

3. # # # #

4. # # # #

5. # # # #

6. # # # #

7. # # # #

8. # # # #

Quand vous avez un programme qui génère ce motif, définissez une variable taille = 8 et modifiez le programme pour qu'il fonctionne avec n'importe quelle taille, affichant une grille de largeur et hauteur données.

Vous pouvez construire la chaîne en commençant avec une chaîne vide ("") et en ajoutant des caractères de façon répétitive. Un caractère nouvelle ligne s'écrit "\n". Pour travailler avec deux dimensions, vous allez avoir besoin d'une boucle dans une boucle. Mettez des accolades autour des corps de chaque boucle pour facilement voir où elles commencent et se terminent. Essayez d'indenter correctement le contenu des boucles.

L'ordre des boucles doit suivre l'ordre dans lequel nous allons construire la chaîne (ligne par ligne, de gauche à droite, de haut en bas). Donc, la boucle extérieure se charge des lignes, et la boucle intérieure se charge des caractères sur une ligne. Vous aurez besoin de deux variables pour suivre votre progression. Pour savoir s'il faut mettre un espace ou un dièse à une position donnée, vous pouvez tester si la somme des deux compteurs est paire (% 2).

Terminer une ligne en ajoutant un caractère nouvelle ligne doit être fait après que la ligne a été construite, donc le faire après la boucle intérieure, mais dans la boucle extérieure.

IV - Les fonctions

Les fonctions sont le pain et le beurre de la programmation JavaScript. Le concept d'envelopper une partie du programme dans une valeur a de nombreuses utilisations. Cela nous permet de structurer des programmes plus importants, de réduire les répétitions, d'associer des noms à des sous-programmes et d'isoler ces sous-programmes les uns des autres.

L'application la plus évidente des fonctions est la définition d'un nouveau vocabulaire. Créer de nouveaux mots en prose est généralement un mauvais style. Mais en programmation, c'est indispensable.

Les anglophones adultes typiques ont environ 20 000 mots dans leur vocabulaire. Peu de langages de programmation intègrent 20 000 commandes. Et le vocabulaire disponible tend à être défini plus précisément, et donc avec moins de souplesse, que dans le langage humain. Par conséquent, nous devons généralement introduire de nouveaux concepts pour éviter de nous répéter trop souvent.

IV-A - Définir une fonction

Une définition de fonction est une variable dont la valeur est une fonction.

Par exemple, ce code définit une fonction qui retourne le carré d'un nombre x  donné en paramètre :

1. const square = function (x) {

2. return x * x;

3. };

5. console.log(square(12));

6. // → 144

La déclaration d'une fonction commence par le mot-clef function (il existe une autre façon de les déclarer, que nous verrons plus tard). Les fonctions ont un ensemble de paramètres (dans ce cas, uniquement x) et un corps, qui contient les instructions à exécuter lors de l'appel de celle-ci. Le corps d'une fonction doit toujours être placé entre accolades, même s'il ne comporte qu'une seule instruction.

Une fonction peut avoir plusieurs paramètres ou aucun. Dans l'exemple suivant, makeNoise ne prend pas de paramètre :

1. const makeNoise = function () {

2. console.log("Pling!");

3. };

5. makeNoise ();

6. // → Pling!

Ici, power en prend deux :

1. const power = function(base, exponent) {

2. let result = 1;

3. for (let count = 0; count < exponent; count++) {

4. result *= base;

5. }

6. return result;

7. };

9. console.log(power(2, 10));

10. // → 1024

Certaines fonctions telles que power et square retournent une valeur, d'autres non, comme makeNoise. Une instruction return détermine la valeur renvoyée par la fonction. Lorsque l'interpréteur rencontre une telle instruction, il sort immédiatement de la fonction en cours et renvoie la valeur au code à l’origine de l’appel. L'utilisation de l’instruction return sans valeur retournera undefined Les fonctions qui ne possèdent pas d'instruction de , return telles que makeNoise, renvoient de la même manière undefined.

Les paramètres d'une fonction se comportent comme des variables, mais leur valeur initiale est donnée par l'appelant de la fonction et non par le code de la fonction elle-même.

IV-B - Fixations et portées

Chaque variable a une portée, qui est la partie du programme dans laquelle la variable est visible. Pour les variables définies en dehors de toute fonction ou bloc, la portée est le programme complet - vous pouvez vous référer à ces variables où vous voulez. Celles-ci sont appelées globales.

Mais les variables créées pour les paramètres de fonction ou déclarées à l'intérieur d'une fonction ne peuvent être référencées que dans celle-ci, elles sont donc locales. Chaque fois que la fonction est appelée, de nouvelles instances de ces variables sont créées. Ceci fournit une certaine isolation entre les fonctions - chaque appel de fonction agit dans son propre petit monde (son environnement local) et peut souvent être interprété sans trop savoir ce qui se passe dans l'environnement global.

Les variables déclarées avec let et const sont en fait locales au bloc dans lequel elles sont déclarées. Par conséquent, si vous créez l'un de ceux-ci dans une boucle, le code avant et après la boucle ne peut pas les utiliser. Les variables anciennes, créées avec le mot-clef , sont var lisibles dans toute la fonction dans laquelle elles apparaissent - ou dans la portée globale, si elles ne figurent pas dans une fonction.

1. let x = 10;

2. if (true) {

3. const y = 20;

4. var z = 30;

5. console.log (x + y + z);

6. // → 60

7. }

8. // y n'est pas visible ici

9. console.log (x + z);

10. // → 40

Chaque portée peut « regarder » dans la portée qui l'entoure, donc x est visible à l'intérieur du bloc, dans cet exemple. Il existe plusieurs variables qui ont le même nom - dans ce cas, le code ne peut voir que la plus interne. Par exemple, lorsque le code dans la fonction half fait référence à n, il voit son propre n, pas le n global.

IV-C - Portée imbriquée

JavaScript distingue seulement les variables globales et locales. Des blocs et des fonctions peuvent être créés à l'intérieur d'autres blocs et fonctions, produisant plusieurs degrés de localité.

Par exemple, cette fonction - qui produit les ingrédients nécessaires pour faire un lot de hummus - déclare une autre fonction à l'intérieur :

1. const hummus = function(factor) {

2. const ingredient = function(amount, unit, name) {

3. let ingredientAmount = amount * factor;

4. if (ingredientAmount > 1) {

5. unit += "s";

6. }

7. console.log(`${ingredientAmount} ${unit} ${name}`);

8. };

9. ingredient(1, "can", "chickpeas");

10. ingredient(0.25, "cup", "tahini");

11. ingredient(0.25, "cup", "lemon juice");

12. ingredient(1, "clove", "garlic");

13. ingredient(2, "tablespoon", "olive oil");

14. ingredient(0.5, "teaspoon", "cumin");

15. };

Le code dans la fonction ingredient peut voir la variable factor de la fonction hummus. Mais ses variables locales, telles que unit ou ingredientAmount, ne sont pas visibles dans la fonction hummus.

L'ensemble des variables visibles à l'intérieur d'un bloc est déterminé par la place de ce bloc dans le texte du programme. Chaque portée locale peut également voir toutes les étendues locales qui la contiennent et toutes les portées peuvent voir la portée globale. Cette approche de la visibilité de variable est appelée portée lexicale.

IV-D - Les fonctions en tant que valeur

Le nom d'une fonction, déclarée en tant que variable, permet sa réutilisation. Une telle variable est définie une fois et n'est jamais modifiée. Cela facilite la confusion entre la fonction et son nom.

Il est possible de stocker une valeur de fonction dans une nouvelle variable, de la passer en argument à une fonction, etc. De même, une variable qui contient une fonction est toujours une variable régulière et peut, si elle n'est pas constante, se voir attribuer une nouvelle valeur, comme ceci :

1. let launchMissiles = function() {

2. missileSystem.launch("now");

3. };

4. if (safeMode) {

5. launchMissiles = function() {/* do nothing */};

6. }

Au chapitre 5, nous aborderons l'utilisation des variables de fonction dans d'autres fonctions.

IV-E - Autres déclarations

Il existe un moyen légèrement plus court de créer une variable de fonction. Lorsque le mot-clef function est utilisé au début d'une instruction, il fonctionne différemment.

1. function square(x) {

2. return x * x;

3. }

Ceci est une déclaration de fonction. L'instruction définit la variable square et la fait pointer vers la fonction donnée. Il est légèrement plus facile à écrire et ne nécessite pas de point-virgule après la fonction.

Il y a une subtilité avec cette forme de définition de fonction.

1. console.log("The future says:", future());

3. function future() {

4. return "You'll never have flying cars";

5. }

Le code précédent fonctionne, même si la fonction est définie sous le code qui l'utilise. Les déclarations de fonction ne font pas partie du flux d'interprétation de haut en bas. Elles sont conceptuellement déplacées au sommet de l'exécution et peuvent être utilisées par tous les codes de cette portée. Ce code est parfois utile, car il permet de déclarer les fonctions là où elles sont le plus utilisées.

IV-F - Fonctions fléchées

Il y a une troisième forme de notation pour les fonctions, forme très différente des autres. Au lieu du mot-clef function, cette forme utilise une flèche (=>) composée d'un signe égal et d'un caractère supérieur à (à ne pas confondre avec l'opérateur supérieur ou égal à, qui est écrit >=).

1. const power = (base, exponent) => {

2. let result = 1;

3. for (let count = 0; count < exponent; count++) {

4. result *= base;

5. }

6. return result;

7. };

La flèche vient après la liste des paramètres, elle est suivie par le corps de la fonction. Il exprime quelque chose comme « cette entrée (les paramètres) produit ce résultat (le corps) ».

Lorsqu'il n'y a qu'un seul nom de paramètre, vous pouvez omettre les parenthèses autour de la liste de paramètres. Si le corps est une expression unique, plutôt qu'un bloc entre accolades, cette expression sera renvoyée par la fonction.

Ces deux définitions de square font la même chose :

1. const square1 = (x) => { return x * x; };

2. const square2 = x => x * x;

Lorsqu'une fonction de flèche n'a aucun paramètre, sa liste de paramètres est simplement un ensemble vide de parenthèses.

1. const horn = () => {

2. console.log("Toot");

3. };

Les fonctions fléchées et le mot-clef function peuvent être utilisés ensemble. Mis à part un détail mineur, dont nous parlerons au chapitre 6, ils ont le même comportement. Les fonctions fléchées ont été ajoutées en 2015, principalement pour permettre d'écrire des expressions de petite taille de manière moins verbeuse. Nous les utiliserons beaucoup au chapitre 5.

IV-G - La pile d'exécution

La manière dont l'interpréteur passe par les fonctions est appelée la pile d'exécution.

Regardons cela de plus près. Voici un programme simple qui appelle une fonction :

1. function greet(who) {

2. console.log("Hello " + who);

3. }

4. greet("Harry");

5. console.log("Bye");

Un passage à travers ce programme se passe comme ceci : l'appel à greet transfère l'exécution au début de cette fonction (ligne 2). La fonction appelle console.log, qui récupère l'exécution, effectue son travail, puis « retourne » à la ligne 2. Là, elle atteint la fin de la fonction de greet, donc elle retourne à l'endroit qui l'a appelée, c’est-à-dire la ligne 4. L'appel au nouveau console.log s'exécute. Après cela, le programme arrive à son terme.

Nous pourrions montrer le flux de contrôle schématiquement comme ceci :

1. not in function

2. in greet

3. in console.log

4. in greet

5. not in function

6. in console.log

7. not in function

Comme une fonction doit revenir à l'endroit où elle a été appelée lorsque les instructions du bloc se terminent (à l'accolade fermante ou à l'aide d'un return), l'ordinateur doit mémoriser le contexte dans lequel l'appel a eu lieu.

L'emplacement où l'ordinateur stocke ce contexte est la pile d'exécution. Chaque fois qu'une fonction est appelée, le contexte actuel est stocké sur cette pile. Lorsqu'une fonction se termine, elle supprime le contexte supérieur de la pile et utilise ce contexte pour continuer l'exécution.

Stocker cette pile nécessite de l'espace dans la mémoire de l'ordinateur. Lorsque la pile devient trop grande, l'exécution du programme échoue avec un message tel que « out of stack space » ou « too much recursion ». Le code suivant illustre cela en posant à l'ordinateur une question vraiment difficile qui provoque un va-et-vient infini entre deux fonctions. L'ordinateur ne peut conserver en mémoire une pile d'exécution infinie, ce code générera une erreur :

1. function chicken() {

2. return egg();

3. }

4. function egg() {

5. return chicken();

6. }

7. console.log(chicken() + " came first.");

8. // → ??

IV-H - Arguments optionnels

Le code suivant est autorisé et s'exécute sans problème :

1. function square(x) { return x * x; }

2. console.log(square(4, true, "hedgehog"));

3. // → 16

Nous avons défini un calcul de carré avec un seul paramètre. Pourtant, quand on l'appelle avec trois, le langage ne se plaint pas. Il ignore les arguments supplémentaires et calcule le carré du premier.

JavaScript est extrêmement large quant au nombre d'arguments que vous transmettez à une fonction. Si vous en passez trop, les arguments excédentaires sont ignorés. Si vous en passez trop peu, les paramètres manquants se voient attribuer la valeur undefined.

L'inconvénient de cela est qu'il est possible, voire probable, que vous transmettiez accidentellement un nombre d'arguments incorrect aux fonctions. Et le code aura un comportement inattendu.

L'avantage est que ce comportement peut être utilisé pour permettre à une fonction d'être appelée avec un nombre différent d'arguments. Par exemple, cette fonction minus essaye d'imiter l'opérateur « - » en agissant sur un ou deux arguments :

1. function minus(a, b) {

2. if (b === undefined) return -a;

3. else return a - b;

4. }

6. console.log(minus(10));

7. // → -10

8. console.log(minus(10, 5));

9. // → 5

Si vous écrivez un opérateur = après un paramètre, suivi d'une expression, la valeur de cette expression remplacera l'argument lorsqu'il n'est pas donné.

Par exemple, cette version de power rend son deuxième argument facultatif. Si vous ne le fournissez pas, la valeur par défaut sera deux et la fonction l'utilisera ainsi.

1. function power(base, exponent = 2) {

2. let result = 1;

3. for (let count = 0; count < exponent; count++) {

4. result *= base;

5. }

6. return result;

7. }

9. console.log(power(4));

10. // → 16

11. console.log(power(2, 6));

12. // → 64

Dans le chapitre suivant, nous verrons comment un corps de fonction peut obtenir la liste complète des arguments qui lui ont été transmis. C’est utile, car cela permet à une fonction d'accepter un nombre quelconque d'arguments. Par exemple, console.log fait cela – il affiche toutes les valeurs qui lui sont données.

1. console.log("C", "O", 2);

2. // → C O 2

IV-I - Fermeture (Closure)

La possibilité de traiter les fonctions comme des valeurs, combinée au fait que les variables locales sont recréées chaque fois qu'une fonction est appelée, soulève une question intéressante. Qu'advient-il des variables locales lorsque l'appel de fonction, qui les a créées, est terminé ?

Le code suivant en montre un exemple. Il définit une fonction, wrapValue, qui crée une variable locale. Il retourne ensuite une fonction qui accède à et renvoie en retour cette variable locale.

1. function wrapValue(n) {

2. let local = n;

3. return () => local;

4. }

6. let wrap1 = wrapValue(1);

7. let wrap2 = wrapValue(2);

8. console.log(wrap1());

9. // → 1

10. console.log(wrap2());

11. // → 2

Ce code fonctionne exactement comme demandé : les deux instances de la variable sont toujours accessibles. Cette situation est une bonne démonstration du fait que les variables locales sont recréées à chaque appel, et que les différents appels ne peuvent pas écraser les variables locales déclarée plus haut.

Cette fonctionnalité - être capable de référencer une instance spécifique d'une variable locale dans une portée englobante - s'appelle la fermeture (closure). Ce comportement vous permet non seulement de vous soucier de la durée de vie des variables, mais également de réutiliser les paramètres de fonction.

Avec un léger changement, nous pouvons transformer l'exemple précédent en un moyen de créer des fonctions qui se multiplient par une quantité arbitraire :

1. function multiplier(factor) {

2. return number => number * factor;

3. }

5. let twice = multiplier(2);

6. console.log(twice(5));

7. // → 10

Dans l'exemple wrapValue, déclarer la variable locale n'est pas nécessaire, car n est déjà une variable du scope de wrapValue.

Penser à des programmes comme celui-ci demande un peu de pratique. Un bon modèle mental est de penser que les valeurs fonctionnelles contiennent à la fois le code dans leur corps et l'environnement dans lequel elles sont créées. Lorsqu'il est appelé, le corps de la fonction voit l'environnement dans lequel il a été créé, pas l'environnement dans lequel il est appelé.

Dans l'exemple, multiplier est appelé et crée un environnement dans lequel son paramètre factor est lié à la valeur 2. La valeur de la fonction renvoyée, où est stockée twice, mémorise cet environnement. Donc, quand la fonction multiplierest appelée, elle multiplie son argument par 2. la valeur de twice est appelée instance de multiplier.

IV-J - Récursion

Il est possible qu'une fonction s'appelle elle-même, à condition qu'elle respecte la limite de la pile d'exécution. Une fonction qui s'appelle elle-même est appelée récursive. La récursivité permet l'écriture de certaines fonctions dans un style différent. Prenons, par exemple, cette mise en œuvre différente du power :

1. function power(base, exponent) {

2. if (exponent == 0) {

3. return 1;

4. } else {

5. return base * power(base, exponent - 1);

6. }

7. }

9. console.log(power(2, 3));

10. // → 8

Ceci est assez proche de ce que les mathématiciens appellent l'exponentiation et décrit sans doute le concept plus clairement que la variante utilisant une boucle. La fonction s'appelle plusieurs fois avec des exposants de plus en plus petits pour obtenir le résultat.

Mais cette implémentation pose un problème : dans les implémentations JavaScript typiques, elle est environ trois fois plus lente que la version avec boucle. Passer par une simple boucle est généralement moins coûteux que d'appeler plusieurs fois une fonction.

Le dilemme de la vitesse contre l'élégance est intéressant. On peut y voir une sorte de continuum entre la convivialité humaine et la convivialité mécanique. Presque n'importe quel programme peut être rendu plus rapide en le rendant plus grand et plus compliqué. Le programmeur doit décider d'un équilibre approprié.

Dans le cas de la fonction de power, la version inélégante (en boucle) est encore assez simple et facile à lire. Cela n'a pas beaucoup de sens de la remplacer par la version récursive. Cependant, souvent, un programme traite de concepts si complexes qu'il est utile de renoncer à une certaine efficacité pour rendre le programme plus lisible.

Par conséquent, commencez toujours par écrire quelque chose de correct et facile à comprendre. Si vous craignez que l’exécution soit trop lente (ce qui n'est généralement pas le cas, car la plus grande partie du code n'est tout simplement pas exécutée assez souvent pour prendre beaucoup de temps), vous pouvez effectuer une mesure ultérieure et l'améliorer si nécessaire.

La récursivité n'est pas toujours une mauvaise solution de remplacement de la boucle. Certains problèmes sont plus faciles à résoudre avec la récursivité qu'avec les boucles. Le plus souvent, il s'agit de problèmes qui nécessitent d'explorer ou de traiter plusieurs « branches », chacune d'elles pouvant se ramifier dans encore plus de branches.

Considérez ce casse-tête :

En commençant par le nombre 1 et soit en ajoutant 5, soit en multipliant par 3, un nombre infini de nombres peut être obtenu. Comment écririez-vous une fonction qui, à partir d’un nombre, tente de trouver une séquence d’additions et de multiplications de ce type ?

Par exemple, le nombre 13 peut être atteint en multipliant d'abord par 3 et en ajoutant ensuite deux fois 5, alors que le nombre 15 ne peut pas être atteint du tout.

Voici une solution récursive :

1. function findSolution(target) {

2. function find(current, history) {

3. if (current == target) {

4. return history;

5. } else if (current > target) {

6. return null;

7. } else {

8. return find(current + 5, `(${history} + 5)`) ||

9. find(current * 3, `(${history} * 3)`);

10. }

11. }

12. return find(1, "1");

13. }

15. console.log(findSolution(24));

16. // → (((1 * 3) + 5) * 3)

Notez que ce programme ne trouve pas nécessairement la séquence d'opérations la plus courte. Il est satisfait quand il trouve ne séquence quelconque.

Ce n'est pas grave si vous ne voyez pas immédiatement comment cela fonctionne.

Il s'agit d'un excellent exercice de pensée récursive. Voici le fonctionnement :

La fonction interne find effectue la récursion réelle. Elle prend deux arguments : le nombre actuel et une chaîne qui enregistre comment nous avons atteint ce nombre. Si elle trouve une solution, elle renvoie une chaîne indiquant comment accéder à la cible. Si aucune solution ne peut être trouvée à partir de ce numéro, elle renvoie null.

Pour ce faire, la fonction effectue l'une des trois actions suivantes :

Si le nombre actuel est le numéro cible, l'historique actuel est un moyen d'atteindre cette cible, il est donc retourné. Si le nombre actuel est supérieur à la cible, il est inutile d'explorer davantage cette branche, car l'ajout et la multiplication ne feraient que rendre le nombre plus grand, il renvoie donc null. Enfin, si nous sommes toujours en dessous du nombre cible, la fonction essaye les deux chemins possibles qui partent du nombre actuel en s'appelant deux fois, une fois pour l'ajout et une fois pour la multiplication. Si le premier appel renvoie quelque chose qui n'est pas null, il est renvoyé. Sinon, le deuxième appel est renvoyé, qu'il produise une chaîne (String) ou null.

Pour mieux comprendre comment cette fonction produit l'effet recherché, examinons tous les appels à find lors de la recherche d'une solution pour le numéro 13.

1. find(1, "1")

2. find(6, "(1 + 5)")

3. find(11, "((1 + 5) + 5)")

4. find(16, "(((1 + 5) + 5) + 5)")

5. too big

6. find(33, "(((1 + 5) + 5) * 3)")

7. too big

8. find(18, "((1 + 5) * 3)")

9. too big

10. find(3, "(1 * 3)")

11. find(8, "((1 * 3) + 5)")

12. find(13, "(((1 * 3) + 5) + 5)")

13. found!

L'indentation indique la profondeur de la pile d'appels. La première fois que la fonction find est appelée, elle commence par s'appeler elle-même pour explorer la solution qui commence par (1 + 5). Cet appel permettra en outre d'explorer toutes les solutions continues qui donnent un nombre inférieur ou égal au nombre cible. Comme il n'en trouve pas qui corresponde, il renvoie null au premier appel. Là, l'opérateur || provoque l'appel à l'exploration (1 * 3). Cette recherche a plus de chance - son premier appel récursif, via un autre appel récursif, obtient le numéro cible. Cet appel le plus interne renvoie une chaîne et chacun des || dans les appels intermédiaires transmet cette chaîne, retournant finalement la solution.

IV-K - Fonctions réutilisables

Il y a deux manières plus ou moins naturelles d'introduire des fonctions dans les programmes.

La première est pour éviter d'écrire plusieurs fois un même code. Avoir plus de code signifie plus d'erreurs reproduites et moins de lisibilité. La fonction répond à cette problématique.

La deuxième façon est le besoin d'une fonctionnalité potentiellement réutilisable. Vous commencerez par nommer la fonction, puis vous écrirez son corps. La difficulté de trouver un nom pour une fonction est une bonne indication de la clarté du concept que vous essayez d'encapsuler. Passons en revue un exemple.

Nous voulons écrire un programme qui logue deux nombres : le nombre de vaches et de poulets dans une ferme, avec les mots « Cows » et « Chickens » précédés par leur nombre à 3 chiffres.

007 Cows
011 Chickens

Cela nécessite une fonction prenant deux arguments : le nombre de vaches et le nombre de poulets. Voici le code :

1. function printFarmInventory(cows, chickens) {

2. let cowString = String(cows);

3. while (cowString.length < 3) {

4. cowString = "0" + cowString;

5. }

6. console.log(`${cowString} Cows`);

7. let chickenString = String(chickens);

8. while (chickenString.length < 3) {

9. chickenString = "0" + chickenString;

10. }

11. console.log(`${chickenString} Chickens`);

12. }

13. printFarmInventory(7, 11);

L'écriture de .length juxtaposé au nom d’une variable contenant une chaîne de caractères, nous donnera la longueur de cette chaîne. Ainsi, les boucles while ajoutent des zéros devant les chaînes de nombres jusqu'à ce qu'elles aient au moins trois caractères.

Mission accomplie ! Mais au moment d'envoyer le code, il s'avère que nous devons aussi compter les cochons.

Nous pouvons certainement. Voici une première tentative utilisant le copier-coller de l'appel :

1. function printZeroPaddedWithLabel(number, label) {

2. let numberString = String(number);

3. while (numberString.length < 3) {

4. numberString = "0" + numberString;

5. }

6. console.log(`${numberString} ${label}`);

7. }

9. function printFarmInventory(cows, chickens, pigs) {

10. printZeroPaddedWithLabel(cows, "Cows");

11. printZeroPaddedWithLabel(chickens, "Chickens");

12. printZeroPaddedWithLabel(pigs, "Pigs");

13. }

15. printFarmInventory(7, 11, 3);

Ça marche ! Mais ce nom, printZeroPaddedWithLabel, est un peu bizarre. Il confond trois choses : l'impression, l'ajout des zéros et l'ajout d'une étiquette dans une seule fonction.

Au lieu de supprimer la partie répétée de notre programme, rendons-la générique :

1. function zeroPad(number, width) {

2. let string = String(number);

3. while (string.length < width) {

4. string = "0" + string;

5. }

6. return string;

7. }

9. function printFarmInventory(cows, chickens, pigs) {

10. console.log(`${zeroPad(cows, 3)} Cows`);

11. console.log(`${zeroPad(chickens, 3)} Chickens`);

12. console.log(`${zeroPad(pigs, 3)} Pigs`);

13. }

15. printFarmInventory(7, 16, 3);

Une fonction avec un nom lisible, comme zeroPad, facilite la lecture du code. Et une telle fonction est utile dans plus de situations que ce programme précédent. Par exemple, vous pouvez l'utiliser pour imprimer des tableaux de nombres bien alignés.

Dans quelle mesure notre fonction doit-elle être intelligente et polyvalente ? Nous pourrions écrire n'importe quoi, d'une fonction terriblement simple, qui ne peut contenir que trois caractères sur un nombre, à un système complexe de formatage numérique qui gère les nombres fractionnaires, les nombres négatifs, l'alignement des points décimaux, le remplissage avec différents caractères, etc.

Ne pas ajouter d'intelligence à moins d'être absolument certain que vous en aurez besoin est un principe de la programmation. Il peut être tentant d'écrire des « frameworks » généraux pour chaque fonctionnalité que vous rencontrerez. Résistez à cette envie. Vous n'obtiendrez aucun travail réel - vous écrirez simplement du code que vous n'utiliserez jamais.

IV-L - Synthèse

Ce chapitre vous a appris à écrire vos propres fonctions. Le mot-clef , function utilisé comme expression, crée une fonction. Utilisé en tant que déclaration, il peut être utilisé pour déclarer une variable et lui attribuer une fonction. Les fonctions fléchées sont encore une autre façon de créer des fonctions.

1. // Define f to hold a function value

2. const f = function(a) {

3. console.log(a + 2);

4. };

6. // Declare g to be a function

7. function g(a, b) {

8. return a * b * 3.5;

9. }

11. // A less verbose function value

12. let h = a => a % 3;

Un aspect essentiel de la compréhension des fonctions est la maîtrise des portées d'application. Chaque bloc crée une nouvelle portée. Les paramètres et variables déclarés dans une portée sont locaux à cette portée et non visibles de l'extérieur. Les variables déclarées avec var se comportent différemment : leur portée est globale.

Séparer les tâches effectuées par votre programme en différentes fonctions est utile. Vous n'aurez plus besoin de vous répéter, et les fonctions peuvent vous aider à organiser un programme en regroupant le code en plusieurs éléments.

IV-M - Exercices

IV-M-1 - Le minimum

Le chapitre précédent a introduit la fonction standard Math.min qui renvoie son plus petit argument. Vous allez recoder cette fonction. Écrivez une fonction min qui prend deux arguments et retourne leur minimum.

1. // Votre code ici.

3. console.log(min(0, 10));

4. // → 0

5. console.log(min(0, -10));

6. // → -10

Si vous avez des difficultés à placer les accolades et les parenthèses au bon endroit pour obtenir une définition de fonction valide, commencez par copier un des exemples de ce chapitre et modifiez-le.

Une fonction peut contenir plusieurs déclarations de retour.

IV-M-2 - Récursion

Nous avons vu que % (l’opérateur modulo) peut être utilisé pour tester si un nombre est pair ou impair en utilisant % 2 pour savoir s'il est divisible par deux. Voici une autre façon de définir si un nombre entier positif est pair ou impair :

- 0 est pair ;

- 1 est impair ;

- pour tout autre nombre N, sa régularité est la même que N - 2.

Définissez une fonction récursive isEven correspondant à cette description. La fonction devra accepter un seul paramètre (un nombre entier positif) et renvoyer un booléen.

Testez-la sur 50 et 75. Voyez comment elle se comporte sur -1.

1. // Votre code ici.

3. console.log(isEven(50));

4. // → true

5. console.log(isEven(75));

6. // → false

7. console.log(isEven(-1));

8. // → ??

Votre fonction ressemblera probablement un peu à la fonction interne find dans l'exemple récursif findSolution de ce chapitre, avec une chaîne if/else if/else qui teste lequel des trois cas s'applique. Le dernier else, correspondant au troisième cas, fait l'appel récursif. Chacune des branches doit contenir une déclaration de retour ou, d'une autre manière, faire en sorte qu'une valeur spécifique soit retournée.

Lorsqu'on lui donne un nombre négatif, la fonction va boucler indéfiniment , en se passant un nombre de plus en plus négatif, s'éloignant ainsi de plus en plus du retour d'un résultat. Elle finira par manquer d'espace dans la pile et annulera.

IV-M-3 - Comptage des majuscules

Vous pouvez obtenir le Nième caractère, ou lettre, d'une chaîne en écrivant "string"[N]. La valeur renvoyée sera une chaîne contenant un seul caractère (par exemple, ) "b". Le premier caractère se trouve à la position 0, donc, le dernier se trouve à . En d'autres termes, une string.length – 1 chaîne de deux caractères a une longueur de 2 et ses caractères ont les positions 0 et 1.

Écrivez une fonction countBs qui prend une chaîne comme seul argument et retourne le nombre de caractères majuscules «B» dans la chaîne.

Ensuite, écrivez une fonction appelée countChar qui se comporte comme des countBs, sauf qu'elle prend un second argument qui indique le caractère à compter (plutôt que de ne compter que les caractères majuscules « B »).Réécrivez countBs pour utiliser cette nouvelle fonction.

1. // Votre code ici.

3. console.log(countBs("BBC"));

4. // → 2

5. console.log(countChar("kakkerlak", "k"));

6. // → 4

Votre fonction aura besoin d'une boucle qui examine chaque caractère de la chaîne. Elle peut exécuter un index de zéro à un en dessous de sa longueur (< string.length). Si le caractère à la position courante est le même que celui que la fonction recherche, elle ajoute 1 à une variable compteur. Une fois la boucle terminée, le compteur peut être retourné.

Prenez soin de rendre locales à la fonction toutes les liaisons utilisées dans cette fonction en déclarant ces liaisons correctement avec le mot-clef let ou const.

V - Chapitre 4 : structure des données - Objets et Tableaux

À deux reprises, on m'a demandé : « Priez, M. Babbage, si vous mettez dans la machine des chiffres erronés, les bonnes réponses seront-elles données ? » [...] Je ne suis pas en mesure d'appréhender correctement le genre de confusion d'idées susceptible de provoquer une telle question.Charles Babbage, Passages de la vie d'un philosophe (1864)

Nombres, booléens et chaînes de caractères sont les atomes à partir desquels les structures des données sont construites. Cependant, de nombreuses informations requièrent plus d’un atome. Les Objets nous permettent de grouper des valeurs, y compris d’autres objets, pour construire des structures plus complexes.

Les programmes que nous avons conçus jusqu’à présent étaient limités par le fait qu’ils ne géraient que des données de types simples. Ce chapitre va introduire les bases des structures de données. En fin de chapitre, vous en connaîtrez suffisamment pour écrire des programmes utiles.

Le chapitre va se développer au travers d’un exemple de programme plus ou moins réaliste, introduisant les concepts au fur et à mesure qu’ils apparaissent dans le problème. Le code d’exemple se construira souvent sur des fonctions et des liens introduits précédemment dans le texte.

V-A - L’écureuil-garou

Régulièrement, entre 8 et 10 heures du matin, Jacques se retrouve transformé en un furieux petit rongeur à la queue touffue.

D’un côté, Jacques est heureux de ne pas être transformé en trop classique lycanthrope : se transformer en écureuil lui cause moins de problèmes que de se transformer en loup. Au lieu de s’inquiéter d’avoir éventuellement mangé le voisin (ça, ce serait gênant), il doit s’inquiéter de ne pas se faire manger par le chat du voisin. Après s’être réveillé par deux fois sur une fine petite branche à la cime d’un arbre, nu et désorienté, il s’est mis à barricader portes et fenêtres de sa chambre le soir et à placer quelques noix sur le sol pour se tenir occupé. Cela résout le problème du chat et de l’arbre. Mais Jacques voudrait être complètement débarrassé de cette situation. La survenance aléatoire de la transformation le pousse à soupçonner que cela pourrait être déclenché par quelque chose. Pendant un temps, il a cru que cela n’arrivait que les jours où il avait été en contact avec des chênes. Mais le fait d’éviter les chênes n’a rien changé au problème. Passant à une approche plus scientifique, Jacques a commencé à tenir un journal quotidien de tout ce qu’il fait et des moments où il change de forme. Avec ces données, il espère cerner les conditions qui déclenchent les transformations.

La première chose dont il a besoin c’est d’une structure de données pour stocker l’information.

V-B - Ensemble de données

Pour travailler avec un ensemble de données numériques, nous devons d’abord trouver un moyen de représenter cela dans la mémoire de notre machine. Ainsi, nous voulons représenter la collection des nombres 2, 3, 5, 7 et 11.

Nous pouvons être créatifs et travailler avec des chaînes de caractères (string) car après tout une chaîne de caractères peut être de n’importe quelle longueur et donc, nous pourrions utiliser la chaîne « 2 3 5 7 11 » dans notre exemple. Mais c’est maladroit. Pour les utiliser, vous devriez à chaque fois en extraire les différents nombres et les transformer en valeurs numériques.

Heureusement, JavaScript fournit un type de données spécifique pour stocker une série de valeurs. Ce type de données est appelé Array (tableau) et il est écrit comme une liste de valeurs entre crochets séparées par des virgules.

1. let listOfNumbers = [2, 3, 5, 7, 11];

2. console.log(listOfNumbers[2]);

3. // → 5

4. console.log(listOfNumbers[0]);

5. // → 2

6. console.log(listOfNumbers[2 - 1]);

7. // → 3

La notation qui permet de récupérer des éléments à l’intérieur d’un tableau utilise également les crochets. Une paire de crochets immédiatement après une expression, incluant une autre expression, pointera sur l’élément de l‘expression de gauche correspondant à l’index donné par l’expression entre crochets.

Le premier index d’un tableau est zéro, pas un. Donc le premier élément de la liste est retrouvé par listeDeNombres[0]. Le comptage partant de zéro a une longue tradition dans la technologie et il est assez logique d’un certain point de vue, mais il faut un certain temps pour s’y habituer. Imaginez l’index comme le nombre d’éléments à laisser tomber en partant du début du tableau.

V-C - Propriétés

Nous avons vu plusieurs expressions suspectes comme myString.length (pour récupérer la longueur d’une chaîne de caractères) et Math.max (la fonction maximum) dans les chapitres précédents. Ce sont des expressions qui permettent d’accéder à une propriété de certaines valeurs. Dans le premier cas, nous accédons à la propriété longueur (length) de la valeur dans myString. Dans le second cas, nous accédons à la propriété appelée max de l’objet Math (qui est une collection de constantes et fonctions mathématiques).

À peu près toutes les valeurs en JavaScript ont des propriétés. Les seules exceptions sont null et undefined. Si vous tentez d’accéder à une propriété d’une de ces non-valeurs, vous obtenez une erreur.

null.length;
// → TypeError: null has no properties

Les deux principaux moyens d’accéder aux propriétés en JavaScript sont le point et les crochets. valeur.x et valeur[x] accèdent à une propriété de valeur (mais pas forcément à la même propriété). La différence est la manière dont x est interprété. En utilisant un point, le mot après le point est le nom littéral deense la propriété. En utilisant les crochets, l’expression entre crochets est évaluée pour récupérer le nom de la propriété. Alors que valeur.x va chercher la propriété de la valeur nommée « x », valeur[x] tente d’évaluer l’expression x et utilise le résultat, converti en chaîne de caractères, comme nom de la propriété. Donc, si vous savez que la propriété qui vous intéresse s’appelle couleur, vous dites valeur.couleur. Si vous souhaitez extraire la propriété nommée d’après la valeur contenue dans la constante i, vous dites valeur[i]. Les noms des propriétés sont des chaînes de caractères. Ils peuvent être n’importe quelle chaîne de caractères, mais la notation avec point ne fonctionne que si le nom ressemble à un nom de variable valide. Donc si vous souhaitez accéder à une propriété nommée 2 ou Pierre Carré, vous devez utiliser les crochets: valeur[2] ou valeur["Pierre Carré"].

Les éléments dans un tableau (Array) sont stockés comme des propriétés de ce tableau, utilisant des nombres comme nom de propriété. Parce que vous ne pouvez utiliser la notation point avec des nombres et que vous souhaitez néanmoins utiliser la constante qui contient l’index, vous devez utiliser la notation entre crochets pour les récupérer. La propriété length (longueur) d’un tableau nous indique le nombre d’éléments qu’il contient. Ce nom de propriété est une constante valide et nous connaissons ce nom à l’avance, donc pour trouver la longueur d’un tableau, vous pouvez écrire array.length parce que c’est aussi plus facile que d’écrire array["length"].

V-D - Méthodes

Les objets string et array contiennent, en plus de la propriété length, un certain nombre de propriétés qui contiennent des valeurs de fonctions.

1. let doh = "Doh";

2. console.log(typeof doh.toUpperCase);

3. // → function

4. console.log(doh.toUpperCase());

5. // → DOH

Chaque chaîne de caractères possède une propriété toUpperCase. Lorsqu’elle est appelée, elle va renvoyer une copie de la chaîne de caractères dans laquelle toutes les lettres ont été mises en majuscule. Il existe aussi la propriété toLowerCase, pour faire le contraire (mettre en minuscule).

Fait intéressant, même si l’appel de la fonction toUppercase ne passe aucun argument, la fonction a accès à la chaîne de caractères « Doh », la valeur de la propriété que nous appelons. Le fonctionnement de ceci est expliqué au Chapitre 6

Les propriétés qui contiennent des fonctions sont généralement appelées méthodes de la variable à laquelle elles appartiennent. « toUpperCase » est une méthode d’une chaîne de caractères.

Cet exemple montre deux méthodes que vous pouvez utiliser pour manipuler un tableau :

1. let sequence = [1, 2, 3];

2. sequence.push(4);

3. sequence.push(5);

4. console.log(sequence);

5. // → [1, 2, 3, 4, 5]

6. console.log(sequence.pop());

7. // → 5

8. console.log(sequence);

9. // → [1, 2, 3, 4]

La méthode push ajoute des valeurs à la fin d’un tableau et la méthode pop fait le contraire, supprimant la dernière valeur du tableau et retournant celui-ci.

Ces noms quelque peu ridicules sont les termes traditionnels pour les opérations sur un empilement. Un empilement, en programmation, est une structure de données dans laquelle vous pouvez pousser des valeurs et les ressortir dans l’ordre opposé de sorte que le dernier élément ajouté est le premier retiré. C’est assez commun en programmation. Vous pourriez vous souvenir de la fonction call stack du <chapitre précédent> qui est un autre exemple de la même idée.

V-E - Objets

Retour à l’écureuil-garou. Un ensemble d’entrées quotidiennes du journal peut être représenté comme un tableau. Mais chaque entrée n’est pas juste un nombre ou une chaîne de caractères. Chaque entrée doit stocker une liste d’activités et une valeur booléenne qui indique si Jacques s’est transformé en écureuil ou pas. Idéalement, nous souhaiterions regrouper tout cela en une seule variable et mettre toutes ces variables dans un tableau du journal d’entrées.

Les variables de type Object sont une collection arbitraire de propriétés. Une manière de créer un objet est d’utiliser des accolades comme dans une expression.

1. let day1 = {

2. squirrel: false,

3. events: ["work", "touched tree", "pizza", "running"]

4. };

5. console.log(day1.squirrel);

6. // → false

7. console.log(day1.wolf);

8. // → undefined

9. day1.wolf = false;

10. console.log(day1.wolf);

11. // → false

Dans les accolades, il y a une liste de propriétés séparées par des virgules. Chaque propriété a un nom suivi de deux points et d’une valeur. Quand un objet est écrit sur plusieurs lignes, l’indentation, comme dans l’exemple, aide à la lisibilité. Les propriétés qui ne seraient pas des noms de constante valides ou des nombres valides doivent être mises entre guillemets.

1. let descriptions = {

2. work: "Went to work",

3. "touched tree": "Touched a tree"

4. };

Ceci veut dire que les accolades ont deux significations en JavaScript. En début d’une déclaration, elles démarrent le bloc d’instruction. Dans toute autre position, elles décrivent un objet. Heureusement, il est rarement utile de commencer une instruction avec un objet dans des accolades, donc cette ambiguïté n’est pas réellement un problème.

La lecture d’une propriété inexistante vous donnera une valeur indéfinie (undefined).

Il est possible d'attribuer une valeur à une propriété avec l’opérateur « = ». Ceci remplacera la valeur de la propriété si elle existe déjà ou créera une nouvelle propriété sur l’objet si elle n’existe pas encore.

Revenons brièvement à notre modèle tentaculaire de liaisons : les liaisons de propriétés sont similaires. Elles saisissent des valeurs, mais d'autres liaisons et propriétés peuvent conserver ces mêmes valeurs. Vous pouvez penser à des objets comme des pieuvres avec un nombre quelconque de tentacules portant chacun un nom tatoué.

L'opérateur delete coupe un tentacule de la pieuvre. C'est un opérateur unaire qui, lorsqu'il est appliqué à une propriété d'objet, supprimera la propriété nommée de l'objet. Ce n'est pas une chose que l’on fait couramment, mais c'est possible.

1. let anObject = {left: 1, right: 2};

2. console.log(anObject.left);

3. // → 1

4. delete anObject.left;

5. console.log(anObject.left);

6. // → undefined

7. console.log("left" in anObject);

8. // → false

9. console.log("right" in anObject);

10. // → true

L’opérateur binaire in, quand il se rapporte à une chaîne de caractères ou à un objet, indique si l’objet à une propriété avec ce nom. La différence entre définir une propriété comme indéfinie (undefined) et la supprimer est que dans le premier cas, l’objet a toujours la propriété (elle n’a simplement pas une valeur très intéressante), alors que dans le second cas, la propriété n’existe plus et cela renverra une valeur « faux ».

Pour trouver les propriétés qu’un objet possède, vous pouvez utiliser la fonction Object.keys. Vous lui donnez un objet et elle renvoie un tableau de chaînes de caractères avec les noms des propriétés de l’objet.

console.log(Object.keys({x: 0, y: 0, z: 2}));
// → ["x", "y", "z"]

Il existe une fonction Object.assign qui copie toutes les propriétés d’un objet vers un autre.

1. let objectA = {a: 1, b: 2};

2. Object.assign(objectA, {b: 3, c: 4});

3. console.log(objectA);

4. // → {a: 1, b: 3, c: 4}

Les tableaux (arrays) sont donc un type d’objet spécialisé pour stocker une série de choses. Si vous testez typeof [], il produira « object ». Vous pouvez vous les représenter comme de longues pieuvres plates avec tous leurs tentacules bien alignés, étiquetés avec un numéro.

Nous allons représenter le journal tenu par Jacques comme un tableau d’objets.

1. let journal = [

2. {events: ["work", "touched tree", "pizza",

3. "running", "television"],

4. squirrel: false},

5. {events: ["work", "ice cream", "cauliflower",

6. "lasagna", "touched tree", "brushed teeth"],

7. squirrel: false},

8. {events: ["weekend", "cycling", "break", "peanuts",

9. "beer"],

10. squirrel: true},

11. /* et ainsi de suite... */

12. ];

V-F - Mutabilité

Nous allons bientôt rentrer dans la vraie programmation. Mais avant cela, il y a encore un point de théorie à comprendre.

Nous avons vu que les valeurs d’un objet peuvent être modifiées. Les types de variables ont été étudiés dans les chapitres précédents comme nombres, chaînes de caractères, booléens et sont immuables (il n’est pas possible de changer des variables de ces types).

Vous pouvez les combiner, déduire de nouvelles valeurs à partir d’elles, mais quand vous prenez une chaîne de valeur spécifique, cette valeur restera toujours la même. Le texte à l’intérieur ne peut être changé. Si vous avez une chaîne de caractères qui contient « chat », ce n’est pas possible pour un autre code de modifier deux caractères dans la chaîne pour faire « rat ».

Les objets fonctionnent différemment. Vous pouvez changer leurs propriétés de sorte qu’une seule variable peut avoir des contenus différents à des moments différents.

Quand nous avons deux nombres, 120 et 120, nous pouvons les considérer comme étant précisément le même nombre, qu’ils se réfèrent ou pas aux mêmes octets physiques. Avec des objets, il y a une différence entre avoir deux références au même objet et avoir deux objets qui contiennent la même propriété. Voyez le code suivant :

1. let objet1 = {value: 10};

2. let objet2 = object1;

3. let objet3 = {value: 10};

5. console.log(objet1 == objet2);

6. // → true

7. console.log(objet1 == objet3);

8. // → false

10. objet1.value = 15;

11. console.log(objet2.value);

12. // → 15

13. console.log(objet3.value);

14. // → 10

L’objet1 et l’objet2 liés contiennent le même objet, c’est pourquoi une modification de l’objet1 change également la valeur de l’objet2. On dit qu’ils ont la même identité. Par contre, l’objet3 pointe sur un objet différent qui au départ contient la même propriété que l’objet1 mais vit une vie séparée.

Les liens peuvent être modifiables ou constants, mais ceci est indépendant de la façon dont leurs valeurs se comportent. Même si les valeurs numériques ne changent pas, vous pouvez utiliser une variable let pour suivre l’évolution d’un nombre en modifiant la valeur vers laquelle pointe ce let. De même, une constante liée à un objet ne peut pas être changée et continuera de pointer vers le même objet, le contenu de l’objet peut changer.

1. const score = {visitors: 0, home: 0};

2. // ceci est correct

3. score.visitors = 1;

4. // ceci n’est pas permis

5. score = {visitors: 1, home: 1};

Quand vous comparez des objets JavaScript avec l’opérateur « == », il le compare par identité : il produira « vrai » uniquement si les objets ont exactement la même valeur. En comparant différents objets, il retournera « faux » même si leurs propriétés sont identiques. En JavaScript, il n’existe pas d’opération de comparaison en « profondeur » qui comparerait des objets au niveau de leur contenu, mais il est possible d’écrire cela vous-même (c’est l’objet d’un des exercices à la fin de ce chapitre).

V-F-1 - Le journal du lycanthrope

Ainsi, Jacques démarre son interpréteur JavaScript et met en place l’environnement dont il a besoin pour tenir son journal.

let journal = [];

function addEntry(events, squirrel) {
journal.push({events, squirrel});
}

Notez bien que les objets ajoutés au journal paraissent un peu étranges. Au lieu de déclarer des propriétés comme events:events, cela donne juste un nom de propriété. Ceci est un raccourci qui signifie la même chose : si le nom de la propriété dans une notation entre parenthèses n’est pas suivi d’une valeur, cette valeur est reprise du lien de même nom.

Ainsi donc, chaque soir vers 10 heures, ou parfois le matin après être descendu du dessus de son étagère, Jacques enregistre sa journée.

1. addEntry(["work", "touched tree", "pizza", "running",

2. "television"], false);

3. addEntry(["work", "ice cream", "cauliflower", "lasagna",

4. "touched tree", "brushed teeth"], false);

5. addEntry(["weekend", "cycling", "break", "peanuts",

6. "beer"], true);

Une fois qu’il a assez de données, il tente d’utiliser des statistiques pour trouver quel événement entraîne les « écureuillifications ».

La Corrélation est la mesure de la dépendance entre des variables statistiques. Une variable statistique n’est pas tout à fait la même chose qu’une variable en programmation. En statistiques, vous avez un ensemble de mesures et chaque variable est mesurée pour chaque mesure : la corrélation entre variables est le plus souvent exprimée par une valeur comprise entre -1 et 1. Une corrélation de zéro signifie que les variables ne sont pas du tout liées. Une corrélation de 1 indique que les deux variables sont parfaitement liées, quand vous avez l’une, vous avez l’autre. Une corrélation de -1 indique que les variables sont parfaitement liées mais opposées : quand l’une est vraie, l’autre est fausse.

Pour calculer la mesure de corrélation entre deux variables booléennes, nous pouvons utiliser le coefficient phi (ϕ). Il s’agit d’une formule dont l’entrée est une table de fréquence contenant le nombre de fois que les différentes variables ont été observées. Le résultat de la formule est un nombre compris entre -1 et 1 qui décrit la corrélation.

Nous pourrions prendre l’événement « manger une pizza » et avoir une table de fréquence comme ceci où chaque nombre indique le nombre de fois que la combinaison est apparue dans nos mesures :

Si nous appelons cette table n, nous pouvons calculer ϕ en utilisant la formule suivante:

(Si à ce point, vous laissez tomber le livre pour revenir sur un terrible retour en arrière sur vos cours de mathématiques du secondaire, attendez ! Je ne souhaite pas vous torturer avec des pages entières de notation cryptiques, c’est juste une formule. Et tout ce que nous allons en faire c’est la transformer en JavaScript).

La notation n01 indique le nombre de mesures où la première variable (écureuillisation - squirrel) est fausse (0) et la seconde variable (pizza) est vraie (1). Dans la table pizza, n01 est égal à 9.

La valeur n1• fait référence à la somme de toutes les mesures pour lesquelles la première variable est vraie, ce qui correspond à 5 dans notre exemple. De même, n•0 fait référence à la somme des mesures pour lesquelles la seconde variable est fausse.

Donc, pour la table pizza, la partie supérieure de la division (le dividende) serait 1×76−4×9 = 40, et la partie en dessous (le diviseur) serait la racine carrée de 5×85×10×80 ou √340000. Ce qui ne fait que ϕ ≈ 0.069, ce qui est très petit. Manger des pizzas ne semble pas avoir d’influence sur les transformations.

V-G - Calcul des corrélations

Nous pouvons représenter un tableau de 2 sur 2 en JavaScript avec quatre éléments de type Array ([76, 9, 4, 1]). Nous pouvons aussi utiliser d’autres représentations comme un tableau contenant deux doubles éléments de type Array ([[76, 9], [4, 1]]) ou un objet avec des noms de propriétés comme « 11 » et « 01 », mais le simple Array est facile et permet d’utiliser des expressions d’accès à la table agréablement courtes.

Nous allons interpréter les index du tableau comme des nombres binaires à 2 bits où le chiffre le plus à gauche (plus signifiant) fait référence à la variable squirrel et celui le plus à droite (le moins signifiant) fait référence à la variable event. Par exemple, le nombre binaire 10 fait référence au cas où Jacques s’est transformé en écureuil, mais l’événement (disons, « pizza ») n’a pas eu lieu. C’est arrivé 4 fois. Et puisque 10 en binaire est 2 en décimal, nous allons enregistrer le nombre à l’index 2 du tableau.

Ceci est une fonction qui calcule le coefficient ϕ d’un tel tableau :

1. function phi(table) {

2. return (table[3] * table[0] - table[2] * table[1]) /

3. Math.sqrt((table[2] + table[3]) *

4. (table[0] + table[1]) *

5. (table[1] + table[3]) *

6. (table[0] + table[2]));

7. }

8. console.log(phi([76, 9, 4, 1]));

9. // → 0.06859943405700354

C’est une traduction directe de la formule ϕ en JavaScript. Math.sqrt est la fonction racine carrée telle que fournie par l’objet Math dans un environnement JavaScript standard. Nous devons ajouter deux champs de la table pour obtenir des champs comme n1• parce que la somme des lignes et des colonnes n’est pas stockée directement dans notre structure de données.

Jacques a tenu son journal pendant trois mois. Les données qui en résultent sont disponibles dans le <bac à sable de codage> relatif à ce chapitre où il est stocké dans l’élément JOURNAL et dans un fichier <téléchargeable>.

Pour extraire un tableau de deux sur deux du journal, nous devons boucler sur toutes les entrées et indiquer combien de fois l’événement survient en relation avec les transformations en écureuil.

1. function tableFor(event, journal) {

2. let table = [0, 0, 0, 0];

3. for (let i = 0; i < journal.length; i++) {

4. let entry = journal[i], index = 0;

5. if (entry.events.includes(event)) index += 1;

6. if (entry.squirrel) index += 2;

7. table[index] += 1;

8. }

9. return table;

10. }

12. console.log(tableFor("pizza", JOURNAL));

13. // → [76, 9, 4, 1]

Les tableaux ont une méthode intégrée qui permet de vérifier si une valeur existe dans le tableau. La fonction ci-dessus utilise cela pour déterminer si l’événement qui nous intéresse fait partie de la liste des événements pour un jour donné.

Le corps de la boucle dans tableFor analyse dans quelle boîte de la table, chaque entrée du journal arrive, en vérifiant si l’entrée contient l’événement spécifique qui l’intéresse et quels événements arrivent en même temps que l’incident de l’écureuil. La boucle ajoute ensuite un (1) dans la boîte correspondante de la table.

Nous avons maintenant les outils dont nous avons besoin pour calculer les corrélations individuelles. La dernière étape étant de trouver une corrélation pour tout type d’événement qui a été enregistré et de voir si quelque chose en ressort.

V-H - Boucle dans un tableau

Dans la fonction tableFor, Il y a une boucle comme celle-ci :

1. for (let i = 0; i < JOURNAL.length; i++) {

2. let entry = JOURNAL[i];

3. // Fais quelque chose de cette entrée

4. }

Ce type de boucle est commun en JavaScript classique : passer en revue les éléments d’une table l’un après l’autre est quelque chose de fréquent et pour le faire, vous devez lancer un compteur de la longueur du tableau et prendre les éléments un par un.

Il y a un moyen plus simple d’écrire ce genre de boucle dans le JavaScript moderne.

1. for (let entry of JOURNAL) {

2. console.log(`${entry.events.length} events.`);

3. }

Quand une instruction for boucle comme ceci avec le mot of après la définition d’une variable, elle va boucler sur les éléments de la valeur donnée après le of. Cela ne fonctionne pas uniquement pour des tableaux, mais également pour les chaînes de caractères et certaines autres structures de données. Nous discuterons de ce fonctionnement dans le Chapitre 6.

V-I - L’analyse finale

Nous devons calculer la corrélation pour tout type d’événements qui arrivent dans l’ensemble des données. Pour faire cela, nous devons d’abord découvrir tous les types d’événements.

1. function journalEvents(journal) {

2. let events = [];

3. for (let entry of journal) {

4. for (let event of entry.events) {

5. if (!events.includes(event)) {

6. events.push(event);

7. }

8. }

9. }

10. return events;

11. }

13. console.log(journalEvents(JOURNAL));

14. // → ["carrot", "exercise", "weekend", "bread", …]

En passant en revue tous les événements et en ajoutant ceux qui ne sont pas encore dans le tableau des événements, la fonction collecte tous les types d’événements.

En utilisant cela, nous pouvons voir toutes les corrélations.

1. for (let event of journalEvents(JOURNAL)) {

2. console.log(event + ":", phi(tableFor(event, JOURNAL)));

3. }

4. // → carrot:   0.0140970969

5. // → exercise: 0.0685994341

6. // → weekend:  0.1371988681

7. // → bread:   -0.0757554019

8. // → pudding: -0.0648203724

9. // et ainsi de suite...

La plupart des corrélations semblent proches de zéro. Manger des carottes, du pain ou du pudding ne déclenche apparemment pas l’écureuil-lycanthropie. Mais il semble se passer quelque chose de plus fréquent le week-end. Filtrons les résultats pour n’avoir que les corrélations plus grandes que 0.1 et moins que -0.1.

1. for (let event of journalEvents(JOURNAL)) {

2. let correlation = phi(tableFor(event, JOURNAL));

3. if (correlation > 0.1 || correlation < -0.1) {

4. console.log(event + ":", correlation);

5. }

6. }

7. // → weekend:        0.1371988681

8. // → brushed teeth: -0.3805211953

9. // → candy:          0.1296407447

10. // → work:          -0.1371988681

11. // → spaghetti:      0.2425356250

12. // → reading:        0.1106828054

13. // → peanuts:        0.5902679812

Aha! Il y a deux facteurs avec une corrélation qui est clairement plus forte que les autres. Manger des cacahuètes a un effet positif fort sur les chances de se transformer en écureuil alors que se laver les dents à un effet négatif significatif.

Intéressant. Essayons quelque chose.

1. for (let entry of JOURNAL) {

2. if (entry.events.includes("peanuts") &&

3. !entry.events.includes("brushed teeth")) {

4. entry.events.push("peanut teeth");

5. }

6. }

7. console.log(phi(tableFor("peanut teeth", JOURNAL)));

8. // → 1

Voici un résultat fort. Le phénomène apparaît précisément quand Jacques mange des cacahuètes et oublie de se brosser les dents. Si seulement l’hygiène dentaire lui tenait à cœur, il n’aurait jamais connu cette affection.

Sachant cela, Jacques arrête complètement de manger des cacahuètes et découvre que ses transformations ne reviennent pas.

Pendant quelques années, tout va bien pour Jacques. Mais à un moment il perd son travail. Parce qu’il vit dans un pays pourri où ne pas avoir de travail signifie ne pas jouir de services médicaux, il est forcé de se produire dans un cirque où il présente l’incroyable homme-écureuil, emplissant sa bouche de beurre de cacahuète avant chaque représentation.

Un jour, n’en pouvant plus de cette misérable existence, Jacques oublie de se retransformer en humain, saute à travers un trou dans la toile du chapiteau et disparaît dans la forêt. On ne l’a plus revu.

V-J - Plus loin avec les tableaux

Avant de terminer ce chapitre, je voudrais introduire quelques autres concepts orientés objet. Je vais commencer par introduire quelques méthodes des tableaux généralement très utiles.

Nous avons vu plus haut dans ce chapitre, push et pop, qui ajoutent et enlèvent des éléments à la fin d’un tableau. Les méthodes correspondantes pour ajouter et retirer un élément au début du tableau sont appelées unshift et shift.

1. let todoList = [];

2. function remember(task) {

3. todoList.push(task);

4. }

5. function getTask() {

6. return todoList.shift();

7. }

8. function rememberUrgently(task) {

9. todoList.unshift(task);

10. }

Ce programme gère des files de tâches. Vous pouvez ajouter des tâches à la fin de la liste en faisant appel à remember("groceries") et quand vous êtes prêts à faire quelque chose, vous appelez getTask() pour récupérer (et supprimer) le premier élément de la file. La fonction rememberUrgently ajoute également une tâche mais l’ajoute au début plutôt qu’à la fin de la liste des tâches.

Pour rechercher une valeur spécifique, les tableaux fournissent une méthode indexOf. La méthode recherche dans le tableau, du début à la fin, et renvoie l’index auquel se trouve la valeur recherchée (ou -1 s’il ne l’a pas trouvée). Pour rechercher à partir de la fin plutôt qu’à partir du début, il y a une méthode similaire appelée lastIndexOf.

1. console.log([1, 2, 3, 2, 1].indexOf(2));

2. // → 1

3. console.log([1, 2, 3, 2, 1].lastIndexOf(2));

4. // → 3

Les deux fonctions indexOf et lastIndexOf acceptent un second argument optionnel qui indique à partir d’où entamer la recherche.

Une autre méthode fondamentale est slice, qui récupère les index de début et de fin et en fait un tableau qui ne reprend que les éléments compris entre eux. L’index de début est inclusif, celui de fin est exclusif.

1. console.log([0, 1, 2, 3, 4].slice(2, 4));

2. // → [2, 3]

3. console.log([0, 1, 2, 3, 4].slice(2));

4. // → [2, 3, 4]

Quand l’index de fin n’est pas donné, slice reprend tous les éléments après l’index de début. Vous pouvez également omettre l’index de début pour copier l’ensemble du tableau.

La méthode concat peut être utilisée pour coller ensemble des tableaux et créer un nouveau tableau comme l’opérateur + le fait pour des chaînes de caractères.

L’exemple suivant montre concat et slice en action : il prend un tableau et un index et il renvoie un nouveau tableau qui est une copie du tableau original, avec l’élément de l’index donné qui a été supprimé.

function remove(array, index) {
return array.slice(0, index)
.concat(array.slice(index + 1));
}
console.log(remove(["a", "b", "c", "d", "e"], 2));
// → ["a", "b", "d", "e"]

Si vous passez à concat un argument qui n’est pas un tableau, cette valeur sera ajoutée à un nouveau tableau comme s’il s’agissait d’un tableau à un seul élément.

V-K - Chaînes de caractères et leurs propriétés

Nous pouvons lire les propriétés comme length et toUpperCase à partir de valeurs de type chaînes de caractères. Mais si vous essayez d’ajouter une nouvelle propriété, cela ne fonctionne pas.

1. let kim = "Kim";

2. kim.age = 88;

3. console.log(kim.age);

4. // → undefined

Les valeurs de type chaînes de caractères, nombres et booléens ne sont pas des objets et même si le langage ne rouspète pas, si vous essayez de leur ajouter de nouvelles propriétés, il ne les enregistre pas réellement.

Comme nous l’avons mentionné plus haut, de telles valeurs sont immuables.

Mais ces types ont des propriétés prédéfinies. Chaque chaîne de caractères a un certain nombre de méthodes. Slice  et indexOf sont deux méthodes très utiles qui ressemblent aux méthodes des tableaux avec le même nom.

1. console.log("coconuts".slice(4, 7));

2. // → nut

3. console.log("coconut".indexOf("u"));

4. // → 5

Une différence est que le indexOf d’une chaîne de caractères peut rechercher une chaîne qui contient plus qu’un caractère alors que la méthode correspondante du tableau recherche un seul élément.

1. console.log("one two three".indexOf("ee"));

2. // → 11

La méthode trim supprime les espaces (espaces, passages à la ligne, tabulations et autres caractères semblables) au début et à la fin de la chaîne de caractères.

1. console.log("  okay \n ".trim());

2. // → okay

La fonction zeroPad du <chapitre précédent> existe également comme méthode. Elle est appelée padStart et utilise la longueur désirée et le caractère de remplissage comme arguments.

1. console.log(String(6).padStart(3, "0"));

2. // → 006

Vous pouvez couper une chaîne de caractères sur chaque occurrence d’une autre chaîne avec split et relier à nouveau les éléments avec join.

1. let sentence = "Secretarybirds specialize in stomping";

2. let words = sentence.split(" ");

3. console.log(words);

4. // → ["Secretarybirds", "specialize", "in", "stomping"]

5. console.log(words.join(". "));

6. // → Secretarybirds. specialize. in. stomping

Une chaîne de caractères peut être répétée avec la méthode repeat qui crée une nouvelle chaîne contenant des copies multiples de la chaîne originale collées ensemble.

1. console.log("LA".repeat(3));

2. // → LALALA

Nous avons déjà vu la propriété  length des chaînes de caractères. Accéder à chaque caractère d’une chaîne ressemble à accéder aux éléments d’un tableau (avec un avertissement dont nous discuterons au Chapitre 5).

1. let string = "abc";

2. console.log(string.length);

3. // → 3

4. console.log(string[1]);

5. // → b

V-L - Le paramètre Rest

Il peut être utile pour une fonction d’accepter n’importe quel nombre d’arguments. Par exemple, la fonction max de l’objet Math calcule le maximum dans l’ensemble des arguments qui lui sont passés.

Pour écrire une telle fonction, vous ajoutez trois points avant le dernier paramètre de la fonction comme ceci :

1. function max(...numbers) {

2. let result = -Infinity;

3. for (let number of numbers) {

4. if (number > result) result = number;

5. }

6. return result;

7. }

8. console.log(max(4, 1, 9, -2));

9. // → 9

Quand une telle fonction est appelée, le paramètre rest est lié à un tableau contenant tous les autres arguments. S’il y a d’autres paramètres devant celui-ci, leur valeur ne fait pas partie du tableau. Quand, comme dans max, c’est le seul paramètre, il contiendra tous les arguments.

Vous pouvez utiliser la même notation avec les 3 points pour appeler une fonction avec un tableau d’arguments.

1. let numbers = [5, 1, 7];

2. console.log(max(...numbers));

3. // → 7

Ceci étend l’appel de la fonction à l’ensemble du tableau passant chaque élément comme un argument isolé. Il est possible d’ajouter un tableau comme cela avec d’autres arguments comme dans :

max(9, ...numbers, 2).

La notation des tableaux avec des crochets permet également l’utilisation de l’opérateur 3 points pour étendre un tableau dans un autre.

let words = ["never", "fully"];
console.log(["will", ...words, "understand"]);
// → ["will", "never", "fully", "understand"]

V-M - L’objet Math

Comme nous l’avons vu, Math est un grand sac avec un grand nombre de fonctions bien utiles comme Math.max (maximum), Math.min (minimum) et Math.sqrt (racine carrée).

L’objet Math est utilisé comme un contenant pour regrouper un tas de fonctions connexes. Il n’y a qu’un seul objet Math et il n’est quasiment jamais utile comme variable. Voyons-le plutôt comme un espace de noms qui évite de devoir lier ces différentes fonctions et valeurs. Devoir lier globalement trop d’éléments « pollue » l’espace de noms. Plus vous donnez de noms, plus vous risquez d’écraser une valeur existante. Par exemple, il n’est pas improbable de vouloir nommer quelque chose max dans vos programmes. Comme la fonction max est rangée de manière sécurisée dans l’objet Math, il n’y a pas de crainte de l’écraser.

Beaucoup de langages vous arrêteront ou au moins vous avertiront quand vous nommerez une variable avec un nom qui est déjà donné. JavaScript le fait pour des variables déclarées avec let ou const mais pas pour des variables standards ou des variables déclarées avec var ou function.

Revenons à l’objet Math. Si vous devez faire de la trigonométrie, Math peut vous aider. Il contient cos (cosinus), sin (sinus) et tan (tangente) ainsi que leurs fonctions inverses, respectivement, acos, asin  et atan. Le nombre π (pi) (ou en tout cas une bonne approximation qui correspond aux nombres en JavaScript) est disponible via Math.PI. Il y a une ancienne tradition de nommer les constantes en lettres capitales.

1. function randomPointOnCircle(radius) {

2. let angle = Math.random() * 2 * Math.PI;

3. return {x: radius * Math.cos(angle),

4. y: radius * Math.sin(angle)};

5. }

6. console.log(randomPointOnCircle(2));

7. // → {x: 0.3667, y: 1.966}

Si les sinus et cosinus ne vous sont pas familiers, ne vous inquiétez pas. Quand ils seront utilisés dans ce livre, au Chapitre 14, je les expliquerai.

L’exemple suivant utilise Math.random. Cette fonction renvoie un nouveau nombre quasi aléatoire compris entre zéro (inclus) et un (exclus) chaque fois qu’elle est appelée.

1. console.log(Math.random());

2. // → 0.36993729369714856

3. console.log(Math.random());

4. // → 0.727367032552138

5. console.log(Math.random());

6. // → 0.40180766698904335

Bien que les ordinateurs soient des machines déterministes (ils réagissent toujours de la même manière si la même information entrante est donnée), il est possible pour eux de produire des nombres qui apparaissent aléatoires. Pour le faire, la machine garde une variable cachée et chaque fois que vous demandez un nouveau nombre aléatoire, de complexes calculs sont faits sur cette variable cachée pour créer une nouvelle valeur. Elle est stockée comme une nouvelle variable et renvoie un nombre qui en dérive. Ainsi, elle produit à chaque fois un nouveau nombre difficilement prédictible qui semble aléatoire.

Si nous souhaitons un nombre entier plutôt qu’une fraction, nous pouvons utiliser Math.floor (qui arrondit vers le bas vers le nombre entier le plus proche) sur le résultat de Math.random.

console.log(Math.floor(Math.random() * 10));
// → 2

En multipliant un nombre aléatoire par 10, cela nous donne un nombre plus grand ou égal à zéro et en dessous de 10. Comme Math.floor arrondit vers le bas, cette expression produira avec une même probabilité n’importe quel nombre de 0 à 9.

Il existe aussi les fonctions Math.ceil (arrondit vers le haut au premier entier), Math.round (au plus proche entier) et Math.abs qui prend la valeur absolue d’un nombre, ce qui signifie qu'il nie les valeurs négatives, mais laisse les positives comme elles sont.

V-N - Déstructuration

Revenons un instant à la fonction phi.

1. function phi(table) {

2. return (table[3] * table[0] - table[2] * table[1]) /

3. Math.sqrt((table[2] + table[3]) *

4. (table[0] + table[1]) *

5. (table[1] + table[3]) *

6. (table[0] + table[2]));

7. }

Une des raisons pour lesquelles cette fonction est étrange à lire, c’est que nous avons une variable pointant sur notre tableau, mais nous aurions préféré avoir des variables pointant sur les éléments du tableau, c’est  let n00 = table[0] et ainsi de suite. Heureusement, il y a un moyen rapide de faire cela en JavaScript.

1. function phi([n00, n01, n10, n11]) {

2. return (n11 * n00 - n10 * n01) /

3. Math.sqrt((n10 + n11) * (n00 + n01) *

4. (n01 + n11) * (n00 + n10));

5. }

Cela fonctionne également pour des variables créées avec let, var et const. Si vous savez que la valeur de la variable est un tableau, vous pouvez utiliser les crochets pour « regarder à l’intérieur » de la variable, en pointant sur son contenu.

Un truc équivalent existe pour les objets, en utilisant des accolades à la place des crochets.

1. let {name} = {name: "Faraji", age: 23};

2. console.log(name);

3. // → Faraji

Notez bien que si vous tentez de déstructurer un null ou undefined, vous recevez un message d’erreur comme si vous essayiez d’accéder à une propriété de ces variables.

V-O - JSON

Parce que les propriétés ne saisissent que les valeurs, plutôt que le contenu, les objets et tableaux sont stockés dans la mémoire de l’ordinateur comme des séquences de bits contenant les adresses (la place dans la mémoire) de leur contenu. Donc, un tableau qui contient un autre tableau est (au minimum) une zone de mémoire pour le tableau interne et une autre pour le tableau extérieur contenant, entre autres choses, un nombre binaire qui représente la position du tableau interne.

Si vous souhaitez sauvegarder les données dans un fichier pour plus tard ou les envoyer vers un autre ordinateur sur le réseau, vous devez en quelque sorte convertir ce fouillis d’adresses mémoire en une description qui peut être stockée ou envoyée. Vous devriez envoyer toute la mémoire de votre ordinateur avec l’adresse de la valeur qui vous intéresse, mais cela ne semble pas être la meilleure approche… Ce que nous pouvons faire, c’est sérialiser les données. Cela signifie les convertir en une description à plat. Un format assez populaire de sérialisation est appelé JSON (prononcez « Jason »), qui signifie JavaScript Object Notation. Ce format est largement utilisé comme système de stockage et de communication de données sur le Web, même dans d’autres langages que JavaScript.

JSON ressemble au mode d’écriture de tableaux et d’objets en JavaScript avec quelques restrictions. Tous les noms des propriétés doivent être entourés de guillemets et seules des expressions de données simples sont permises (pas d’appels de fonctions, de variables ou quoi que ce soit qui implique un réel calcul). Les commentaires ne sont pas admis en JSON.

Une entrée du journal ressemble à ceci quand elle est représentée en données JSON :

{
"squirrel": false,
"events": ["work", "touched tree", "pizza", "running"]
}

JavaScript nous propose les fonctions JSON.stringify et JSON.parse pour convertir des données de et vers ce format. La première prend une valeur JavaScript et renvoie une chaîne encodée JSON. La seconde prend cette chaîne et la convertit dans les valeurs encodées.

1. let string = JSON.stringify({squirrel: false,

2. events: ["weekend"]});

3. console.log(string);

4. // → {"squirrel":false,"events":["weekend"]}

5. console.log(JSON.parse(string).events);

6. // → ["weekend"]

V-P - Résumé

Objets et tableaux (qui sont une forme spécifique d’objets) fournissent des moyens de regrouper plusieurs valeurs en une seule. Conceptuellement, ceci permet de mettre un paquet de choses semblables dans un sac et de s’en aller avec le sac plutôt que de s’encombrer les bras avec plein de choses isolées en tâchant de les garder séparées.

La plupart des valeurs de JavaScript ont des propriétés, les exceptions étant null et undefined. On accède aux propriétés en utilisant valeur.prop ou valeur["prop"]. Les objets ont tendance à utiliser des noms pour leurs propriétés et stockent plus ou moins un ensemble fixe de propriétés. Les tableaux, de leur côté, contiennent généralement un nombre variable de valeurs identiques et utilisent des nombres (en commençant par 0) comme nom de leurs propriétés. Les méthodes sont des fonctions qui vivent dans les propriétés et (généralement) agissent sur leurs valeurs.

Vous pouvez parcourir un tableau un utilisant une sorte de loop...for (élément let d’un tableau)

V-Q - Exercices

V-Q-1 - La somme d’un intervalle

<L'introduction> de ce livre faisait allusion à ce qui suit comme une belle méthode pour calculer la somme d’un intervalle de nombres :

console.log(sum(range(1, 10)));

Écrivez une fonction range qui prend deux arguments, début et fin, et renvoie un tableau contenant tous les nombres depuis le début jusqu’à la fin incluse.

Ensuite, écrivez une fonction somme qui prend un tableau de nombres et renvoie la somme de ces nombres. Lancez le programme et vérifiez qu’il renvoie bien 55.

En mission bonus, modifiez votre fonction range pour prendre un troisième argument optionnel indiquant la valeur « étape » quand on construit le tableau. Si aucune étape n’est donnée, l’élément avance par incrément de un correspondant à l’ancien comportement. La fonction appelée range(1, 10, 2) devrait retourner [1, 3, 5, 7, 9]. Soyez sûr que cela fonctionne aussi avec des étapes négatives de sorte que range(5, 2, -1) produit[5, 4, 3, 2].

1. // Votre code ici.

3. console.log(range(1, 10));

4. // → [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

5. console.log(range(5, 2, -1));

6. // → [5, 4, 3, 2]

7. console.log(sum(range(1, 10)));

8. // → 55

La construction d'un tableau se fait plus facilement en initialisant d'abord une liaison à [] (un nouveau tableau vide) et en appelant à plusieurs reprises sa méthode push pour ajouter une valeur. N'oubliez pas de retourner le tableau à la fin de la fonction.

Puisque la limite finale est inclusive, vous devrez utiliser l'opérateur <= plutôt que < pour vérifier la fin de votre boucle.

Le paramètre step peut être un paramètre optionnel qui prend la valeur 1 par défaut (à l'aide de l'opérateur =).

Pour que range puisse comprendre les valeurs négatives de step, il est probablement mieux de faire deux boucles séparées - une pour compter vers le haut et une pour compter vers le bas - parce que la comparaison qui vérifie si la boucle est terminée doit être >= plutôt que <= pour compter vers le bas.

Il peut également être utile d'utiliser un autre paramètre step par défaut, à savoir -1, lorsque la fin de la plage est inférieure au début. De cette façon, range(5, 2) renvoie quelque chose de significatif, plutôt que de rester coincé dans une boucle infinie. Il est possible de se référer aux paramètres précédents dans la valeur par défaut d'un paramètre.

V-Q-2 - Renverser un tableau

Les tableaux ont une méthode reverse qui change le tableau en intervertissant l’ordre dans lequel les éléments apparaissent. Pour cet exercice, écrivez deux fonctions, reverseArray et reverseArrayInPlace. La première, reverseArray, prend un tableau comme argument et produit un nouveau tableau qui contient les mêmes éléments dans l’ordre inverse. La seconde, reverseArrayInPlace, fait ce que la méthode reverse fait : elle modifie le tableau donné en argument en renversant ses éléments. Aucune ne peut utiliser la méthode reverse standard.

En repensant aux notes sur les effets secondaires et les fonctions pures du <chapitre précédent>, quelle variante croyez-vous qui sera la plus utile dans le plus de situations ? Laquelle ira le plus vite ?

1. // Votre code ici.

3. console.log(reverseArray(["A", "B", "C"]));

4. // → ["C", "B", "A"];

5. let arrayValue = [1, 2, 3, 4, 5];

6. reverseArrayInPlace(arrayValue);

7. console.log(arrayValue);

8. // → [5, 4, 3, 2, 1]

Il y a deux façons évidentes d'implémenter le reverseArray. La première est de passer simplement en revue le tableau d'entrée de l'avant vers l'arrière et d'utiliser la méthode unshift sur le nouveau tableau pour insérer chaque élément à son début. La seconde est de boucler le tableau d'entrée vers l'arrière et d'utiliser la méthode push. Itérer à l'envers sur un tableau nécessite une spécification (quelque peu gênante), comme (let i = array.length - 1 ; i >= 0 ; i--).

Inverser le tableau en place est plus difficile. Vous devez faire attention à ne pas écraser les éléments dont vous aurez besoin par la suite. Utiliser reverseArray ou copier le tableau entier (array.slice(0) est un bon moyen de copier un tableau) fonctionne.

L'astuce consiste à échanger le premier et le dernier élément, puis le deuxième et l'avant-dernier, et ainsi de suite. Vous pouvez le faire en bouclant sur la moitié de la longueur du tableau (utilisez Math.floor pour arrondir vers le bas - vous n'avez pas besoin de toucher l'élément central d'un tableau ayant un nombre impair d'éléments) et en inversant l'élément à la position i avec celui de position array.length - 1 - i. Vous pouvez utiliser une liaison locale pour maintenir momentanément un des éléments, remplacer celui-ci par son image miroir, puis placer la valeur de la liaison locale dans l'emplacement où se trouvait l'image miroir.

V-Q-3 - Une liste

Les objets, comme ensemble générique de valeurs, peuvent être utilisés pour construire n’importe quelle sorte de structure de données. Une structure de donnée très commune est la liste (à ne pas confondre avec le tableau). Une liste est un ensemble de données imbriquées avec le premier objet contenant une référence au second, le second au troisième et ainsi de suite.

let list = {
value: 1,
rest: {
value: 2,
rest: {
value: 3,
rest: null
}
}
};

L’objet qui en résulte forme une chaîne comme ceci :

Un point intéressant à propos des chaînes est qu’elles peuvent partager une partie de leur structure. Par exemple, si je crée deux nouvelles valeurs {value: 0, rest: list} et {value: -1, rest: list} (avec list faisant référence à une variable définie précédemment), elles forment deux listes indépendantes, mais elles partagent la structure qui constitue leurs trois derniers éléments. La liste originale est également une liste valide à trois éléments.

Écrivez une fonction arrayToList qui construit une structure de liste comme celle montée quand on lui fait passer [1, 2, 3] comme arguments. Écrivez également une fonction listToArray qui produit un tableau à partir de la liste. Ajoutez ensuite une fonction auxiliaire prepend qui prend un élément et une liste et crée une nouvelle liste qui ajoute l’élément au début de la liste d’entrée. Créez également nth qui prend une liste et un nombre et renvoie l’élément à une position spécifique de la liste (avec zéro faisant référence au premier élément) ou undefined quand il n’y a pas cet élément.

Si vous en l’avez pas encore fait, écrivez également une version récursive de nth.

1. // Votre code ici.

3. console.log(arrayToList([10, 20]));

4. // → {value: 10, rest: {value: 20, rest: null}}

5. console.log(listToArray(arrayToList([10, 20, 30])));

6. // → [10, 20, 30]

7. console.log(prepend(10, prepend(20, null)));

8. // → {value: 10, rest: {value: 20, rest: null}}

9. console.log(nth(arrayToList([10, 20, 30]), 1));

10. // → 20

Il est plus facile d'établir une liste lorsqu'elle est faite de l'arrière vers l'avant. Ainsi, arrayToList pourrait itérer sur le tableau vers l'arrière (voir l'exercice précédent) et, pour chaque élément, ajouter un objet à la liste. Vous pouvez utiliser un lien local pour tenir la partie de la liste qui a été construite jusqu'à présent et utiliser une affectation comme liste = {valeur : X, rest : list} pour ajouter un élément.

Pour exécuter une liste (dans listToArray et nth), une spécification de boucle comme celle-ci peut être utilisée :

1. for (let node = list ; node ; node ; node = node.rest) {}

Voyez-vous comment ça fonctionne ? À chaque itération de la boucle, le nœud pointe vers la sous-liste courante, et le corps peut lire sa propriété de valeur pour obtenir l'élément courant. À la fin d'une itération, le nœud passe à la sous-liste suivante. Quand c'est nul, nous avons atteint la fin de la liste, et la boucle est terminée.

Votre test pour savoir si vous avez affaire à un objet réel ressemblera à quelque chose comme typeof x === "object" && x != null. Veillez à ne comparer les propriétés que lorsque les deux arguments sont des objets. Dans tous les autres cas, vous pouvez simplement retourner immédiatement le résultat de la demande ====.

Utilisez les touches Object.keys pour passer en revue les propriétés. Vous devez tester si les deux objets ont le même jeu de noms de propriétés et si ces propriétés ont des valeurs identiques. Une façon de le faire est de s'assurer que les deux objets ont le même nombre de propriétés (les longueurs des listes de propriétés sont les mêmes). Et puis, lorsque vous mettez en boucle les propriétés d'un objet pour les comparer, assurez-vous d'abord que l'autre possède bien une propriété portant ce nom. S'ils ont le même nombre de propriétés et que toutes les propriétés de l'une existent aussi dans l'autre, ils ont le même ensemble de noms de propriétés.

V-Q-4 - Comparaison profonde

L’opérateur == compare deux objets par identité. Mais parfois, vous préféreriez comparer la valeur de leurs propriétés.

Écrivez une fonction deepEqual qui prend deux valeurs et renvoie true uniquement si elles sont de même valeur ou sont des objets avec la même propriété où les valeurs des propriétés sont égales quand elles sont comparées par un appel récursif à deepEqual.

Pour savoir si des valeurs doivent être comparées directement (utilisez l’opérateur === pour cela) ou si leurs propriétés doivent être comparées, vous pouvez utiliser l’opérateur typeof. S’il retourne « object » pour les deux valeurs, vous devez effectuer une comparaison en profondeur. Mais vous devez prendre en compte une stupide exception : à cause d’un accident historique, typeof null produit également « object ».

La fonction Object.keys sera utile quand vous devrez passer dans les propriétés des objets pour les comparer.

1. // Votre code ici.

3. let obj = {here: {is: "an"}, object: 2};

4. console.log(deepEqual(obj, obj));

5. // → true

6. console.log(deepEqual(obj, {here: 1, object: 2}));

7. // → false

8. console.log(deepEqual(obj, {here: {is: "an"}, object: 2}));

9. // → true

Votre test pour savoir si vous avez affaire à un objet réel ressemblera à quelque chose comme typeof x == "object" && x != null. Veillez à ne comparer les propriétés que lorsque les deux arguments sont des objets. Dans tous les autres cas, vous pouvez simplement retourner immédiatement le résultat de la demande ===.

Utilisez les touches Object.keys pour passer en revue les propriétés. Vous devez tester si les deux objets ont le même jeu de noms de propriétés et si ces propriétés ont des valeurs identiques. Une façon de le faire est de s'assurer que les deux objets ont le même nombre de propriétés (les longueurs des listes de propriétés sont les mêmes). Et puis, lorsque vous mettez en boucle les propriétés d'un objet pour les comparer, assurez-vous d'abord que l'autre possède bien une propriété portant ce nom. S'ils ont le même nombre de propriétés et que toutes les propriétés de l'une existent aussi dans l'autre, ils ont le même ensemble de noms de propriétés.

La meilleure façon de retourner la valeur correcte de la fonction est de renvoyer immédiatement false lorsqu'une inadéquation est trouvée et de renvoyer true à la fin de la fonction.

VI - Les fonctions d'ordre supérieur

Tzu-li et Tzu-ssu se vantaient de la taille de leurs derniers programmes. « Deux cent mille lignes », dit Tzu-li, « sans compter les commentaires ! » Tzu-ssu répondit, « Pff, le mien a au moins un million de lignes déjà. Maître Yuan-Ma dit « Mon meilleur programme a cinq cents lignes ». » En entendant cela, Tzu-li et Tzu-ssu ont été éclairés.— Maître Yuan-Ma, Le Livre de la Programmation.

« Il y a deux façons de construire un logiciel : une façon est de le faire si simple qu'il n'y a pas de défauts flagrants et l'autre façon est de le faire si complexe qu'il n'y a pas de défauts flagrants. »— C.A.R. Hoare, 1980 ACM Turing Award Lecture

Un long programme est un programme coûteux et pas seulement à cause du temps qu'il faut pour le construire. La taille importante implique presque toujours la complexité, et la complexité rend les programmeurs confus. Des programmeurs confus, à leur tour, introduisent des erreurs (bugs) dans les programmes. Un long programme fournit beaucoup de place à ces bugs pour se cacher, les rendant difficiles à trouver.

Revenons aux deux derniers exemples de l'introduction. Le premier est autonome et long de six lignes.

1. let total = 0, count, 1;

2. while (count <= 10) {

3. total += count;

4. count += 1;

5. }

6. console.log(total);

Le second dépend de deux fonctions externes et ne fait qu’une ligne.

1. console.log(sum(range(1,10)));

Lequel contiendra plus probablement un bug?

Si on compte la taille des définitions de sum et range, le second programme est aussi grand — même plus grand — que le premier. Mais je dirais quand même qu’il est plus probable qu'il soit correct.

Il est plus probable qu'il soit correct, car la solution est exprimée dans un vocabulaire qui correspond au problème résolu. Faire la somme d'un intervalle de nombres n'a rien à voir avec des boucles et des compteurs. Il s'agit d'intervalles et de sommes.

Les définitions de ces vocabulaires (les fonctions sum et range) impliqueront bien sûr des boucles, compteurs et autres détails secondaires. Mais parce qu'elles expriment des concepts plus simples que le programme en entier, elles sont plus faciles à faire correctement.

VI-A - Abstraction

Dans le contexte de la programmation, ces types de vocabulaires sont habituellement appelés abstractions. Les abstractions masquent les détails et donnent la possibilité de discuter des problèmes à un niveau plus élevé (ou plus abstrait).

Pour analogie, comparez ces deux recettes d’une soupe aux pois cassés.
La première est écrite ainsi :

Mettre une tasse de pois cassés par personne dans un récipient. Ajouter de l'eau jusqu'à ce que les pois soient complètement recouverts. Laisser les pois dans l'eau pour au moins douze heures. Enlever les pois de l'eau et les mettre dans une casserole. Ajouter quatre tasses d'eau par personne. Couvrir la casserole et laisser les pois mijoter pendant deux heures. Prendre un demi-oignon par personne. Le couper en morceaux avec un couteau. L'ajouter aux pois. Prendre une branche de céleri par personne. Le couper en morceaux avec un couteau. L'ajouter aux pois. Prendre une carotte par personne. Le couper en morceaux. Avec un couteau ! L'ajouter aux pois. Cuire pendant dix minutes supplémentaires.

Et voilà la seconde recette :

Par personne : une tasse de pois cassés, un demi-oignon, une branche de céleri et une carotte. Faire tremper les pois pendant douze heures. Faire mijoter pendant deux heures dans quatre tasses d'eau (par personne). Hacher et ajouter les légumes. Cuire pendant dix minutes supplémentaires.

La seconde est plus courte et plus facile à interpréter. Mais vous devez comprendre quelques mots de cuisine supplémentaires comme faire tremper, mijoter, hacher, et, je suppose, légumes.

Lorsque l'on programme, on ne peut pas s'attendre à trouver tous les mots dont on a besoin dans le dictionnaire. Et donc, on peut se retrouver dans le cas de la première recette : identifier précisément les étapes que l'ordinateur doit exécuter, une par une, aveugle aux concepts de haut niveau qu'elles expriment.

C'est une compétence utile, en programmation, que de remarquer quand vous travaillez à un niveau d’abstraction trop bas.

VI-B - Abstraire la répétition

Les fonctions simples, comme nous en avons vu jusque-là, sont de bonnes façons de construire des abstractions. Mais des fois elles se révèlent insuffisantes.

Il est courant pour un programme de faire quelque chose un certain nombre de fois. Pour cela, vous pouvez écrire une boucle for, comme :

1. for (let i = 0; i < 10; i++) {

2. console.log(i);

3. }

Pouvons-nous abstraire « faire quelque chose N fois » en tant que fonction ? Eh bien, c'est facile d’écrire une fonction qui appelle console.log N fois.

1. function repeatLog(n) {

2. for (let i = 0; i < n; i++) {

3. console.log(i);

4. }

5. }

Mais qu'en est-il si on veut faire quelque chose d'autre qu'écrire les nombres dans des messages de journalisation ? Puisque « faire quelque chose » peut être représenté comme une fonction et que les fonctions ne sont que des valeurs, on peut passer notre action comme une valeur de fonction.

1. function repeat(n, action) {

2. for (let i = 0; i < n; i++) {

3. action(i);

4. }

5. }

7. repeat(3, console.log);

8. // → 0

9. // → 1

10. // → 2

Il n'est pas nécessaire de passer une fonction prédéfinie à repeat. Souvent, il est plus simple de créer une valeur de fonction.

1. let labels = [];

2. repeat(5, i => {

3. labels.push(`Unit ${i + 1}`);

4. });

5. console.log(labels);

6. // → ["Unit 1", "Unit 2", "Unit 3", "Unit 4", "Unit 5"]

Cette structure est un peu comme une boucle for,— elle décrit d'abord le type de boucle puis fournit un contenu. Cependant, le contenu est maintenant écrit comme une valeur de fonction, qui est comprise entre les parenthèses de l'appel à repeat. C'est pourquoi elle doit être terminée par une accolade fermante et une parenthèse fermante. Pour des cas simples comme celui-ci, où le contenu est une unique petite expression, vous pouvez aussi omettre les accolades et écrire la boucle sur une seule ligne.

VI-C - Les fonctions d'ordre supérieur

Les fonctions qui opèrent sur d'autres fonctions, soit en les prenant en tant qu'arguments soit en les retournant, sont appelées fonctions d'ordre supérieur. Puisque nous avons déjà vu que les fonctions sont des valeurs régulières, il n'y a rien de remarquable dans le fait que de telles fonctions existent. Le terme vient des mathématiques, où la différence entre fonctions et autres valeurs est prise plus au sérieux.

Les fonctions d'ordre supérieur nous permettent d'abstraire les actions, pas seulement des valeurs. Elles existent sous différentes formes. Par exemple, nous pouvons avoir des fonctions qui créent de nouvelles fonctions.

1. function greaterThan(n) {

2. return m => m > n;

3. }

4. let greaterThan10 = greaterThan(10);

5. console.log(greaterThan10(11));

6. // → true

Et nous pouvons avoir des fonctions qui modifient d'autres fonctions.

1. function noisy(f) {

2. return (...args) => {

3. console.log("calling with", args);

4. let result = f(...args);

5. console.log("called with", args, ", returned", result);

6. return result;

7. };

8. }

9. noisy(Math.min)(3, 2, 1);

10. // → calling with [3, 2, 1]

11. // → called with [3, 2, 1] , returned 1

Nous pouvons même écrire des fonctions qui fournissent de nouveaux types de flux de contrôle.

1. function unless(test, then) {

2. if (!test) then();

3. }

5. repeat(3, n => {

6. unless(n % 2 == 1, () => {

7. console.log(n, "is even");

8. });

9. });

10. // → 0 is even

11. // → 2 is even

Il y a une méthode standard pour les tableaux, forEach, qui fournit l’équivalent d'une boucle for/of comme une fonction d'ordre supérieur.

1. ["A", "B"].forEach(l => console.log(l));

2. // → A

3. // → B

VI-D - Jeu de données de scripts

Un domaine où les fonctions d'ordre supérieur excellent est le traitement de données. Pour traiter des données, nous allons avoir besoin de données réelles. Ce chapitre utilisera un jeu de données relatif aux scripts, systèmes d’écriture comme le latin, le cyrillique ou l'arabe.

Vous souvenez-vous d'Unicode vu au chapitre 1 ? Le système assigne un nombre à chaque caractère d'une langue écrite. La plupart de ces caractères sont associés à un script particulier. La norme contient 140 scripts différents, 81 sont toujours utilisés aujourd'hui, 59 sont historiques.

Bien que je ne sache que lire les caractères latins, j'ai conscience du fait que des gens écrivent des textes dans au moins 80 autres systèmes d’écriture, dont beaucoup d'entre eux que je ne pourrais pas reconnaître. Par exemple, voici un échantillon d’écriture manuscrite tamoul.

Le jeu de données de l'exemple contient des morceaux d'information relatifs aux 140 scripts définis dans Unicode. Il est disponible dans le bac à sable de codage de ce chapitre en tant que SCRIPTS binding. Le binding contient un tableau d'objets, chacun d'entre eux décrivant un script.

1. {

2. name: "Coptic",

3. ranges: [[994, 1008], [11392, 11508], [11513, 11520]],

4. direction: "ltr",

5. year: -200,

6. living: false,

7. link: "https://en.wikipedia.org/wiki/Coptic_alphabet"

8. }

Un tel objet nous fournit le nom du script, les intervalles Unicode qui lui sont assignés, la direction dans laquelle il est écrit, la date approximative d'origine, s'il est toujours utilisé et un lien vers plus d'informations. La direction peut être « ltr » pour de gauche à droite (left to right), « rtl » pour de droite à gauche (right to left)  —la façon dont les textes arabes et hébreux sont écrits — ou « ttb » pour de haut en bas (top to bottom) — comme dans l’écriture mongole.

La propriété ranges contient un tableau d'intervalles de caractères Unicode, chacun d'entre eux étant un tableau à deux éléments contenant une limite inférieure et une limite supérieure. N'importe quel code de caractère compris dans ces intervalles est assigné au script. La limite inférieure est inclusive (le code 994 est un caractère copte), et la limite supérieure est non inclusive (le code 1008 n'est pas un caractère copte).

VI-E - Filtrer les tableaux

Pour trouver, dans le jeu de données, les scripts qui sont toujours utilisés, la fonction suivante peut être utile. Elle retire les éléments d'un tableau qui ne satisfont pas une condition.

1. function filter(array, test) {

2. let passed = [];

3. for (let element of array) {

4. if (test(element)) {

5. passed.push(element);

6. }

7. }

8. return passed;

9. }

11. console.log(filter(SCRIPTS, script => script.living));

12. // → [{name: "Adlam", …}, …]

La fonction utilise l'argument appelé test, une valeur de fonction, pour compléter un « manque » dans le traitement : le processus pour décider quels éléments collecter.

Notez comment la fonction filter, plutôt que de supprimer des éléments du tableau existant, construit un nouveau tableau avec seulement les éléments qui satisfont la condition test. Cette fonction est pure. Elle ne modifie pas le tableau qui lui est donné.

Comme forEach, filter est une méthode standard pour les tableaux. L'exemple définit la fonction seulement pour montrer ce qui se passe à l’intérieur. À partir de maintenant, nous l'utiliserons plutôt comme cela :

1. console.log(SCRIPTS.filter(s => s.direction == "ttb"));

2. // → [{name: "Mongolian", …}, …]

VI-F - Transformer avec map

Assumons que nous avons un tableau d'objets représentant des scripts, tableau produit en filtrant le tableau SCRIPT de quelque façon. Mais nous voulons un tableau de noms, qui est plus simple à parcourir.

La méthode map transforme un tableau en appliquant une fonction à tous ses éléments et construit un nouveau tableau avec les valeurs résultantes. Le nouveau tableau aura la même longueur que le tableau d’entrée, mais son contenu sera associé (mapped) à une nouvelle structure par la fonction.

1. function map(array, transform) {

2. let mapped = [];

3. for (let element of array) {

4. mapped.push(transform(element));

5. }

6. return mapped;

7. }

9. let rtlScripts = SCRIPTS.filter(s => s.direction == "rtl");

10. console.log(map(rtlScripts, s => s.name));

11. // → ["Adlam", "Arabic", "Imperial Aramaic", …]

Comme forEach et filter, map est une méthode standard pour les tableaux.

VI-G - Résumer par réduction à une seule valeur

Une autre chose commune à faire avec des tableaux est de calculer une valeur unique à partir des éléments d’un tableau. Notre exemple récurrent, sommer une collection de nombres, en est un exemple. Un autre exemple est de trouver le script avec le plus de caractères.

L’opération d'ordre supérieur qui représente ce modèle est appelée reduce (quelquefois aussi appelée fold). Elle construit une valeur en prenant de manière répétitive un unique élément du tableau et en le combinant avec la valeur actuelle. En sommant des nombres, vous commencez avec le nombre zéro et, pour chaque élément, l'ajoutez à la somme.

Les paramètres de reduce sont, à part le tableau, une fonction d’accumulation et une valeur de départ. Cette fonction est un peu moins simple que filter et map, donc regardez-la de près :

1. function reduce(array, combine, start) {+

3. let current = start;

4. for (let element of array) {

5. current = combine(current, element);

6. }

7. return current;

8. }

10. console.log(reduce([1, 2, 3, 4], (a, b) => a + b, 0));

11. // → 10

La méthode standard pour les tableaux reduce, qui bien sûr correspond à cette fonction, a un avantage supplémentaire. Si votre tableau contient au moins un élément, vous pouvez omettre l'argument start. Cette méthode prendra le premier élément du tableau comme valeur de départ et commencera la réduction à partir du second élément.

1. console.log([1, 2, 3, 4].reduce((a, b) => a + b));

2. // → 10

Pour utiliser reduce (deux fois) pour trouver le script avec le plus de caractères, on peut écrire quelque chose comme :

1. function characterCount(script) {

2. return script.ranges.reduce((count, [from, to]) =>{

3. return count + (to - from);

4. }, 0);

5. }

7. console.log(SCRIPTS.reduce((a, b) => {

8. return characterCount(a) < characterCount(b) ? b : a;

9. }));

10. // → {name: "Han", …}

La fonction characterCount réduit les intervalles assignés à un script en sommant leur taille. Notez l'utilisation de la déstructuration dans la liste de paramètres de la fonction de réduction. Le deuxième appel à reduce utilise alors cela pour trouver le script le plus grand en comparant de manière répétitive deux scripts et en retournant le plus grand.

Le script han a plus de 89 000 caractères qui lui sont assignés dans la norme Unicode, en faisant de loin le plus grand système d’écriture dans le jeu de données. Han est un script quelques fois utilisé pour les textes chinois, japonais et coréens. Ces langues partagent beaucoup de caractères, cependant elles tendent à les écrire différemment. Le consortium Unicode (basé aux États-Unis) a décidé de les traiter comme un unique système d’écriture pour sauver des codes caractère. On l'appelle unification han et cela continue de faire des mécontents.

VI-H - Composition de fonctions

Considérez comment nous aurions écrit l'exemple précédent (trouver le plus large script) sans les fonctions d'ordre supérieur. Le code n'est pas bien pire.

1. let biggest = null;

2. for (let script of SCRIPTS) {

3. if (biggest == null ||

4. characterCount(biggest) < characterCount(script)) {

5. biggest = script;

6. }

7. }

8. console.log(biggest);

9. // → {name: "Han", …}

Il y a quelques affectations de données de plus, et le programme est plus long de 4 lignes. Mais il reste très lisible.

Les fonctions d'ordre supérieur commencent à briller quand vous avez besoin de composer des opérations. En tant qu'exemple, écrivons le code qui trouve l’année moyenne d'origine pour les scripts vivants et morts dans le jeu de données.

1. function average(array) {

2. return array.reduce((a, b) => a + b) / array.length;

3. }

5. console.log(Math.round(average(

6. SCRIPTS.filter(s => s.living).map(s => s.year))));

7. // → 1165

8. console.log(Math.round(average(

9. SCRIPTS.filter(s => !s.living).map(s => s.year))));

10. // → 204

Donc les scripts morts dans Unicode sont, en moyenne, plus anciens que les vivants. Ce n'est pas vraiment une statistique significative ni surprenante. Mais j’espère que vous serez d'accord que le code utilisé pour calculer cela n'est pas difficile à lire. Vous pouvez le voir comme un pipeline : on commence avec tous les scripts, on extrait les vivants (ou les morts), on récupère leur année d’origine, on calcule la moyenne et on arrondit le résultat.

Vous pouvez bien sûr aussi écrire ce calcul comme une large boucle.

1. let total = 0, count = 0;

2. for (let script of SCRIPTS) {

3. if (script.living) {

4. total += script.year;

5. count += 1;

6. }

7. }

8. console.log(Math.round(total / count));

9. // → 1165

Mais il est difficile de voir ce qui est calculé et comment. Et parce que les résultats intermédiaires ne sont pas représentés comme des valeurs cohérentes, cela demandera plus de travail pour extraire quelque chose comme average dans une fonction séparée.

En termes de ce que l'ordinateur fait vraiment, ces deux approches sont aussi très différentes. La première construira de nouveaux tableaux lors de l’exécution de filter et map, alors que la seconde calcule seulement quelques nombres, faisant moins de travail. Vous pouvez généralement vous permettre l'approche lisible, mais si vous traitez des tableaux énormes et le faites plusieurs fois, le style moins abstrait pourrait être plus performant.

VI-I - Chaînes de caractères et codage des caractères

Une utilisation du jeu de données serait de trouver quel script est utilisé dans un morceau de texte. Examinons un programme qui fait cela.

Souvenez-vous que chaque script a un tableau de codes de caractère qui lui est associé. Donc étant donné un code de caractère, nous pouvons utiliser une fonction comme la suivante pour trouver le script correspondant (s'il existe).

1. function characterScript(code) {

2. for (let script of SCRIPTS) {

3. if (script.ranges.some(([from, to]) => {

4. return code >= from && code < to;

5. })) {

6. return script;

7. }

8. }

9. return null;

10. }

12. console.log(characterScript(121));

13. // → {name: "Latin", …}

La méthode some est une autre fonction d'ordre supérieur. Elle prend une fonction prédicat et vous indique si la fonction retourne true pour n'importe quel élément dans le tableau.

Mais comment est-ce que l'on récupère les codes caractère dans une chaîne de caractères ?

Dans le Chapitre 1, j'ai mentionné que les chaînes de caractères JavaScript sont encodées comme une séquence de nombres de 16 octets. Ils sont appelés points de code. Un code de caractère Unicode était initialement supposé tenir dans une telle unité (ce qui vous donne un peu plus de 65 000 caractères). Quand il est devenu clair que cela ne sera pas suffisant, beaucoup de personnes ont hésité à utiliser plus de mémoire par caractère. Pour pallier cette insuffisance, UTF-16, le format utilisé par les chaînes de caractères JavaScript, a été inventé. Il décrit les caractères les plus communs en utilisant un unique point de code de 16 octets, mais utilise une paire de deux de ces unités pour les autres.

UTF-16 est généralement considéré comme une mauvaise idée de nos jours. Il semble presque avoir été intentionnellement conçu pour faire des erreurs. Il est facile d’écrire des programmes qui prétendent que les points de code et les caractères sont la même chose. Et si votre langue n'utilise pas les caractères à deux points de code, alors il semblera fonctionner très bien. Mais dès que quelqu'un essaye d'utiliser un tel programme avec des caractères chinois moins communs, ça casse. Heureusement, avec l’avènement des émoticônes, tout le monde a commencé à utiliser les caractères à deux points de code, et le fardeau de traiter de tels problèmes est plus équitablement réparti.

Malheureusement, les opérations courantes sur les chaînes de caractères JavaScript, telles que récupérer leur longueur avec la propriété length et accéder à leur contenu en utilisant les crochets, gèrent uniquement les points de code.

1. // Deux caractères émoticônes, cheval et chaussure

2. let horseShoe = "🐴👟";

3. console.log(horseShoe.length);

4. // → 4

5. console.log(horseShoe[0]);

6. // → (Demi-caractère invalide)

7. console.log(horseShoe.charCodeAt(0));

8. // → 55357 (Point de code du demi-caractère)

9. console.log(horseShoe.codePointAt(0));

10. // → 128052 (Réel point de code pour l'émoticône cheval)

La méthode JavaScript charCodeAt vous retourne un point de code et non pas le code de caractère complet. La méthode codePointAt, ajoutée plus tard, retourne un caractère Unicode complet. Donc, nous pouvons l'utiliser pour récupérer les caractères d'une chaîne de caractères. Mais l'argument passé à codePointAt reste un index dans la séquence d’unité de code. Donc, pour parcourir tous les caractères dans une chaîne de caractères, nous aurons toujours besoin de savoir si un caractère prend un ou deux points de code.

Dans le chapitre précédent, j'ai mentionné qu'une boucle for/of peut aussi être utilisée avec les chaînes de caractères. Comme codePointAt, ce type de boucle a été introduit à un moment où les gens étaient très au fait des problèmes liés à UTF-16. Quand vous l'utilisez pour parcourir la chaîne de caractères, elle retourne de vrais caractères et non pas des points de code.

1. let roseDragon = "🌹🐉";

2. for (let char of roseDragon) {

3. console.log(char);

4. }

5. // → 🌹

6. // → 🐉

Si vous avez un caractère (qui sera une chaîne de caractères d'un ou deux points de code), vous pouvez utiliser codePointAt(0) pour retrouver son code.

VI-J - Reconnaître du texte

Vous avez une fonction characterScript et un moyen de parcourir correctement les caractères. La prochaine étape est de compter les caractères qui appartiennent à chaque script. L'abstraction de comptage suivante sera utile ici :

1. function countBy(items, groupName) {

2. let counts = [];

3. for (let item of items) {

4. let name = groupName[item];

5. let known = counts.findIndex(c => c.name == name);

6. if (known == -1) {

7. count.push({name, count: 1}):

8. } else {

9. counts[known].count++

10. }

11. }

12. return counts;

13. }

15. console.log(countBy([1, 2, 3, 4, 5], n => n > 2));

16. // → [{name: false, count: 2}, {name: true, count: 3}]

La fonction countBy attend une collection (n'importe quoi que nous pouvons parcourir avec for/of) et une fonction qui détermine un nom de groupe pour un élément donné. Elle retourne un tableau d'objets, chaque objet désigne un groupe et vous indique le nombre d’éléments qui ont été trouvés pour ce groupe.

Elle utilise une autre méthode de tableaux — findIndex. Cette méthode est similaire à indexOf, mais au lieu de chercher une valeur particulière, elle cherche la première valeur pour laquelle la fonction fournie retourne true. Comme indexOf, elle retourne -1 quand aucun élément n'est trouvé.

En utilisant countBy, nous pouvons écrire la fonction qui nous indique quels scripts sont utilisés dans un morceau de texte.

1. function textScripts(text) {

2. let scripts = countBy(text, char => {

3. let script = characterScript(char.codePointAt(0));

4. return script ? script.name : "none";

5. }).filter(({name}) => name != "none");

7. let total = scripts.reduce((n, {count}) => n + count, 0);

8. if (total == 0) return "No scripts found";

10. return scripts.map(({name, count}) => {

11. return `${Math.round(count * 100 / total)}% ${name}`;

12. }).join(", ");

13. }

15. console.log(textScripts('英国的狗说"woof", 俄罗斯的狗说"тяв"'));

16. // → 61% Han, 22% Latin, 17% Cyrillic

La fonction compte d'abord les caractères par nom, utilisant characterScript pour leur assigner un nom et, à défaut, utilise la chaîne none pour les caractères ne faisant pas partie d'un script. L'appel à filter supprime l’entrée correspondante à none du tableau de résultats, puisque nous ne sommes pas intéressés par ces caractères.

Pour être capables de calculer des pourcentages, nous avons d'abord besoin du nombre total de caractères qui appartiennent à un script, ce que nous pouvons calculer avec reduce. Si elle ne trouve pas de tels caractères, la fonction retourne une chaîne de caractères spécifique. Sinon, elle transforme le compte des entrées en chaînes de caractères lisibles avec map et puis les combine avec join.

VI-K - Résumé

Être capable de passer des valeurs de fonctions à d'autres fonctions est un aspect extrêmement utile de JavaScript. Il permet d’écrire des fonctions qui modélisent des opérations qui contiennent des « manques ». Le code qui appelle ces fonctions peut combler ces manques en fournissant les valeurs de fonction.

Les tableaux fournissent un bon nombre de fonctions pratiques d'ordre supérieur. Vous pouvez utiliser forEach pour parcourir les éléments d'un tableau. La méthode filter retourne un nouveau tableau contenant uniquement les éléments qui satisfont la fonction prédicat. Transformer un tableau en faisant passer chaque élément au travers d'une fonction est réalisé avec map. Vous pouvez utiliser reduce pour combiner tous les éléments d'un tableau en une seule valeur. La méthode some teste si n'importe quel élément correspond à une fonction prédicat. Et findIndex trouve la position du premier élément qui satisfait un prédicat.

VI-L - Exercices

VI-L-1 - Aplatissement

Utilisez la méthode reduce en combinaison avec la méthode concat pour « aplatir » un tableau de tableaux en un seul tableau qui contient tous les éléments des tableaux d'origine.

1. let arrays = [[1, 2, 3], [4, 5], [6]];

2. // Your code here.

3. // → [1, 2, 3, 4, 5, 6]

VI-L-2 - Votre propre boucle

Écrire une fonction d'ordre supérieur loop qui fournit quelque chose comme une instruction de boucle for. Elle prend une valeur, une fonction prédicat, une fonction de mise à jour et une fonction de contenu. À chaque itération, elle exécute la fonction prédicat sur la valeur courante de la boucle et s'interrompt si la fonction retourne false. Elle appelle alors la fonction de contenu, lui passant sa valeur actuelle. Finalement, elle appelle la fonction de mise à jour pour créer une nouvelle valeur et recommence depuis le début.

Au moment de définir la fonction, vous pouvez utiliser une boucle normale pour faire le vrai bouclage.

1. // Your code here.

3. loop(3, n => n > 0, n => n - 1, console.log);

4. // → 3

5. // → 2

6. // → 1

VI-L-3 - Tout

Analogues à la méthode some, les tableaux ont aussi une méthode every. Celle-ci retourne true quand la fonction donnée retourne true pour tous les éléments dans le tableau. D'une certaine façon, some est une version de l’opérateur || qui agit sur les tableaux, et every est comme l’opérateur &&.

Implémentez every comme une fonction qui prend un tableau et une fonction prédicat en paramètres. Écrivez deux versions, une utilisant une boucle et une utilisant la méthode some.

1. function every(array, test) {

2. // Your code here.

3. }

5. console.log(every([1, 3, 5], n => n < 10));

6. // → true

7. console.log(every([2, 4, 16], n => n < 10));

8. // → false

9. console.log(every([], n => n < 10));

10. // → true

Comme l’opérateur &&, la méthode every peut arrêter d’évaluer les éléments plus en avant dès qu'elle en a trouvé un qui ne correspond pas. Donc, la version basée sur la boucle peut sortir de la boucle — avec break ou return — dès lors qu'elle rencontre un élément pour lequel la fonction prédicat retourne false. Si la boucle arrive à sa fin sans trouver un tel élément, nous savons alors que tous les éléments concordent et nous pouvons retourner true.

Pour construire every à partir de some, nous pouvons appliquer les lois de De Morgan qui énoncent que a && b égale `!(!a || !b)`. Ce qui peut être généralisé aux tableaux, où tous les éléments d'un tableau concordent, s'il n'existe pas d’élément du tableau qui ne concorde pas.

VI-L-4 - Direction d’écriture dominante

Écrire une fonction qui détermine la direction dominante d’écriture dans une chaîne d'un texte. Souvenez-vous que chaque objet script a une propriété direction qui peut être « ltr » (gauche à droite), « rtl » (droite à gauche), ou « ttb » (haut en bas).

La direction dominante est la direction pour une majorité de caractères qui ont un script qui leur est associé. Les fonctions characterScript et countBy définies plus tôt dans ce chapitre sont probablement utiles ici.

1. function dominantDirection(text) {

2. // Your code here.

3. }

5. console.log(dominantDirection("Hello!"));

6. // → ltr

7. console.log(dominantDirection("Hey, مساء الخير"));

8. // → rtl

Votre solution pourrait ressembler beaucoup à la première moitié de l'exemple textScript. Vous devez encore compter des caractères avec un critère basé sur charactereScript et puis enlever la partie des résultats qui fait référence aux caractères sans intérêt (sans script).Trouver la direction avec le plus grand nombre de caractères peut être fait avec reduce. Si le comment n'est pas clair, référez-vous à l'exemple plus haut dans ce chapitre, où reduce a été utilisé pour trouver le script avec le plus de caractères.

VII - La vie secrète des objets

VII-A - Introduction

Un type de données abstrait est réalisé en écrivant un type spécial de programme […] qui définit le type en termes d'opérations qui peuvent être effectuées dessus.
-Barbara Liskov, Programming with Abstract Data Types

On a introduit les objets JavaScript dans le chapitre 4. Dans la culture de la programmation, il existe une chose appelée la programmation orientée objet, un ensemble de techniques qui utilisent des objets (et tous les concepts en relation) comme un principe central de la structure d’un programme.

Bien qu’il y ait de nombreux différents sur sa définition précise, la programmation orientée objet a façonné la conception de nombreux langages de programmation dont le JavaScript. Ce chapitre décrira comment ces idées peuvent êtres appliquées pour ce langage.

VII-B - Encapsulation

L’idée mère de la programmation orientée objet est de fragmenter des programmes en plus petites parties et faire en sorte que chaque partie puisse gérer son propre état.

De cette manière, les détails de fonctionnement d’une partie du programme sont « local » à celle-ci. Quelqu’un qui travaille sur le reste du programme n’a pas à se rappeler ou même à être au courant de ces détails. À chaque fois que ces détails en local changent, seul le code autour de ceux-ci a besoin d’être mis à jour.

Les différentes pièces d’un tel programme interagissent entre elles à travers des interfaces, c’est-à-dire des ensembles de fonctions ou d’attaches limitées de fonctions ou de liaisons qui fournissent des fonctionnalités à un niveau plus abstrait tout en cachant leur implémentation précise.

Ces morceaux de programme sont modélisés à l’aide d’objets. Leur interface consiste en un ensemble de méthodes et de propriétés spécifiques. Ces propriétés qui font partie de l’interface sont appelées publiques (public), tandis que les autres, celles auxquelles on ne devrait pas avoir accès, sont appelées privées (private).

De nombreux langages nous permettent de distinguer les propriétés publiques et privés et d’empêcher le code extérieur d’accéder à celles qui sont privées. JavaScript, qui encore une fois prend l’approche la plus minimaliste, ne le permet pas, en tous cas pas pour l’instant. Même si on a commencé à travailler là-dessus pour l’ajouter au langage.

Bien que ce langage n’ait pas cette distinction implémentée, les programmeurs JavaScript utilisent malgré tout cette idée. Typiquement, les interfaces disponibles sont décrites dans les documentations ou les commentaires. Il est aussi courant de mettre un tiret-du-bas (_) au début des noms de propriétés pour indiquer que celles-ci sont privées.

Séparer l’interface de l’implémentation est une bonne idée. On donne généralement le nom d’encapsulation à cette pratique.

VII-C - Méthodes

Les méthodes ne sont rien de plus que des propriétés qui stockent des valeurs de fonctions. Voici une méthode très simple :

1. let rabbit = {};

2. rabbit.speak = function(line) {

3. console.log(`The rabbit says '${line}'`);

4. };

6. rabbit.speak("I'm alive.");

7. // → The rabbit says 'I'm alive.'

En général une méthode a pour but d’effectuer quelque chose avec l’objet depuis lequel elle a été appelée. Lorsqu’une fonction est appelée en tant que méthode elle est recherchée en tant que propriété puis elle est immédiatement appelée comme dans object.method() , la liaison appelée this , utilisable dans le corps de la méthode, pointe automatiquement vers l’objet sur lequel elle a été appelée.

1. function speak(line) {

2. console.log(`The ${this.type} rabbit says '${line}'`);

3. }

4. let whiteRabbit = {type: "white", speak};

5. let hungryRabbit = {type: "hungry", speak};

7. whiteRabbit.speak("Oh my ears and whiskers, " +

8. "how late it's getting!");

9. // → The white rabbit says 'Oh my ears and whiskers, how

10. //   late it's getting!'

11. hungryRabbit.speak("I could use a carrot right now.");

12. // → The hungry rabbit says 'I could use a carrot right now.'

Vous pouvez par exemple considérer this comme un paramètre supplémentaire transmis en argument d’une manière implicite. Si vous voulez le passer de manière explicite vous pouvez utiliser la méthode call , qui prend comme premier argument la valeur de this et traite normalement les autres arguments.

1. speak.call(hungryRabbit, "Burp!");

2. // → The hungry rabbit says 'Burp!'

Puisque chaque fonction a sa propre attache à this , dont la valeur dépend de la façon dont elle est appelée, vous ne pouvez pas vous référer au this global dans le cadre d’une fonction définie avec le mot clé function.

Les fonctions fléchées sont différentes : elles n’attachent pas leur propre this , mais elles peuvent voir l’attache du this autour d’elles. Ainsi vous pouvez faire quelque chose comme le code suivant, qui mentionne this depuis une fonction locale :

1. function normalize() {

2. console.log(this.coords.map(n => n / this.length));

3. }

4. normalize.call({coords: [0, 2, 3], length: 5});

5. // → [0, 0.4, 0.6]

Si j’avais écrit l’argument de la méthode map avec une fonction (en utilisant le mot-clé function ) au lieu d’une fonction fléchée, le code (this.length) n’aurait pas fonctionné.

VII-D - Prototypes

Regardez attentivement.

1. let empty = {};

2. console.log(empty.toString);

3. // → function toString(){…}

4. console.log(empty.toString());

5. // → [object Object]

J’ai récupéré une propriété depuis un objet vide. Magique !

J'ai simplement reçu des informations sur le fonctionnement des objets JavaScript. La plupart des objets ont aussi un prototype en plus de leur ensemble de propriétés. Un prototype est un autre objet qui est utilisé comme source de propriétés. Quand un objet obtient une requête pour une propriété qu’il n’a pas, son prototype cherchera cette propriété, puis le prototype de ce prototype, et ainsi de suite.

Du coup, quel est le prototype de cet objet vide ? C’est le prototype d’origine, l’entité derrière quasiment tous les objets,Object.prototype .

1. console.log(Object.getPrototypeOf({}) ==

2. Object.prototype);

3. // → true

4. console.log(Object.getPrototypeOf(Object.prototype));

5. // → null

Vous pouvez vous en douter Object.getPrototypeOf retourne le prototype d’un objet.

La relation des prototypes JavaScript forme une structure arborescente, et à la racine de celle-ci siège Object.prototype. Il fournit quelques méthodes qui sont valables pour tous les objets tels que toString, qui converti un objet en une représentation sous forme de chaîne de caractères.

Beaucoup d’objets n’ont pas directement un Object.prototype comme prototype, mais ont à la place un autre objet qui fournit différents ensembles de propriétés par défaut. Les fonctions dérivent de Fonction.prototype et les arrays dérivent de Array.prototype.

1. console.log(Object.getPrototypeOf(Math.max) ==

2. Function.prototype);

3. // → true

4. console.log(Object.getPrototypeOf([]) ==

5. Array.prototype);

6. // → true

Un tel objet prototype aura lui-même un prototype, très souvent Object.prototype. De cette manière il fournit toujours les méthodes comme toString mais indirectement.

Vous pouvez utiliser Object.create pour créer un objet avec un prototype spécifique :

1. let protoRabbit = {

2. speak(line) {

3. console.log(`The ${this.type} rabbit says '${line}'`);

4. }

5. };

6. let killerRabbit = Object.create(protoRabbit);

7. killerRabbit.type = "killer";

8. killerRabbit.speak("SKREEEE!");

9. // → The killer rabbit says 'SKREEEE!'

Une propriété telle que speak(line) dans un objet est une manière abrégée de définir une méthode. Une propriété appelée speak est en fait créée, et on lui donne une fonction comme valeur.

Le lapin (rabbit) « proto » agit comme un container pour les propriétés qui sont partagées par tous les lapins. Un objet lapin individuel tel que le lapin tueur (killer rabbit) contient des propriétés qui s’appliquent sur lui-même, en l’occurrence son type, et qui dérivent des propriétés partagées par son prototype.

VII-E - Classes

Le système prototype de JavaScript peut être interprété comme une approche quelque peu informelle d'un concept orienté objet appelé « classe ». Une classe définit la forme d'un type d'objet, ses méthodes et ses propriétés. Un tel objet s'appelle une instance de la classe.

Les prototypes sont utiles pour définir des propriétés pour lesquelles toutes les instances d’une classe partagent la même valeur : il en va de même pour les méthodes. Les propriétés différentes entre chaque instance, telles que notre propriété type sur nos lapins, doivent être stockées directement dans les objets eux-mêmes.

Pour créer une instance d’une classe donnée, il faut donc faire un objet qui dérive du prototype approprié, mais vous devez aussi faire en sorte qu’il veille sur lui-même et qu’il ait les propriétés que les instances de cette classe sont supposées avoir. C’est ce qu‘une fonction constructor (constructeur) fait :

1. function makeRabbit(type) {

2. let rabbit = Object.create(protoRabbit);

3. rabbit.type = type;

4. return rabbit;

5. }

JavaScript fournit une meilleure façon de définir ce type de fonction. Si vous mettez le mot-clé

new devant l’appel d’une fonction, cette dernière est traitée comme un constructeur. Cela signifie qu’un objet avec le prototype approprié est automatiquement créé, attaché à this dans la fonction et retourné à la fin de la fonction.

L’objet prototype utilisé lorsqu’on construit des objets peut être obtenu en récupérant la propriété prototype de la fonction « constructeur ».

1. function Rabbit(type) {

2. this.type = type;

3. }

4. Rabbit.prototype.speak = function(line) {

5. console.log(`The ${this.type} rabbit says '${line}'`);

6. };

8. let weirdRabbit = new Rabbit("weird");

Les constructeurs (en fait, toutes les fonctions) récupèrent automatiquement une propriété appelée prototype qui par défaut stocke un objet vide dérivant de Object.prototype. Vous pouvez soit la réécrire avec un nouvel objet, soit ajouter des propriétés à l’objet existant tel que le fait l’exemple.

Par convention, les noms des constructeurs sont en capitales pour qu’ils soient facilement distinguables des autres fonctions.

Il est important de comprendre la distinction entre la façon dont un prototype est associé avec un constructeur (à travers sa propriété prototype) et la façon qu’ont les objets d’avoir un prototype (qui peut être obtenu avec Object.getPrototypeOf). Le prototype actuel d’un constructeur est Function.prototype depuis que les constructeurs sont des fonctions. Sa propriété prototype stocke le prototype utilisé pour les instances créées à travers celui-ci.

1. console.log(Object.getPrototypeOf(Rabbit) ==

2. Function.prototype);

3. // → true

4. console.log(Object.getPrototypeOf(weirdRabbit) ==

5. Rabbit.prototype);

6. // → true

VII-F - La notation de classe

Les classes JavaScript sont donc des fonctions « constructeur » avec une propriété « prototype ». Voilà comment cela fonctionne, et jusqu’en 2015 c’était comme ça que vous deviez les écrire. De nos jours, on a une notation moins délicate.

1. class Rabbit {

2. constructor(type) {

3. this.type = type;

4. }

5. speak(line) {

6. console.log(`The ${this.type} rabbit says '${line}'`);

7. }

8. }

10. let killerRabbit = new Rabbit("killer");

11. let blackRabbit = new Rabbit("black");

Le mot-clé class lance la déclaration d’une classe, ce qui nous permet de définir un constructeur et un ensemble de méthodes, toutes à l’intérieur des accolades de déclaration : peu importe leur nombre. Celle intitulée constructor est traitée de manière spéciale. Elle fournit la fonction « constructeur » actuelle qui sera liée au nom Rabbit. Les autres sont empaquetées dans ce prototype de constructeur. Ainsi, la déclaration de la classe précédente est équivalente à la définition du constructeur du paragraphe précédent. C’est juste plus beau maintenant.

Les déclarations de classes n’autorisent pour l’instant que les méthodes, les propriétés qui contiennent les fonctions qui seront ajoutées au prototype. Cela peut être quelque peu problématique lorsque vous voulez sauvegarder une variable n’appartenant pas à une seule fonction. Pour l’instant, vous pouvez créer de telles propriétés en manipulant directement le prototype après que vous avez défini la classe.

Comme le mot clé function, class peut être utilisé à la fois dans les déclarations et dans les expressions. Lorsqu’il est utilisé comme expression, il n’effectue pas d’attache mais renvoie juste le constructeur comme une variable. Vous avez le droit d’omettre le nom de la classe dans l’expression d’une classe.

1. let object = new class { getWord() { return "hello"; } };

2. console.log(object.getWord());

3. // → hello

VII-G - Réécrire des propriétés dérivées

Quand vous ajoutez une propriété à un objet, qu’elle soit présente dans le prototype ou pas, la propriété est ajoutée à l’objet lui-même. S’il y avait déjà une propriété avec le même nom dans le prototype, cette propriété n’affectera désormais plus cet objet comme elle est maintenant cachée derrière la propriété de son propre objet.

1. function Rabbit(type) {

2. this.type = type;

3. }

5. Rabbit.prototype.teeth = "small";

7. let blackRabbit = new Rabbit("black");

8. let killerRabbit = new Rabbit("killer");

10. console.log(killerRabbit.teeth);

11. // → small

13. killerRabbit.teeth = "long, sharp, and bloody";

15. console.log(killerRabbit.teeth);

16. // → long, sharp, and bloody

18. console.log(blackRabbit.teeth);

19. // → small

21. console.log(Rabbit.prototype.teeth);

22. // → small

Le diagramme suivant illustre la situation après que ce code a été lancé. Les prototypes Object et Rabbit se situent derrière killerRabbit comme une sorte d’arrière-plan où les propriétés qui ne sont pas trouvées dans l’objet lui-même sont consultées.

Réécrire des propriétés qui existent dans un prototype peut être utile. En effet, comme l’exemple sur la dent de lapin le montre, la réécriture peut être utilisée pour exprimer des propriétés exceptionnelles d’une classe d’objets plus générique plutôt que laisser l’objet prendre une valeur par défaut depuis son prototype.

La réécriture est aussi utilisée pour donner aux prototypes de fonctions standards et d’array, une méthode toString différente par rapport à celle de l’objet prototype basique.

1. console.log(Array.prototype.toString ==

2. Object.prototype.toString);

3. // → false

4. console.log([1, 2].toString());

5. // → 1,2

L’appel de toString sur un Array donne un résultat similaire à l’appel de .join(","): cela met des virgules entre chaque valeur dans l’array. Appeler directement Object.prototype.toString avec un Array donne un résultat différent. Cette fonction ne sait rien sur les Arrays : elle insère donc tout simplement le mot object et le nom du type entre des crochets.

1. console.log(Object.prototype.toString.call([1, 2]));

2. // → [object Array]

VII-H - Maps

Nous avons abordé le mot-clé map utilisé dans le chapitre précédent pour une opération qui transforme une structure de données en appliquant une fonction sur ses éléments. Aussi confus que cela puisse paraître, en programmation le même mot-clé est aussi utilisé pour autre chose bien que ce quelque chose ait à voir avec la première utilisation.

Une map (nom) est une structure de données qui associe des valeurs (les clefs) avec d’autres valeurs. Par exemple, il est possible que vous vouliez associer des noms avec des ages. Il est donc possible d’utiliser des objets pour cela.

1. let ages = {

2. Boris: 39,

3. Liang: 22,

4. Júlia: 62

5. };

7. console.log(`Júlia is ${ages["Júlia"]}`);

8. // → Júlia is 62

9. console.log("Is Jack's age known?", "Jack" in ages);

10. // → Is Jack's age known? false

11. console.log("Is toString's age known?", "toString" in ages);

12. // → Is toString's age known? true

Ici, les noms de la propriété de l’objet sont les noms des personnes et les valeurs de la propriété sont leur age. Mais nous n’avons certainement pas listé les noms de chaque personne dans notre map. Cependant, comme les objets classiques dérivent de Object.prototype, il semblerait que la propriété se trouve ici.

Utiliser des objets comme maps est donc dangereux. Il y a plusieurs manières d’éviter ce problème. Premièrement, il est possible de créer des objets sans prototype. Si vous donnez null à Object.create, l’objet résultant ne dérivera plus de Object.prototype et pourra être utilisé comme map de manière sécurisée :

1. console.log("toString" in Object.create(null));

2. // → false

Les noms des propriétés « objet » doivent être des chaînes de caractères. Si vous avez besoin d’une map dont les clefs ne peuvent pas être facilement converties en chaînes de caractères tel que les objets, vous ne pouvez pas utiliser votre objet en tant que map.

Heureusement, JavaScript fourni une classe nommée Map qui a été écrite dans ce but précis. Elle stocke une map qui autorise tous les types de clefs, pas seulement les chaînes de caractères.

1. let ages = new Map();

2. ages.set("Boris", 39);

3. ages.set("Liang", 22);

4. ages.set("Júlia", 62);

6. console.log(`Júlia is ${ages.get("Júlia")}`);

7. // → Júlia is 62

8. console.log("Is Jack's age known?", ages.has("Jack"));

9. // → Is Jack's age known? false

10. console.log(ages.has("toString"));

11. // → false

Les méthodes set, get et has font partie de l’interface d’un objet Map. Écrire une structure de données qui peut être rapidement mise à jour et peut chercher un ensemble de valeurs important n’est pas facile, mais heureusement, on n’a pas besoin de s’embêter avec ça. Quelqu’un d’autre l’a fait pour nous, et on va pouvoir passer à travers cette interface très simple pour utiliser leur travail.

Si vous avez besoin de traiter un objet ordinaire comme une map, peu importent les raisons, il est important de savoir que Object.keys renvoie seulement les clefs de son propre objet et non pas ceux qui se trouvent dans le prototype. Comme alternative à l’opérateur in, vous pouvez utiliser la méthode hasOwnProperty qui ignore le prototype de l’objet.

1. console.log({x: 1}.hasOwnProperty("x"));

2. // → true

3. console.log({x: 1}.hasOwnProperty("toString"));

4. // → false

VII-I - Polymorphisme

Quand vous appelez la fonction String (qui convertit n’importe quelle valeur en chaîne de caractères) sur un objet, la méthode toString sera en fait appelé sur cet objet pour essayer de créer une chaîne de caractères compréhensible depuis celui-ci. Je tiens à le préciser, car certains des prototypes standards définissent leur propre version de toString pour qu’il puisse créer une chaîne de caractères qui contienne plus d’informations utiles que « [object Object]». Vous pouvez par ailleurs le faire vous-même.

1. function Rabbit(type) {

2. this.type = type;

3. }

5. Rabbit.prototype.speak = function (line) {

6. console.log(`The ${this.type} rabbit says '${line}'`);

7. };

9. Rabbit.prototype.toString = function () {

10. return `a ${this.type} rabbit`;

11. };

13. let blackRabbit = new Rabbit("black");

15. console.log(String(blackRabbit));

16. // → a black rabbit

Voici un exemple simple tiré d’une idée bien plus complexe. Quand un bout de code est écrit de façon à fonctionner avec des objets qui ont une interface donnée, dans notre cas une méthode toString, n’importe quel objet qui peut supporter cette interface peut être ajouté au code et cela fonctionnera parfaitement

Cette technique est appelée polymorphism. Un code polymorphique peut fonctionner avec des valeurs de différentes formes tant qu’elles sont en adéquation avec l’interface attendue.

J’ai mentionné dans le chapitre 4 qu’une boucle for/ of peut parcourir plusieurs types de structures de données. C’est un autre exemple du polymorphisme : de telles boucles s’attendent à ce que la structure de données soit constituée d’un type de données spécifique, ce que les arrays et les chaînes de caractères font ! Mais avant de pouvoir faire cela, nous devons savoir ce que sont les symboles.

VII-J - Symboles

Il est possible pour de multiples interfaces d’utiliser un même nom de propriété pour des choses différentes. Par exemple, je pourrais définir une interface dans laquelle la méthode toString convertit l’objet en une chaîne de caractères plus exhaustive. Il est donc impossible pour l’objet de se conformer à la fois à cette interface et à l’utilisation standard de toString .

Ça serait donc une mauvaise idée, et ce problème n’est pas si commun. La plupart des programmeurs JavaScript n’y pensent tout simplement pas. Mais les concepteurs de langage, dont le travail est de justement penser à ça, nous ont fourni une solution.

Lorsque j’ai dit que les noms de propriétés sont des chaînes de caractères, ce n’était pas en fait cent pour cent vrai. Bien que ce soit généralement le cas, ils peuvent aussi être des symboles. Les symboles sont des valeurs créées avec la fonction Symbol. Au contraire des chaînes de caractères les symboles nouvellement créés sont uniques, vous ne pouvez pas créer le même symbole deux fois.

1. function Rabbit(type) {

2. this.type = type;

3. }

5. Rabbit.prototype.speak = function (line) {

6. console.log(`The ${this.type} rabbit says '${line}'`);

7. };

9. Rabbit.prototype.toString = function () {

10. return `a ${this.type} rabbit`;

11. };

13. let blackRabbit = new Rabbit("black");

15. let sym = Symbol("name");

17. console.log(sym == Symbol("name"));

18. // → false

19. Rabbit.prototype[sym] = 55;

21. console.log(blackRabbit[sym]);

22. // → 55

La chaîne de caractère que vous passez à Symbol est incluse lorsque vous convertissez ce dernier en chaîne de caractère : cela peut donc être plus facile à reconnaître lorsque, par exemple, vous l’affichez dans la console. Mais il n’y a aucun sens derrière ceci, il est possible que plusieurs symboles aient le même nom.

Étant à la fois uniques et utilisables en tant que noms de propriétés, les symboles conviennent parfaitement à la définition d'interfaces pouvant cohabiter pacifiquement avec d'autres propriétés, quel que soit leur nom.

1. const toStringSymbol = Symbol("toString");

2. Array.prototype[toStringSymbol] = function() {

3. return `${this.length} cm of blue yarn`;

4. };

6. console.log([1, 2].toString());

7. // → 1,2

8. console.log([1, 2][toStringSymbol]());

9. // → 2 cm of blue yarn

Il est possible d'inclure des propriétés de symbole dans des expressions d'objet et des classes en utilisant des crochets autour du nom de la propriété. Cela provoque l'évaluation du nom de la propriété, un peu comme la notation d'accès à la propriété entre crochets, qui nous permet de faire référence à une liaison qui contient le symbole.

1. const toStringSymbol = Symbol("toString");

3. let stringObject = {

4. [toStringSymbol]() {

5. return "a jute rope";

6. }

7. };

9. console.log(stringObject[toStringSymbol]());

10. // → a jute rope

VII-K - L’interface Iterator

On attend d’un objet qu’il soit itérable lorsqu’il est donné à une boucle for /of. Cela signifie qu’il a une méthode nommée avec le symbole Symbol.iterator(une valeur symbole définie par le langage et stockée comme une propriété de la fonction Symbol).

Lorsqu'elle est appelée, cette méthode doit renvoyer un objet fournissant une deuxième interface, l'itérateur. C'est la chose réelle qui itère. Elle possède une méthode next qui renvoie le prochain résultat. Ce résultat doit être un objet avec une propriétévalue qui fournit la prochaine valeur s’il y en a une, ainsi qu’une propriété done qui doit être true lorsqu’il n’y a plus de résultat ou false dans le cas opposé.

Gardez en tête que les noms de propriétés next , value et done sont des strings natives et non pas des symboles. C’est seulement Symbol.iterator, susceptible d’être ajouté à beaucoup d’autres objets, qui est un symbole réel.

On peut directement utiliser ces interfaces soi-même.

1. let okIterator = "OK"[Symbol.iterator]();

2. console.log(okIterator.next());

3. // → {value: "O", done: false}

4. console.log(okIterator.next());

5. // → {value: "K", done: false}

6. console.log(okIterator.next());

7. // → {value: undefined, done: true}

Implémentons une structure de données itérable. Pour ce faire nous construirons une classe matrix (matrice) qui se comportera comme un array à deux dimensions.

1. class Matrix {

2. constructor(width, height, element = (x, y) => undefined) {

3. this.width = width;

4. this.height = height;

5. this.content = [];

7. for (let y = 0; y < height; y++) {

8. for (let x = 0; x < width; x++) {

9. this.content[y * width + x] = element(x, y);

10. }

11. }

12. }

14. get(x, y) {

15. return this.content[y * this.width + x];

16. }

17. set(x, y, value) {

18. this.content[y * this.width + x] = value;

19. }

20. }

La classe stocke son contenu dans un seul tableau d'éléments largeur × hauteur. Les éléments sont stockés ligne par ligne. Ainsi, par exemple, le troisième élément de la cinquième ligne est (en utilisant l’indexation basée sur zéro) stocké à la position 4 × largeur + 2.

La fonction constructeur prend en arguments une largeur, une hauteur et une fonction element optionnelle qui sera utilisée pour initialiser les valeurs. Par la suite, les méthodes get et set permettent de retrouver et de mettre à jour des éléments dans notre matrice.

Lorsque vous utilisez cette matrice dans une boucle, vous êtes autant intéressés par la position des éléments que par les éléments eux-mêmes : on va donc faire en sorte que notre itérateur renvoie des objets comprenant les propriétés X, Y et value.

1. class Matrix {

2. constructor(width, height, element = (x, y) => undefined) {

3. this.width = width;

4. this.height = height;

5. this.content = [];

7. for (let y = 0; y < height; y++) {

8. for (let x = 0; x < width; x++) {

9. this.content[y * width + x] = element(x, y);

10. }

11. }

12. }

14. get(x, y) {

15. return this.content[y * this.width + x];

16. }

18. set(x, y, value) {

19. this.content[y * this.width + x] = value;

20. }

21. }

23. class MatrixIterator {

24. constructor(matrix) {

25. this.x = 0;

26. this.y = 0;

27. this.matrix = matrix;

28. }

30. next() {

31. if (this.y == this.matrix.height) return {

32. done: true

33. };

35. let value = {

36. x: this.x,

37. y: this.y,

38. value: this.matrix.get(this.x, this.y)

39. };

40. this.x++;

41. if (this.x == this.matrix.width) {

42. this.x = 0;

43. this.y++;

44. }

45. return {

46. value,

47. done: false

48. };

49. }

50. }

52. Matrix.prototype[Symbol.iterator] = function () {

53. return new MatrixIterator(this);

54. };

56. let matrix = new Matrix(2, 2, (x, y) => `value ${x},${y}`);

58. // {x, y, value} est la déstructuration de

59. // l'objet renvoyé par MatrixIterator

60. for (let {x, y, value} of matrix) {

61. console.log(`x = ${x}, y = ${y}, ${value}`);

62. }

64. x = 0, y = 0, value 0,0

65. x = 1, y = 0, value 1,0

66. x = 0, y = 1, value 0,1

67. x = 1, y = 1, value 1,1

Comme ceci, la classe suit la progression de l’itération sur sa matrice avec ses propriétés x et y. La méthode next commence donc par vérifier si la fin de la matrice a été atteinte. Si ce n’est pas le cas, elle commence par créer l’objet qui comprend la valeur actuelle, puis elle met à jour sa position en se déplaçant à la ligne suivante si cela est nécessaire.

Nous allons maintenant faire en sorte que la classe Matrix soit itérable. À travers ce livre, j’utiliserai occasionnellement la manipulation de prototypes après coup, pour rajouter des méthodes à des classes de façon que les bouts de codes individuels restent petits et indépendants. Dans un programme normal, où il n’est pas utile de séparer le code en petits bouts, vous pouvez déclarer ces méthodes directement dans la classe à la place.

1. Matrix.prototype[Symbol.iterator] = function() {

2. return new MatrixIterator(this);

3. };

Nous pouvons maintenant boucler sur une matrice avec for / of .

1. class Matrix {

2. constructor(width, height, element = (x, y) => undefined) {

3. this.width = width;

4. this.height = height;

5. this.content = [];

7. for (let y = 0; y < height; y++) {

8. for (let x = 0; x < width; x++) {

9. this.content[y * width + x] = element(x, y);

10. }

11. }

12. }

14. get(x, y) {

15. return this.content[y * this.width + x];

16. }

18. set(x, y, value) {

19. this.content[y * this.width + x] = value;

20. }

21. }

23. class MatrixIterator {

24. constructor(matrix) {

25. this.x = 0;

26. this.y = 0;

27. this.matrix = matrix;

28. }

30. next() {

31. if (this.y == this.matrix.height) return {

32. done: true

33. };

35. let value = {

36. x: this.x,

37. y: this.y,

38. value: this.matrix.get(this.x, this.y)

39. };

40. this.x++;

41. if (this.x == this.matrix.width) {

42. this.x = 0;

43. this.y++;

44. }

45. return {

46. value,

47. done: false

48. };

49. }

50. }

52. Matrix.prototype[Symbol.iterator] = function () {

53. return new MatrixIterator(this);

54. };

56. let matrix = new Matrix(2, 2, (x, y) => `value ${x},${y}`);

58. // {x, y, value} est la déstructuration de

59. // l'objet renvoyé par MatrixIterator

60. for (let {x, y, value} of matrix) {

61. console.log(`x = ${x}, y = ${y}, ${value}`);

62. }

64. x = 0, y = 0, value 0,0

65. x = 1, y = 0, value 1,0

66. x = 0, y = 1, value 0,1

67. x = 1, y = 1, value 1,1

VII-L - Accesseurs(getters), mutateurs(setters) et statiques

Les interfaces consistent souvent en un ensemble de méthodes, mais il est aussi permis d’y inclure des propriétés qui ne contiennent pas des valeurs de fonctions. Par exemple, l’objet Map possède une propriété size qui vous dit combien de clefs sont stockées.

Il n’est même pas obligatoire pour de tels objets de calculer et de stocker ce type de propriété directement dans une instance. Même les propriétés auxquelles on accède directement peuvent masquer un appel de méthode. De telles méthodes sont appelées getters (accesseurs) et elles sont définies grâce au mot get devant le nom d’une méthode dans l’expression d’un objet ou dans une déclaration de classe.

1. let varyingSize = {

2. get size() {

3. return Math.floor(Math.random() * 100);

4. }

5. };

7. console.log(varyingSize.size);

8. // → 73

9. console.log(varyingSize.size);

10. // → 49

À chaque fois que quelque chose lit cette propriété d’objet size, la méthode associée est appelée. Vous pouvez faire de même lorsqu’une propriété est attribuée à une valeur en utilisant un setter (mutateur).

1. class Temperature {

2. constructor(celsius) {

3. this.celsius = celsius;

4. }

5. get fahrenheit() {

6. return this.celsius * 1.8 + 32;

7. }

8. set fahrenheit(value) {

9. this.celsius = (value - 32) / 1.8;

10. }

12. static fromFahrenheit(value) {

13. return new Temperature((value - 32) / 1.8);

14. }

15. }

17. let temp = new Temperature(22);

18. console.log(temp.fahrenheit);

19. // → 71.6

20. temp.fahrenheit = 86;

21. console.log(temp.celsius);

22. // → 30

La classe Temperature vous permet d’écrire et de lire la température, que se soit en degrés Celsius ou en degrés Fahrenheit, mais en interne elle stocke seulement les Celsius et converti automatiquement en Fahrenheit depuis des Celsius dans l’accesseur et le mutateur fahrenheit .

Il est possible que parfois vous vouliez relier certaines propriétés directement à votre fonction constructeur plutôt que à votre prototype. De telles méthodes n’auront pas accès à une instance de classe mais peuvent par exemple être utilisées pour fournir d’autres façons de créer des instances.

À l’intérieur de la déclaration d’une classe, les méthodes qui ont le mot clef static écrit devant leur nom sont stockées dans le constructeur. Par conséquent, la classe Temperature vous permet d’écrire Temperature.fromFahrenheit(100) pour créer une température en utilisant des degrés Fahrenheit.

VII-M - Héritage

Certaines matrices sont connues pour être symétriques. Si vous regardez l’image miroir d’une matrice symétrique avec pour axe sa diagonale partant de « en haut à gauche » et allant « en bas à droite », elle reste la même. En d’autres termes, la valeur stockée en (x,y) est toujours la même que en (y,x).

Imaginez que nous ayons besoin d’une structure de données semblable à notre classe Matrix mais qui applique le fait que la matrice est et demeure symétrique. On pourrait l’écrire depuis le départ mais cela impliquerait de se répéter dans notre code, puisqu’on écrirait quelque chose de très similaire à ce que l’on a déjà écrit.

Le système de prototypage JavaScript rend possible la création d’une nouvelle classe, ressemblant à l’ancienne, mais avec certaines redéfinitions pour certaines de ses propriétés. Le prototype de cette nouvelle classe dérive de l’ancien prototype mais rajoute une nouvelle définition pour, disons, la méthode set.

Dans les termes de la programmation orientée objet, on appelle cela l’héritage. La nouvelle classe hérite les propriétés et le comportement de l’ancienne classe.

1. class SymmetricMatrix extends Matrix {

2. constructor(size, element = (x, y) => undefined) {

3. super(size, size, (x, y) => {

4. if (x < y) return element(y, x);

5. else return element(x, y);

6. });

7. }

9. set(x, y, value) {

10. super.set(x, y, value);

11. if (x != y) {

12. super.set(y, x, value);

13. }

14. }

15. }

17. let matrix = new SymmetricMatrix(5, (x, y) => `${x},${y}`);

18. console.log(matrix.get(2, 3));

19. // → 3,2

L’utilisation du mot extend indique que cette classe ne doit pas être directement basée sur le prototype Object par défaut, mais sur une autre classe. On appelle ça la Super-classe tandis que la classe dérivée est la sous-classe.

Pour initialiser une instance SymmetricMatrix , le constructeur appelle le constructeur de sa superclass avec le mot-clef super . Cela est obligatoire car si ce nouvel objet doit se comporter (à peu près) comme la classe Matrix, il va avoir besoin des propriétés de l’instance que cette classe possède. Pour que la matrice soit symétrique, le constructeur encapsule la fonction element pour permuter les coordonnées des valeurs situées au-dessous de la diagonale.

La méthode set utilise encore une fois supermais cette fois ci non pas pour appeler le constructeur, mais pour appeler une méthode spécifique de l’ensemble de méthodes de la superclass. Nous redéfinissons set mais nous voulons quand même utiliser le comportement original. Comme this.set se réfère à la nouvelle méthode set , l’appeler ne fonctionnerait pas. À l’intérieur de méthodes de classes, super permet d’appeler des méthodes qui ont été définies dans la superclass.

L’héritage nous permet de construire des types de données légèrement différents d’autres types de données existants avec relativement peu de travail. C’est une partie fondamentale de la tradition de la programmation orientée objet, tout autant que l’encapsulation ou le polymorphisme. Bien que ces deux derniers soient généralement vus comme des inventions brillantes, l’héritage est plus controversé.

Alors que l’encapsulation et le polymorphisme peuvent être utilisés pour séparer des bouts de code les uns des autres, ce qui réduit l’enchevêtrement d’un programme global, l’héritage noue les classes entre elles ce qui crée d’autres enchevêtrements. Lorsqu’une classe hérite d’une autre, vous devez généralement en savoir plus sur la manière dont la classe parente fonctionne que si vous l’utilisiez simplement. L’héritage peut être un outil utile et je l’utilise et l’utiliserai dans mes propres programmes, mais ce ne devrait pas être le premier outil que vous recherchez, et vous ne devriez pas activement chercher des opportunités de construire des hiérarchies de classes (arbres généalogiques de classes).

VII-N - L’opérateur instanceof

Il est parfois utile de savoir si un objet a été dérivé d’une classe spécifique. Pour cela, JavaScript fournit un opérateur binaire appelé instanceof .

1. console.log(

2. new SymmetricMatrix(2) instanceof SymmetricMatrix);

3. // → true

4. console.log(new SymmetricMatrix(2) instanceof Matrix);

5. // → true

6. console.log(new Matrix(2, 2) instanceof SymmetricMatrix);

7. // → false

8. console.log([1] instanceof Array);

9. // → true

L’opérateur verra à travers les types hérités, donc SymmetricMatrix est une instance de Matrix . Il peut être aussi appliqué à des constructeurs standards comme Array, bien que presque tous les objets soient une instance de Object .

VII-O - Résumé

Ainsi, les objets font bien plus que juste contenir leurs propres propriétés. Ils ont des prototypes, qui sont en fait d’autres objets. Ils vont agir comme s’ils avaient des propriétés qu’ils n’ont pas, du moment que leur prototype les ont. Les objets simples ont comme prototype Object.prototype .

Les constructeurs, qui sont en fait des fonctions dont le nom commence habituellement par une majuscule, peuvent être utilisés avec l’opérateur new pour créer de nouveaux objets. Le prototype de l’objet créé sera l’objet trouvé dans la propriété prototype du constructeur. Vous pouvez utilisez intelligemment ceci en mettant les propriétés que toutes les valeurs d’un type donné partagent dans le prototype. Il existe la notation class qui permet de définir de façon claire un constructeur et son prototype.

Vous pouvez définir des getters et des setters pour secrètement appeler des méthodes à chaque fois que la propriété d’un objet est obtenue ou modifiée. Les méthodes statiques sont des méthodes stockées dans le constructeur de la classe, au lieu d’être stockées dans son prototype.

L’opérateur instanceof peut, pour un objet et un constructeur donné, vous dire si cet objet est une instance de ce constructeur.

Une chose utile à faire avec les objets est de leur spécifier une interface et dire à tout le monde qu’ils sont censés discuter avec votre objet seulement à travers cette interface. Les détails supplémentaires qui composent votre objet sont ainsi encapsulés, c’est-à-dire cachés derrière cette interface.

Il est possible que plusieurs types implémentent la même interface. Un code écrit pour utiliser une interface automatiquement sait comment fonctionner, peu importe le nombre d’objets différents qui fournissent l’interface. On appelle cela le polymorphisme.

Lorsqu’on implémente différentes classes qui diffèrent sur seulement quelques détails, il peut être utile d’écrire la nouvelle classe comme une sous-classe de celle existante, de façon qu’elle hérite son comportement.

VII-P - Exercices

VII-P-1 - Un type Vector

Écrivez une classe Vec qui représente un vecteur en deux dimensions. Il doit prendre comme paramètre x et y (des nombres), qui devront être sauvegardés en propriétés du même nom.

Donnez au prototype de Vec deux méthodes, plus et minus, qui prennent un autre vector en paramètre et retournent un nouveau vector comprenant la somme ou la différence de x et y des deux vectors (celui de l’objet et celui du paramètre).

Ajoutez une propriété getter nommée length au prototype qui calcule la taille du vector, c’est-à-dire la distance du point (x, y) depuis l’origine (0,0).

1. // Your code here.

3. console.log(new Vec(1, 2).plus(new Vec(2, 3)));

4. // → Vec{x: 3, y: 5}

5. console.log(new Vec(1, 2).minus(new Vec(2, 3)));

6. // → Vec{x: -1, y: -1}

7. console.log(new Vec(3, 4).length);

8. // → 5

Indices :

- regardez à nouveau l’exemple de la classe Rabbit si vous n’êtes pas certain du fonctionnement de class;

- ajouter une propriété getter au constructeur peut être accompli en mettant le mot-clef get devant le nom de la méthode. Pour calculer la distance de (0, 0) à (x, y), vous pouvez utiliser le théorème de Pythagore, qui dit que le carré de la distance que nous recherchons est égale au carré de la coordonnée x plus le carré de la coordonnée y. Ainsi la solution peut être donnée par la formule √(x2 + y2) : sachez que Math.sqrt permet de calculer une racine carrée en JavaScript.

VII-P-2 - Des groupes

L’environnement standard du JavaScript fournit une autre structure de données nommée Set. De même que pour une instance de Map, un ensemble contenant une série de valeurs. Contrairement à Map, il n’est pas possible d’associer d’autres valeurs avec celles-ci. Cela dit juste quelles valeurs font partie de l’ensemble. Une valeur peut faire partie d’un ensemble seulement une fois, l’ajouter à nouveau n’aura aucun effet.

Écrivez une classe appelée Group (puisque Set est déjà pris). Comme Set, elle a les méthodes add, delete et has. Son constructeur crée un groupe vide, la méthode add ajoute une nouvelle valeur au groupe (seulement s’il n’y a pas déjà cette valeur), delete supprime la valeur donnée en argument de la méthode du groupe (si elle y était), et has renvoie une valeur booléenne indiquant si l’argument qui lui a été passé existe.

Utilisez l’opérateur ===, ou quelque chose d’équivalent tel que indexOf, pour déterminer si deux valeurs sont identiques.

Donnez à la classe une méthode statique from qui prend un objet itérable en argument et crée un groupe qui contient toutes les valeurs produites en itérant ce dernier.

1. class Group {

2. // Your code here.

3. }

5. let group = Group.from([10, 20]);

6. console.log(group.has(10));

7. // → true

8. console.log(group.has(30));

9. // → false

10. group.add(10);

11. group.delete(10);

12. console.log(group.has(10));

13. // → false

Indices :

- la méthode la plus facile pour faire cela consiste à stocker un array contenant les membres du groupe dans une propriété d’instance. Les méthodes includes ou indexOf peuvent être utilisées pour vérifier si une valeur donnée existe dans l’array ;

- le constructeur de votre classe peut paramétrer la série d’éléments en un array vide. Lorsque add est appelée, elle vérifiera si la valeur donnée se trouve déjà dans l’array et le cas échéant elle l’ajoutera, par exemple avec push ;

- supprimer un élément d’un array dans delete est plus difficile, mais vous pouvez utilisez filter pour créer un nouvel array sans la valeur. N’oubliez pas de réécrire la propriété pour qu’elle contienne l’array nouvellement créé ;

- la méthode from peut utiliser une boucle for/ of pour récupérer les valeurs à l’extérieur d’un objet itérable et appeler add pour les mettre dans un groupe nouvellement créé.

VII-P-3 - Des groupes itérables

Faites en sorte que la classe Group de l’exercice précédent soit itérable. Référez-vous à la section sur l’interface iterator plus haut dans le chapitre si vous n’êtes pas certain de la forme exacte de l’interface.

Si vous avez utilisé un array pour représenter les membres du groupe, ne retournez pas juste un itérateur de l’interface iterator en appelant la méthode Symbol.iterator sur l’array. Cela fonctionnerait, mais ça supprime le but de cet exercice.

Il est permis que votre itérateur se comporte bizarrement lorsque le groupe est modifié pendant l’itération.

1. // Your code here (and the code from the previous exercise)

3. for (let value of Group.from(["a", "b", "c"])) {

4. console.log(value);

5. }

6. // → a

7. // → b

8. // → c

C’est probablement rentable de définir une nouvelle classe GroupIterator. Les instances d’itérateurs doivent avoir une propriété qui suit la position actuelle de l’itérateur dans le groupe. À chaque fois que next est appelée, elle vérifie si cette dernière a été déplacée sur la valeur suivant la valeur actuelle et le cas échéant elle modifie cette dernière puis la renvoie.

La classe Group récupère elle-même une méthode nommée Symbol.iterator qui, lorsqu’elle est appelée, retourne une nouvelle instance de la classe de l’itérateur pour ce groupe.

VII-P-4 - Emprunter une méthode

Plus tôt dans le chapitre, j’ai spécifié que la propriété hasOwnProperty peut être utilisée comme une alternative plus fiable à l’opérateur in quand vous voulez ignorer les propriétés du prototype. Mais que se passe-t-il si votre map a besoin d’inclure le mot clef « hasOwnProperty» ? Vous ne serez dorénavant pas capable d’appeler cette méthode, car la propriété propre à l’objet cache la valeur de cette méthode.

Pouvez-vous penser à une façon d’appeler hasOwnProperty sur un objet qui possède une propriété du même nom ?

1. let map = {one: true, two: true, hasOwnProperty: true};

3. // Fix this call

4. console.log(map.hasOwnProperty("one"));

5. // → true

Indices :

- rappelez-vous que les méthodes qui existent sur des objets ordinaires proviennent de Object.prototype;

- rappelez-vous aussi que vous pouvez appeler une fonction avec une attache this particulière en utilisant sa méthode call.