Les types en JavaScript : pour tout savoir !
Mais le JavaScript n'est pas typé ? Mais si, il y en a 13 ! Ah non, il y en 7... Bah il me semble qu'il y a Object, Function, Array, Math, String, Number, Boolean. Et tu fais quoi de RegExp ? Attends, Function c'est pas un type, c'est un sous type mais Null c'est un type... ? Ho là là...
Si vous faites du jQuery à vos heures ou même pas mal de JavaScript sur vos sites web, il est temps d'apprendre tout ce qu'il y a à savoir sur le typage implicite de JavaScript, car oui : contrairement à ce que certain vous ont dit, JavaScript manipule des éléments typés, on peut même dire que le JavaScript est faiblement typé et dynamiquement typé si vous voulez tout savoir. Les fonctions (et les instances) Object, Function, Array, Date, String, Number, Boolean, RegExp, Error ou encore les objets globaux, Math et JSON : tous sont d'un seul et même type, le type Object. Pourtant String, Number et Boolean sont eux-mêmes un type à par entière en plus des deux petits spéciaux les type Null et Undefined.
Si vous deviez retenir quelques trucs rapidement à propos du JavaScript et des types ça serait que :
- Le JavaScript n'a que 6 types : Object, Number, String, Boolean, Null et Undefined.
- À part le type Object : les 5 autres types sont dit des types primitifs.
- Les types Null et Undefined sont des types spéciaux.
- La Function n'est qu'un type Object qui peut être exécuté et instancié avec « new ».
- Array, Date et RegExp sont des types Object instanciables (Function).
- Math et JSON sont simplement un type Object (ne s'instancie pas avec « new »).
- Bien que Number, String et Boolean soient des types primitifs, il existe un équivalent de type Object instanciable (Function) pour chacun d'eux (à ne pas confondre).
Je vais dans un premier temps vous proposer la traduction d'un article de Dmitry Baranovskiy —développeur JavaScript expérimenté— qui explique très bien les types en JavaScript. Je lèverai le doute sur le fameux sixième type (Null ou Function). Et je vous fournirai des lignes de code test pour mettre en évidence ce qui a été expliqué.
Dans cet article les propos entre [ ... ] sont les miens ainsi que ceux qui ne sont pas entre « ... ».
Pour finir, bien que l'auteur vous encourage à lire les spécifications officielles (pour les initiés), je vous encourage pour ma part à lire JavaScript Eloquent (disponible en français ici).
Témoignage de Dmitry Baranovskiy, développeur JavaScript
Le billet original en anglais est sur le blogs d'Adobe (editeur de Flash)
« J'aime le JavaScript. C'est un langage alliant puissance et flexibilité, mais à condition de bien savoir l'utiliser. Une fois que vous maîtrisez le langage JavaScript, vous pouvez construire pratiquement n'importe quoi, et cela vraiment rapidement et de manière interactive. »
« Si vous pensez que le JavaScript est simple ou primitif, alors vous êtes tombé dans un piège. Vous constaterez qu'il y a pas mal de monde dans ce piège. Les soi-disant développeurs JavaScript du dimanche vous diront qu'un autre langage “X” est supérieur. Ils peuvent même vous dire que vous seriez mieux avec un système qui traduit la langue “X” en JavaScript. Pour sortir du piège et maîtriser JavaScript cela exige un effort et du dévouement. Je le sais parce que, en 1997, j'en étais là. »
« Depuis, j'ai appris en long, en large et en travers le JavaScript moi-même en étudiant les spécifications officielles. Vous pouvez apprendre l'intégralité du langage de cette manière. En tout cas, si votre intitulé de poste comprend les mots “développeur JavaScript”, vous devriez. »
Trouverez vous la réponse ?
« Dans ce billet je vais tenter de présenter un petit extrait de programme JavaScript et vous demander de prédire ce qu'il va renvoyer. Si vous êtes un développeur JavaScript, cela sera un jeu d'enfant pour vous. Si vous êtes toujours en train d'apprendre le langage, vous allez avoir quelques soucis et j'espère que vous lirez les explications suivantes. »
« Le code JavaScript suivant va afficher une fenêtre d'alerte. Que va contenir cette fenêtre ? »
var five = 5;
five.three = 3;
alert(five + five.three);
« Allez à la fin de cet article pour trouver la réponse. » En fait allez plutôt à la fin de cette partie. « Et ci-dessous, je vais laisser une explication pour vous démontrer comment JavaScript arrive à ce résultat. »
Les six types qui existent en JavaScript
« Il n'y a que 6 types en JavaScript : Object, Number, String, Boolean, Null, et Undefined. »
- « Object inclue les tableaux [Array], les fonctions [Function] et des objets ordinaires [Object, Date, RegExp, Math, …]. »
- « Number peut être un entier ou un nombre à virgule ou même les valeurs spécifiques NaN et Infinity. »
- « String inclue la chaîne vide "". » Ainsi que toute chaîne de caractère.
- « Booleans n'a que deux uniques valeurs : true et false. »
« Les deux derniers types primitifs sont un peu déroutants : »
- « La seule valeur du type Null est null. »
- « La seule valeur du type Undefined est undefined. »
Note : il est important de garder à l'esprit que les types primitifs Number _(0, -10, NaN...), String ("", "coucou"...) et Bool (true, false) ont chacun leur équivalent en Object. Je parle d'équivalence car (new String("")) n'est pas la même chose que "", le premier est de type Object alors que le second de type String. Cela s'éclairera peut-être plus loin. Pour finir il est bon de rappeler que les valeurs 0, -10, NaN, "", "coucou", function() { return; }, { bla: "", blu,"" }... sont appelés des expressions ou (opérandes) et +, -, /, ==, !==... sont appelés des opérateurs.
« Tous les types à l'exception de Object sont également appelés “primitif”. Le type d'une variable JavaScript n'est pas déclaré explicitement, il est défini par le moteur d'exécution JavaScript. Dans notre exemple, le type de la variable “five” est Number parce que nous lui avons assigné un nombre entier. »
« Exactement comme d'autres langages de programmation, JavaScript va implicitement convertir le type d'une valeur en fonction de l'opérateur qui est appliquée à la valeur. Et à la différence d'autres langages de programmation, JavaScript insiste lourdement là dessus. Par exemple le résultat de "5" - "3" est le nombre 2 parce que l'opérateur moins demande la conversion des expressions ["5" et "3"] en Number. Si un opérateur n'arrive pas à convertir [(trouver une équivalence en Number)] l'un des membres, c'est NaN (Not a Number) qui est utilisé à la place. Par exemple "5" - "Fred" est implicitement converti en 5 - Nan ce qui donne NaN. »
« L'ensemble complet des règles pour les conversions de types implicites n'est pas compliqué tant que vous savez quel type chaque opérateur exige pour les expressions qu'ils manipulent. »
Connaître et comprendre les règles de conversion implicite
« Les Object et String suivent la règle suivante : toute valeur doit être convertie en type primitif ». »
- Si le type d'une expression doit être [converti en] Number [par l'opérateur], cela signifie que le moteur JavaScript va appeler la méthode valueOf() [si l'expression n'est pas de type primitif, donc un type Object] et si le résultat [de la conversion] n'est pas un type primitif, il est converti en type String par la méthode toString(). »
- Si le type de l'opérande doit être String, cela signifie que le moteur JavaScript commence d'abord par appeler la méthode toString() et si le résultat n'est pas un type primitif, il est converti avec la méthode valueOf().
« Dans chacun des cas, si le résultat obtenu au final n'est pas un type primitif. une exception est levée. »
Voyons plutôt les conversions :
Chaque opérateur demandera la conversion de l'expression en un type primitif bien précis pour fonctionner ("if (condition)" demandera la conversion en un type Boolean de "condition", "nombre * 3" demandera la conversion en un type Number de "nombre", etc.).
_Note : L'opérateur + est « spécial ». La majorité des opérateurs imposent un type primitif à leur expression pour effectuer leur opération mais pas + qui lui, peut imposer soit des types String (concaténation), soit des types Number (addition). La condition est que si au moins une des expressions est de type String initialement, l'opérateur va demander des types String, s'il n'y a pas un seul type String initialement, l'opérateur va réclamer des types Number. Ce n'est pas le seul opérateur « spécial », par exemple l'opérateur == est très complexe dans sa réclamation de conversion d'expression.
L'expression doit être transformée en type Number
« Si le type de l'expression doit être Number, et que son type actuel est : »
- un Number : aucune conversion.
- « un Object : la valeur est convertie en un type primitif et si le résultat n'est pas un Number alors l'une des conversions suivantes [les 4 suivantes de cette liste] est appliquée. »
- « un String : la chaîne est convertie en un nombre conformément aux règles habituelles en JavaScript. » NaN dans la majorité des cas.
- « un Boolean : 1 si la valeur est true, sinon 0. »
- « un Null : 0. »
- « un Undefined : NaN »
L'expression doit être transformée en type String
« Si le type de l'expression doit être String, et que son type actuel est : »
- un String : aucune conversion.
- « un Object : la valeur est convertie en un type primitif et si le résultat n'est pas une chaîne de caractères alors l'une des conversions suivantes est appliquée. »
- « un Number : le nombre sous forme de chaîne, par exemple "123" ou "12.34". » ou "NaN".
- « un Boolean : "true" or "false". »
- « un Null : "null". »
- « un Undefined : "undefined". »
L'expression doit être transformée en type Boolean
« Si le type de l'expression doit être Boolean, et que son type actuel est : »
- un Boolean : aucune conversion.
- « un Object : true. »
- « un Number : false si la valeur est zéro [ou NaN], sinon true [même pour Infinity]. »
- « un String : false si la valeur est une chaîne vide, sinon true. »
- « un Null : false. »
- « un Undefined : false. »
Note : méfier vous ! Regardez l'exemple ci-dessous.
// A première vue, si on ne cerne pas la différence entre
// le type String (type primitif) et l'objet String (type Object),
// et bien on peut penser que ces deux lignes sont équivalentes.
var primiveString = "";
var objectString = new String("");
// Mais la première est un type String.
if (primiveString) {
console.log("true");
} else {
console.log("false"); // On passe ici !
}
// Et la conversion en Bool d'un String vide renvoi "false".
// Quant à la deuxième, elle est de type Object.
if (objectString) {
console.log("true"); // On passe ici !
} else {
console.log("false");
}
// Et la conversion en Bool d'un Object renvoi "true" quoi qu'il arrive !
// Mais par contre...
if (objectString.toString()) {
console.log("true");
} else {
console.log("false"); // On passe ici
}
//...avec toString(), on renvoi bien "false".
// car "" et (new String("")).toString() sont eux bien égaux.
L'expression doit être transformée en type Object
« Si le type de l'expression doit être Object, et que son type actuel est : »
- un Object : aucune conversion.
- « un Number : la valeur est injecté dans l'objet équivalent à Number : (new Number(value)). »
- « un String : la valeur est injecté dans l'objet équivalent à String : (new String(value)). »
- « un Boolean : la valeur est injecté dans l'objet équivalent à Boolean : (new Boolean(value)). »
- « un Null : Une exception est levé. »
- « un Undefined : Une exception est levé. »
Et la réponse est !
« Maintenant que les règles de conversions sont claires, retournons à notre exemple du début. »
var five = 5;
five.three = 3;
alert(five + five.three);
« Comme expliqué avant, la première ligne crée une variable nommée five dont le type est Number. »
« Quand l'assesseur de propriété lit five, il est converti en type Object. L'équivalent du type Number est le constructeur Number [(le type Object)] qui produit un objet [(puisque c'est une fonction)], et pas le type primitif Number. La seconde ligne de notre exemple est donc un équivalent [pour le moteur d'exécution dans ce cas précis] de : »
(new Number(five)).three = 3;
« Comme vous pouvez le constater, aucune référence à une variable n'a été faite pour l'objet new Number. Après que cette expression ai été évaluée, la propriété three de l'objet Number est abandonnée. »
« Le five.three de la troisième ligne créée un nouvel objet Number. Comme le nouvel objet n'a pas de propriété three, le type spéciale undefined (est [affectée et] retournée [et five.three est de type Undefined]. Le résultat est équivalent à : »
alert(5 + undefined);
« L'opérateur d'addition converti chacune des deux expressions en type Number. Dans ce cas undefined est converti en NaN [et 5 reste de type Number] ce qui donne : »
« alert(5 + NaN); »
« Ce qui explique pourquoi la fenêtre d'erreur dans notre exemple affiche `NaN`. »
Pour aller plus loin dans la compréhension
Que ce passerai t-il si le constructeur Number (le type Object (qui affiche 'function' testé par typeof)) possédait bel et bien une propriété nommée « three » ?
// Ajoutons au constructeur de la fonction Number (le type Object) la propriété « three ».
Number.prototype.three = 0; // Initialisation la arbitrairement à 0.
var five = 5;
five.three = 3;
alert(five + five.three);
Cette fois la sortie va t-elle être NaN ou 8 ?
Et bien en vérité elle sera 5 (défini dans le prototype). Pourquoi ? La réponse est que five.three devient bien (new Number(five)).three et qu'une propriété three existant bien : à ce moment précis de l'exécution, (new Number(five)).three ne vaut plus 0 (prototype) mais bien 3 (assignation). Cependant, une fois l'instruction terminée : le (new Number(five)) disparait et la valeur 3 de la propriété disparaît avec lui. Passé à l'instruction suivante, five est lu en tant que type Number et à five.three le moteur JavaScript recommence le processus précédent pour aboutir à 0 (pas d'assignation ici).
Obtenir 8 en forçant la valeur du prototype
Pour obtenir en sortie 8 sans toucher au code, la seule solution est de définir le Number.prototype.three à 3
Number.prototype.three = 3;
var five = 5;
five.three = 3;
alert(five + five.three);
Obtenir 8 par avec une variable intermédiaire
Pour obtenir en sortie 8 on peut également utiliser une variable intermédiaire pour « conserver » l'état de transformation (new Number(five)).three.
Number.prototype.three = 0;
var
five = 5,
temp = five.three = 3;
alert(five + temp);
Obtenir 8 par déclaration Object au lieu de Number
Pour obtenir en sortie 8 on peut également initialiser directement un type Number à partir du constructeur Number.
Number.prototype.three = 0;
var five = new Number(5);
five.three = 3;
alert(five + five.three);
Obtenir 8 en Bonus
Pour obtenir en sortie 8 on pouvait aussi faire....
alert(8);
... okay je m'arrête là :) C'était juste, pour finir en vous disant que bien que les tests ci-avant sont « inutiles » : ils sont là uniquement pour vous montrer les différents mécanismes possibles et la richesse de ceux-ci comparés à d'autres langages plus strictes. Après « c'est bien » ou « c'est mal » ; c'est une autre histoire.
Function et Null sont dans un bâteau
D'un côté, Dmitry Baranovskiy et moi-même affirmons que les 6 types en JavaScript sont :
- Object, Number, String, Boolean, Null et Undefined.
Et pourtant, vous pourrez lire, comme dans JavaScript Eloquent (ici, ligne 4) que les 6 types de JavaScript sont :
- Object, Number, String, Boolean, Function et Undefined.
Mais alors qui a raison ? S'il y a bien quelqu'un qui a raison ce sont les spécifications officielles qui liste bien que les 6 types sont Object, Number, String, Boolean, Null et Undefined.
Mais alors comment des ouvrages professionnels peuvent se tromper ? En réalité il ne se trompe pas réellement, mais ne font pas référence aux types JavaScipt mais aux valeurs que peut retourner la fonction typeof censé retourner le type d'une expression. En effet la spécification demande explicitement aux moteurs JavaScript de retourner 'function' et non pas 'object' pour une fonction (type Object) et de retourner 'object' plutôt que 'null' pour la valeur null (le type Null). Et bah franchement...
Mémo : les 6 types JavaScript
Afin de tester ce qui a été évoqué par Dmitry Baranovskiy, voici plusieurs tests.
Le type Object (et déclinaisons)
Object
/********************/
/* Le type Object */
/********************/
// Objet en JavaScript
var objectJS = new Object();
console.log(typeof objectJS); // retourne "object"
console.log(typeof Object); // retourne "function"
console.log(objectJS instanceof Object); // retourne "true"
console.log(objectJS instanceof Array); // retourne "false"
console.log(objectJS instanceof Function); // retourne "false"
// Objet en JSON
var objectJSON = {};
console.log(typeof objectJSON); // retourne "object"
console.log(typeof {}); // retourne "object" (la même chose que "new Object()")
console.log(objectJSON instanceof Object); // retourne "true"
console.log(objectJS instanceof Array); // retourne "false"
console.log(objectJS instanceof Function); // retourne "false"
Array
/**************************/
/* Le type Object : Array */
/**************************/
// Tableau en JavaScript
var arrayJS = new Array();
console.log(typeof arrayJS); // retourne "object"
console.log(typeof Array); // retourne "function"
console.log(arrayJS instanceof Object); // retourne "true"
console.log(arrayJS instanceof Array); // retourne "true"
console.log(arrayJS instanceof Function); // retourne "false"
// Tableau en JSON
var arrayJSON = [];
console.log(typeof arrayJSON); // retourne "object"
console.log(typeof []); // retourne "object" (la même chose que "new Array()")
console.log(arrayJSON instanceof Object); // retourne "true"
console.log(arrayJSON instanceof Array); // retourne "true"
console.log(arrayJSON instanceof Function); // retourne "false"
Function
/*****************************/
/* Le type Object : Function */
/*****************************/
// Fonction en JavaScript
var functionJS = function() {};
console.log(typeof functionJS); // retourne "function"
console.log(typeof Function); // retourne "function"
console.log(functionJS instanceof Object); // retourne "true"
console.log(functionJS instanceof Array); // retourne "false"
console.log(functionJS instanceof Function); // retourne "true"
Date, RegExp, Error
/****************************************/
/* Les autres types Object instanciable */
/****************************************/
// L'objet Date
var date = new Date();
console.log(typeof date); // retourne "object"
console.log(typeof Date); // retourne "function"
console.log(date instanceof Object); // retourne "true"
console.log(date instanceof Date); // retourne "true"
console.log(date instanceof Function); // retourne "false"
// L'objet RegExp
var regex = new RegExp(" ");
console.log(typeof regex); // retourne "object"
console.log(typeof RegExp); // retourne "function"
console.log(regex instanceof Object); // retourne "true"
console.log(regex instanceof RegExp); // retourne "true"
console.log(regex instanceof Function); // retourne "false"
// L'objet RegExp court
var sRegex = / /;
console.log(typeof sRegex); // retourne "object"
console.log(typeof / /); // retourne "object" (la même chose que "new RegExp()")
console.log(sRegex instanceof Object); // retourne "true"
console.log(sRegex instanceof RegExp); // retourne "true"
console.log(sRegex instanceof Function); // retourne "false"
// L'objet Error
var error = new Error();
console.log(typeof error); // retourne "object"
console.log(typeof Error); // retourne "function"
console.log(error instanceof Object); // retourne "true"
console.log(error instanceof Error); // retourne "true"
console.log(error instanceof Function); // retourne "true"
JSON, Math, Global (Window)
/**********************************/
/* Les autres types Object simple */
/**********************************/
// L'objet JSON
//var json = new JSON(); // error
console.log(typeof json); // retourne "undefined" (n'existe pas du coup)
console.log(typeof JSON); // retourne "object"
//console.log(json instanceof Object); // error
//console.log(json instanceof JSON); // error
//console.log(json instanceof Function); // error
// L'objet Math
//var math = new Math(); // error
console.log(typeof math); // retourne "undefined" (n'existe pas du coup)
console.log(typeof Math); // retourne "object"
//console.log(math instanceof Object); // error
//console.log(math instanceof Math); // error
//console.log(math instanceof Function); // error
// L'objet Global (Window dans les navigateurs)
//var globalVar = new global(); // error
console.log(typeof globalVar); // retourne "undefined" (n'existe pas du coup)
console.log(typeof global); // retourne "object"
//console.log(globalVar instanceof Object); // error
//console.log(globalVar instanceof global); // error
//console.log(globalVar instanceof Function); // error
Les types primitifs (et Objets associés)
Number
/******************/
/* Le type Number */
/******************/
// Number primitif
var pNumber = 104.56;
console.log(typeof pNumber); // retourne "number"
console.log(typeof 104.56); // retourne "number"
console.log(pNumber instanceof Object); // retourne "false"
console.log(pNumber instanceof Number); // retourne "false"
// L'Objet Number
var number = new Number(104.56);
console.log(typeof number); // retourne "object"
console.log(typeof Number); // retourne "function"
console.log(number instanceof Object); // retourne "true"
console.log(number instanceof Number); // retourne "true"
// Number primitif à 0
var pNumber0 = 0;
console.log(typeof pNumber0); // retourne "number"
console.log(pNumber0 instanceof Object); // retourne "false"
console.log(pNumber0 instanceof Number); // retourne "false"
// L'Objet Number à 0
var number0 = new Number(0);
console.log(typeof number0); // retourne "object"
console.log(number0 instanceof Object); // retourne "true"
console.log(number0 instanceof Number); // retourne "true"
// Number primitif invalide
var pNumberN = NaN;
console.log(typeof pNumberN); // retourne "number"
console.log(pNumberN instanceof Object); // retourne "false"
console.log(pNumberN instanceof Number); // retourne "false"
// L'Objet Number invalide
var numberN = new Number(NaN);
console.log(typeof numberN); // retourne "object"
console.log(numberN instanceof Object); // retourne "true"
console.log(numberN instanceof Number); // retourne "true"
// Number primitif infinie
var pNumberI = Infinity;
console.log(typeof pNumberI); // retourne "number"
console.log(pNumberI instanceof Object); // retourne "false"
console.log(pNumberI instanceof Number); // retourne "false"
// L'Objet Number infinie
var numberI = new Number(Infinity);
console.log(typeof numberI); // retourne "object"
console.log(numberI instanceof Object); // retourne "true"
console.log(numberI instanceof Number); // retourne "true"
String
/******************/
/* Le type String */
/******************/
// String primitif
var pString = "test";
console.log(typeof pString); // retourne "string"
console.log(typeof "test"); // retourne "string"
console.log(pString instanceof Object); // retourne "false"
console.log(pString instanceof String); // retourne "false"
// L'Objet String
var string = new String("test");
console.log(typeof string); // retourne "object"
console.log(typeof String); // retourne "function"
console.log(string instanceof Object); // retourne "true"
console.log(string instanceof String); // retourne "true"
// String vide primitif
var pStringE = "";
console.log(typeof pStringE); // retourne "string"
console.log(pStringE instanceof Object); // retourne "false"
console.log(pStringE instanceof String); // retourne "false"
// L'Objet vide String
var stringE = new String("");
console.log(typeof stringE); // retourne "object"
console.log(stringE instanceof Object); // retourne "true"
console.log(stringE instanceof String); // retourne "true"
Boolean
/*******************/
/* Le type Boolean */
/*******************/
// Boolean primitif à true
var pBoolTrue = true;
console.log(typeof pBoolTrue); // retourne "boolean"
console.log(typeof true); // retourne "boolean"
console.log(pBoolTrue instanceof Object); // retourne "false"
console.log(pBoolTrue instanceof Boolean); // retourne "false"
// L'Objet Boolean à true
var boolTrue = new Boolean(true);
console.log(typeof boolTrue); // retourne "object"
console.log(typeof Boolean); // retourne "function"
console.log(boolTrue instanceof Object); // retourne "true"
console.log(boolTrue instanceof Boolean); // retourne "true"
// Boolean primitif à false
var pBoolTrue = false;
console.log(typeof pBoolTrue); // retourne "boolean"
console.log(pBoolTrue instanceof Object); // retourne "false"
console.log(pBoolTrue instanceof Boolean); // retourne "false"
// L'Objet Boolean à false
var boolTrue = new Boolean(false);
console.log(typeof boolTrue); // retourne "object"
console.log(boolTrue instanceof Object); // retourne "true"
console.log(boolTrue instanceof Boolean); // retourne "true"
Null
/*******************/
/* Le type Null */
/*******************/
// Null primitif
var pNull = null;
console.log(typeof pNull); // retourne "object"
console.log(typeof null); // retourne "object"
console.log(typeof Null); // retourne "undefined"
console.log(pNull instanceof Object); // retourne "false"
//console.log(pNull instanceof Null); //error
Undefined
/*********************/
/* Le type Undefined */
/*********************/
// Undefined primitif
console.log(typeof thisVarDoesntExist); // retourne "undefined"
console.log(typeof undefined); // retourne "undefined"
console.log(typeof Undefined); // retourne "undefined"
//console.log(thisVarDoesntExist instanceof Object); //error
//console.log(pNull instanceof Undefined); //error