I. Avant propos▲
Quel est le nombre minimum de caractères différents avec lequel on est capable de coder tout et n'importe quoi en JavaScript ? C'est à cette drôle de question que je vous propose de trouver une réponse ensemble dans cet article. Si le défi ne présente pas d'intérêt en soi, il permet en revanche de manière très ludique d'appréhender divers aspects du langage sous un angle tout à fait inhabituel. Un bon niveau de base en JavaScript est requis pour la compréhension de cet article, mais je vous invite à m'envoyer un message si vous souhaitez des explications complémentaires sur certains points.
II. Caractères : les élus▲
Commençons par sélectionner les quelques caractères que nous allons utiliser. Comme en JavaScript tout ou presque est objet, il faut tout d'abord pouvoir parcourir un objet et ses propriétés. Nous pourrions utiliser le caractère « . », mais les caractères crochets « [ » et « ] » présentent bien plus d'intérêt. En effet, d'une part ils nous permettent de sélectionner une propriété par l'intermédiaire d'une variable String que l'on peut générer de manière beaucoup plus flexible, et d'autre part ils nous donnent accès au monde merveilleux des Array (tableaux).
var array =
[
1
,
2
,
3
];
// le premier usage des crochets
array[
"length"
]
// second usage afin d'accéder à la propriété, au lieu d'écrire array.length
Les tableaux en JavaScript sont réputés pour leur facilité à être déclarés et manipulés. On peut les faire contenir des tas d'objets complètement différents dans un nombre variable de dimensions. Ce grand bazar peut donner des sueurs froides à certains, mais nous sera sans aucun doute d'une grande utilité pour notre petit bricolage.
Nous avons les Array, tâchons maintenant de parvenir à d'autres types. Manipuler les nombres serait appréciable, mais il faudrait éviter de devoir inclure tous les chiffres de 0 à 9 dans notre liste. Alors comment générer tous les chiffres avec moins de caractères ? Grâce au pouvoir du type casting (conversion de type) et du meneur de la bande, le caractère « + » ! Il y a en effet très peu de choses que l'opérateur + ne sait pas faire, et il parait même que certains développeurs font de la croix tout un symbole.
Voyons donc ce que nous pouvons faire avec ces trois caractères de départ :
+[] donne 0 par cast de l'opérateur +
Pour avoir 1, on pourrait essayer de préfixer l'expression ci-dessus par l'opérateur d'incrémentation ++. Pour rappel, l'opérateur ++ incrémente une variable numérique et renvoie sa nouvelle valeur si placé devant l'opérande, ou son ancienne valeur si placé derrière l'opérande (et fait l'incrémentation par la suite). Seulement, on ne peut appliquer qu'un seul opérateur à la fois. Cependant, si on place le tout dans un array dont on récupère le premier élément pour ensuite l'incrémenter, là ça fonctionne :
1 : ++[+[]][0]
Comme on sait écrire 0, on arrive à :
1 : ++[+[]][+[]]
L'opérateur ++ permet à la fois de faire l'incrémentation et le cast en Number. On peut donc écrire 1 de manière plus courte par :
1 : ++[[]][+[]]
On peut de la même façon parvenir à 2 avec l'opérateur ++ une seconde fois :
2 : ++[++[[]][+[]][+[]]
En suivant le même principe d'incrémentation à répétition, on arrive à récupérer tous les chiffres de 0 à 9.
De plus, l'opérateur + entre deux Array ou entre un Array et un Number donne comme résultat une String. Ainsi :
"" : []+[]
"1" : []+1
"0" : []+0
Et la concaténation de String se fait elle aussi avec le symbole + ; il sait vraiment tout faire ! Puisque nous avons les chiffres et savons atteindre les String, passons aux lettres !
III. De A à Z avec trois cartouches▲
Pour atteindre les lettres, il va falloir être rusé. Nous n'avons pour le moment aucun moyen simple de passer d'un Array ou d'un nombre à un caractère entre « a » et « z ». Sans parler des majuscules. Heureusement, il existe une autre piste, celle des propriétés globales. Les propriétés globales sont l'ensemble des variables prédéclarées et accessibles partout dans tout code JavaScript. Au sein d'une page Web, elles correspondent à toutes les propriétés de l'objet Window. Vous utilisez déjà sûrement plusieurs d'entre elles comme document, alert, console... ou encore la valeur undefined, que nous pouvons obtenir avec nos trois caractères en tentant de récupérer le premier index d'un Array vide, soit [][0].
undefined : [][+[]]
Et cerise sur le gâteau, le cast en String avec +[] fonctionne sur toutes les propriétés :
"undefined" : undefined + [] soit [][+[]]+[]
Génial, undefined en chaînes de caractères, un tableau de caractères dont on va pouvoir récupérer certaines lettres via les index numériques (qu'on sait déjà coder) :
u : ( [][+[]]+[] )[0]
Nous n'avons pas inclus les parenthèses dans notre sélection de caractères, mais nous pouvons nous servir de la même technique que celle utilisée pour récupérer le chiffre 1, à savoir englober le tout dans un Array puis récupérer l'élément d'index 0 de cet Array avant de lui appliquer l'opération désirée :
[][+[]]+[] --> [[][+[]]+[]][0] --> [[][+[]]+[]][+[]]
undefined nous permet ainsi de récupérer les lettres u, n, d, e, f et i, correspondant respectivement aux index de 0 à 5 :
u : [[]+[][+[]]][+[]][+[]]
n : [[]+[][+[]]][+[]][++[[]][+[]]]
d : [[]+[][+[]]][+[]][++[++[[]][+[]]][+[]]]
e : [[]+[][+[]]][+[]][++[++[++[[]][+[]]][+[]]][+[]]]
f : [[]+[][+[]]][+[]][++[++[++[++[[]][+[]]][+[]]][+[]]][+[]]]
i : [[]+[][+[]]][+[]][++[++[++[++[++[[]][+[]]][+[]]][+[]]][+[]]][+[]]]
Nous avons également accès à NaN (Not A Number), valeur désignant une erreur de conversion après un cast en Number. Produire cette erreur est un jeu d'enfant, on peut par exemple tenter de convertir le undefined que l'on vient de récupérer : +(undefined).
NaN : +[][+[]]
"NaN" : +[][+[]]+[]
Ce qui nous donne deux nouvelles lettres dans notre besace :
N : (+[][+[]]+[])[0] ? [+[][+[]]+[]][0][0] ? [+[][+[]]+[]][+[]][+[]]
a : [+[]+[][+[]]+[]][+[]][++[[]][+[]]]
Qu'avons-nous d'autre ? Hmm, il y a bien null, mais cette valeur est bien plus difficile à récupérer que ses consoeurs car habituellement attribuée manuellement par le développeur pour indiquer l'inexistence d'un objet à un emplacement où on peut légitimement s'attendre à en trouver un. Et puis, cela ne nous offrirait que la lettre « l », c'est un peu radin ! Mais alors sommes-nous déjà bloqués ? Non, il reste un dernier tour dans le sac !
IV. To Infinity and Beyond !▲
Bien sûr que JavaScript sait compter jusqu'à l'infini. Disons juste qu'à partir de +1.7976931348623157e+308 (ou Number.MAX_VALUE), il ne fait plus trop la différence. Ne lui en voulez pas trop pour ça, c'est déjà beaucoup, et cela va bien arranger nos affaires. Car Infinity peut lui aussi être converti en String pour être désossé et piller ses précieuses lettres. Mais comment atteindre l'infini ? Certainement pas avec la même technique que celle qui nous a permis de 1 en 1 à arriver au chiffre 9. Même si c'est théoriquement possible, la longueur du code en résultant devrait certainement avoisiner la distance entre la Terre et la galaxie d'Andromède. Et encore, en Arial Narrow 8px. Mais pas de panique, nous avons à disposition cette merveilleuse invention qu'est la notation scientifique et qui a sauvé de nombreuses craies entre les mains d'astrophysiciens.
Prenons 1e1000. Cela correspond au nombre 10000000... avec 1000 zéros derrière ! Et voilà comment avec six caractères, on atteint l'infini (ou pas loin).
Infinity : +("1e1000") --> +("1"+"e"+"1"+"0"+"0"+"0") --> +[++[+[]][+[]]+[]+[[]+[][+[]]][+[]][++[++[++[+[]][+[]]][+[]]][+[]]]+[++[+[]][+[]]+[]][+[]]+[+[]]+[+[]]+[+[]]][+[]]
I : [Infinity+[]][0][0]
t : [Infinity+[]][0][6]
y : [Infinity+[]][0][7]
Oui, je sais ce que vous vous dites, ça ressemble de plus en plus à un langage extra-terrestre. Rassurez-vous, ce n'est que le début. Nous avons en effet fait le tour de toutes les possibilités (à moins que ?) avec ces trois caractères. Pour aller plus loin, il va falloir agrandir la bande.
V. To be true or not to be true▲
Un type primitif avec lequel nous n'avons pas encore travaillé est le booléen. Et il y a de quoi lorgner sur les lettres t, r, l et s de true et false. Alors sans plus attendre, ajoutons l'opérateur de prédilection des booléens, le point d'exclamation ! Cela me permet de récupérer :
false : ![]
true : !![]
t : [true+[]][0][0] --> [!![]+[]][+[]][+[]]
r : [true+[]][0][1]
l : [false+[]][0][2]
s : [false+[]][0][3]
On peut aussi gagner quelques caractères sur l'écriture des chiffres, en remplaçant l'opérateur ++ par un simple +true :
1 : +!+[]
2 : +!+[]+!![]
3 : +!+[]+!![]+!![]
4 : +!+[]+!![]+!![]+!![] etc...
Nous disposons maintenant de 15 lettres, essayons à présent de faire quelque chose avec. Autrement dit de récupérer une propriété d'un objet global auquel nous avons déjà accès. Après avoir épluché le prototype de Array, Number, String et Boolean, voici enfin le Graal : nous avons de quoi écrire « filter » de Array.filter. En quoi la fonction filter nous intéresse ? Et bien ce n'est pas tant le rôle de la fonction mais surtout le fait qu'il s'agisse d'une fonction, qui une fois castée en String fait pleuvoir un déluge de nouveaux caractères.
A partir de là, le résultat varie un peu selon les navigateurs, car le cast en String des fonctions n'est pas rigoureusement standardisé. Nous perdons donc ici la portabilité du code. Pour la suite de cet article, je me baserais sur les résultats du navigateur Google Chrome.
[]["filter"]+[] --> "function filter() { [native code] }"
Jackpot ! Nous récupérons le c, le o et le v, mais aussi le caractère espace ainsi que les crochets, les accolades et les parenthèses.
VI. Exécution▲
Ne perdons pas de vue notre objectif. Il s'agit de traduire n'importe quel code en un minimum de caractères, tout en s'assurant qu'il soit toujours exécutable et fonctionnel. À présent, nous avons suffisamment de caractères à disposition pour recomposer sous forme de String des bouts de codes simples. Mais comment les exécuter ? Ce qui vient tout de suite à l'esprit, c'est la fonction eval. Seulement, il faut également savoir exécuter cette fameuse fonction eval ! Nous avons donc absolument besoin des parenthèses pour aller plus loin. Nous les avons déjà au format String, mais sans moyen de les évaluer cela ne nous est pas d'une grande utilité.
Rajoutons donc les caractères parenthèses à notre alphabet de base, ce qui porte le total à six caractères : [ ] + ! ( ).
Les parenthèses vont, comme le signe !, simplifier certaines expressions précédentes. On n'aura par exemple plus besoin de la petite astuce consistant à englober une expression dans un Array dont on récupère l'élément d'index zéro ensuite. Les parenthèses sont là pour ça :
f : (![]+[])[+[]]
u : ([][+[]]+[])[+[]]
Mais surtout, nous allons enfin pouvoir appeler des fonctions. Et celle qui nous intéresse le plus est Window.eval, pour que nos String reconstituées puissent être exécutés en tant que code. Nous avons les caractères pour faire « eval », mais pas encore de quoi récupérer l'objet Window auquel la méthode appartient. Pas de problème, il existe un autre moyen d'évaluer une String en tant que code, c'est de passer par le constructeur Function. Constructeur que nous pouvons récupérer tout simplement avec la propriété « constructor » de n'importe quelle fonction. Parfait, nous avons les lettres pour « constructor », et nous avons d'ores et déjà utilisé la fonction Array.filter !
Function : []["filter"]["constructor"]
Evaluer du code : []["filter"]["constructor"]("code")();
Notez que le constructeur Function crée une fonction anonyme à partir du code donné en argument sous forme de String, et qu'il faut encore utiliser les parenthèses derrière pour exécuter cette fonction fraîchement créée. Le scope de cette fonction sera le scope global, ainsi le code suivant renverra l'objet Window :
Window : []["filter"]["constructor"]("return this")()
Function est notre clé passe-partout. En effet, cela devient un jeu d'enfant de récupérer les caractères manquants, car il existe de multiples fonctions pour récupérer des caractères peu ordinaires. Voyez plutôt les différentes méthodes :
A : String.fromCharCode(65)
k : window.atob("a0")
b : (11).toString(16)
La première traduit un code numérique en un caractère, basé sur le standard Unicode (dont est issu l'encodage UTF-8). La seconde décode une String qui a été préalablement encodée en base 64. Enfin, la dernière écrit simplement un nombre dans une base différente (on peut aller jusqu'à la base 36 avec des chiffres allant de 0 à z). Récupération depuis le charCode ou changement de base, il y a l'embarras du choix pour récupérer tous les caractères que l'on désire. Il suffit de regarder ensuite quelle est la méthode la plus courte pour chaque caractère. Certes, il y a quelques caractères dans le nom des fonctions qu'il faut également récupérer par d'autres moyens. Le « C » majuscule dans « fromCharCode » s'est avéré particulièrement délicat. Je vous donne deux des solutions possibles :
C via []["filter"]["constructor"]("return document")()["forms"]["constructor"]()[4] --> 4498 caractères
document.forms est en effet du type HTMLCollection. Tiens, un C majuscule.
C via []["filter"]["constructor"]("return self")()["atob"]("00N")[1] --> 2411 caractères
On peut aussi le récupérer via atob qui décode en base 64. Le b miniscule de « atob » s'obtient plus facilement :
b via (0).constructor === Number --> 832 caractères
ou
b via 11.toString(20) --> 1410 caractères
VII. Place à l'industrialisation▲
Maintenant que nous avons les ingrédients et la recette, il ne reste plus qu'à faire la machinerie. Attaquons-nous donc à la conception d'un compilateur qui traduira n'importe quel code en entrée en série de six caractères. Puisque nous avons de quoi retrouver tous les caractères, il suffit d'établir une table de conversion, de parcourir chacun des caractères du code en entrée dans l'ordre, et de constituer une String concaténant chaque caractère sous sa forme « convertie ».
Voyons ce que ça donne avec le code « test » en entrée :
t : (true+[])[0] --> (!![]+[])[+[]]
e : (undefined+[])[3] --> (!![]+[])[+!+[]+!![]+!![]]
s : (false+[])[3] --> (![]+[])[+!+[]+!![]+!![]]
Le résultat de l'évaluation de « test » sera récupéré comme ceci :
[]["filter"]["constructor"]("t"+"e"+"s"+"t")()
Soit en remplaçant le tout (attention aux yeux) :
[][(![]+[])[+!+[]+!![]+!![]]+([][(![]+[])[+[]]+([][+[]]+[])[+!+[]+!![]+!![]+!![]+!![]]+(![]+[])[+!+[]+!![]]+(!![]+[])[+[]]+(!![]+[])[+!+[]+!![]+!![]]+(!![]+[])[+!+[]]]+[])[+!+[]+!![]+!![]+!![]+!![]+!![]]+(!![]+[])[+!+[]]+(!![]+[])[+[]]][([][(![]+[])[+[]]+([][+[]]+[])[+!+[]+!![]+!![]+!![]+!![]]+(![]+[])[+!+[]+!![]]+(!![]+[])[+[]]+(!![]+[])[+!+[]+!![]+!![]]+(!![]+[])[+!+[]]]+[])[+!+[]+!![]+!![]]+([][(![]+[])[+[]]+([][+[]]+[])[+!+[]+!![]+!![]+!![]+!![]]+(![]+[])[+!+[]+!![]]+(!![]+[])[+[]]+(!![]+[])[+!+[]+!![]+!![]]+(!![]+[])[+!+[]]]+[])[+!+[]+!![]+!![]+!![]+!![]+!![]]+([][+[]]+[])[+!+[]]+(![]+[])[+!+[]+!![]+!![]]+(!![]+[])[+[]]+(!![]+[])[+!+[]]+([][+[]]+[])[+[]]+([][(![]+[])[+[]]+([][+[]]+[])[+!+[]+!![]+!![]+!![]+!![]]+(![]+[])[+!+[]+!![]]+(!![]+[])[+[]]+(!![]+[])[+!+[]+!![]+!![]]+(!![]+[])[+!+[]]]+[])[+!+[]+!![]+!![]]+(!![]+[])[+[]]+([][(![]+[])[+[]]+([][+[]]+[])[+!+[]+!![]+!![]+!![]+!![]]+(![]+[])[+!+[]+!![]]+(!![]+[])[+[]]+(!![]+[])[+!+[]+!![]+!![]]+(!![]+[])[+!+[]]]+[])[+!+[]+!![]+!![]+!![]+!![]+!![]]+(!![]+[])[+!+[]]]((!![]+[])[+[]]+(!![]+[])[+!+[]+!![]+!![]]+(![]+[])[+!+[]+!![]+!![]]+(!![]+[])[+[]])()
Terminons par mettre en place une grosse optimisation. String.fromCharCode peut prendre un grand nombre d'arguments à la suite, et convertira tous les charcodes dans leurs caractères respectifs concaténés en une String. Comme un nombre entier s'encode de manière plus courte en moyenne qu'un caractère, il vaut donc mieux convertir l'intégralité du code en entrée en série de charcodes, puis reconstituer le code correspondant avec String.fromCharCode. Contrairement à la méthode précédente, il faudra évaluer deux fois, la première fois pour exécuter String.fromCharCode et reconstituer le code, la seconde fois pour exécuter ce code récupéré.
Voici l'extrait le plus important de mon implémentation de ce compilateur, que vous pouvez trouver ici : http://syllab.fr/projets/experiments/sixcharsjs/
/* table de conversion pour tous les caractères dont nous avons besoin */
var convertTable =
{
"0"
:
"+[]"
,
"1"
:
"+!+[]"
,
"2"
:
"+!+[]+!![]"
,
"a"
:
"(![]+[])[+!+[]]"
,
"d"
:
"([][+[]]+[])[+!+[]+!![]]"
,
"e"
:
"(!![]+[])[+!+[]+!![]+!![]]"
,
(...
)
};
/* fonction convertissant une String quelconque en séquence de ([+!]) */
var _ =
window
.
_ =
function(
str) {
var out =
[];
for (
var c =
0
;
c <
str.
length;
c++
) {
out.push
(
convertTable[
str[
c]]
);
}
return out.join
(
'+'
);
};
/* fonction convertissant un nombre entier quelconque en séquence de ([+!]) ; elle est très ressemblante à la fonction précédente, avec en plus un cast number -> string au début */
function convertInt
(
int) {
var str =
""
+
int;
var result =
""
;
for (
var c =
0
;
c <
str.
length;
c++
) {
result +=
'+('
+
convertTable[
str[
c]]
+
')'
;
}
return '([]'
+
result +
')'
;
};
/* fonction convertissant de manière optimisée une String quelconque en séquence de ([+!]) grâce à String.from CharCode, puis évalue le résultat comme code JavaScript */
function encode
(
input){
/* on parcourt un à un les caractères du code entré et crée un tableau
contenant l'ensemble des valeurs Unicode de chaque caractère dans l'ordre
La fonction convertInt est similaire à la fonction _ (underscore), sauf qu'elle prend un entier en entrée et renvoie un entier également
*/
var charcodes =
[];
for(
var c=
0
;
c<
input.
length;
c++
){
charcodes.push
(
convertInt
(
input.charCodeAt
(
c) ) );
}
/* le point de départ de notre séquence résultat sera de créer une String avec le contenu
de notre tableau de charCodes concaténé avec le caractère “f” comme séparateur.
Notez la fonction _ (underscore) détaillée plus haut. Les deux “+” encadrant le caractère “f”
converti servent comme opérateurs de concaténation dans la construction de la String. */
*/
var out =
"[]+"
+
charcodes.join
(
"+"
+
_
(
"f"
)+
"+"
);
/* nous reconstituons l'array d'origine en appelant la méthode Array.split sur ce même caractère séparateur “f”. L'enchaînement join puis split a uniquement servi à éviter l'usage du caractère virgule dans le résultat en sortie */
out =
"[]+("
+
out +
")["
+
_
(
"split"
)+
"]("
+
_
(
"f"
)+
")"
;
/* nous avons maintenant une String présentant la séquence de charcodes séparés par des virgules ; encadrons le tout avec le code de String.fromCharCode */
out =
_
(
"return "
) +
"+"
+
convertTable[
"String"
]
+
"+"
+
_
(
".fromCharCode("
) +
"+("
+
out +
")+"
+
_
(
')'
);
/* on évalue une première fois le code pour exécuter la fonction String.fromCharCode et récupérer au format String le code d'origine. La fonction eval est faite pour rappel via []["filter"]["constructor"]("code")() */
out =
eval(
out);
/* puis on évalue une seconde fois pour exécuter le code final ! */
out =
eval(
out);
return out;
};
Vous pouvez également retrouver tout le code source du compilateur ici : http://syllab.fr/projets/experiments/sixcharsjs/js/main.js
VIII. Remerciements et références▲
Si vous avez lu jusqu'au bout, merci et bravo ! Car vous venez d'explorer une facette de JavaScript que peu de gens ont eu l'occasion de découvrir. Je me suis beaucoup amusé à travers ce petit défi, et j'espère que vous avez vous aussi pris du plaisir à lire cet article. Bien entendu, je ne suis pas le premier à avoir eu cette idée et d'autres s'y sont penchés avant moi :
Merci également à KaamoKaamo pour ses bons tuyaux et pour avoir su m'accompagner dans cette périlleuse aventure. Merci à verminevermine pour la relecture et la mise en forme et ses nombreux conseils
Si certains d'entre vous se sentent pousser eux aussi des ailes, sachez qu'il reste de nombreuses améliorations à découvrir. Et qui sait, peut-être peut-on y arriver avec 5 caractères seulement ?