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 gateau, 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 ?