date 17 janvier 2026

JS objet et prototype

La programmation objet par prototype m'a toujours fasciné. Je trouve le paradigme de prototype plus élégant que celui de class.Dans cet article je vais tenter d'en comprendre les secrets.

Je me suis largement inspiré de JavaScript avancé de David Gayerie.

Les exemples que je propose ici ont été testés avec quickjs de Fabrice Bellard. La console de Firefox est imbuvable.

Le modèle objet de JS

Déclaration litérale

On peut définir un point A(-1, 1) par :

let A = {x: -1, y: 1};

Un peu plus élaboré :

let pt = {
    x: 0,
    y: 0,
    set: function (x, y) {
        this.x = x;
        this.y = y;
    }
};

le dernier membre est une méthode. On retrouve le mot-clé this de C++.

J'ai éssayé de rajouter à l'objet pt une méthode après coup par :

pt.prototype.mv = function(dx, dy) ..

mais il n'en veut pas, il faut en passer par un constructeur (1).

Constructeur

Un constructeur est une fonction (2):

function Pt(x, y) {
    this.x = x;
    this.y = y;
}

Gayerie dit que l'initiale en majuscule est l'usage pour un constructeur.

On va ajouter trois méthodes à Pt.

Pt.prototype.set = function (x, y) {
    this.x = x; this.y = y;
}
Pt.prototype.mv = function (dx, dy) {
    this.x += dx; this.y += dy;
}
Pt.prototype.str= function () {
    return "Pt(" + this.x + "," + this.y + ")";
}

Grognements

Généralement les sites qui traitent de la question me mettent en pétard. Exemple sur MDN.

function Personne(prenom, nom, age, genre, interets) {
  // Définitions des propriétés et méthodes
}

Nom d'une pipe ! Quel intéret de noyer le lecteur dans des kilomètres de code ? Est-ce pour le distraire de ce qu'il voudrait comprendre par un verbiage immonde ? Pensent-ils qu'on ne vient au JS que pour coder des applications de e-commerce ?

Ne leur vient-il jamais à l'esprit d'envisager

«Ce que l'on conçoit bien s'énonce clairement, et les mots pour le dire viennent aisément». Jean de La Bruyère.

Héritage

Disons qu'on veut créer un objet Vect, qui hérite de l'objet Pt :

function Vect(x, y) {
}
Vect.prototype = new Pt;
Vect.prototype.str = function () {
    return "Vect(" + this.x + "," + this.y + ")";
}

Après avoir copié-collé le code ci-dessus dans qjs j'ai testé la nouvelle bêt dans quickjs :

qjs > let v = new Vect(1, 1);
undefined
qjs > v.str();
"Vect(undefined,undefined)"
qjs > v.set(1,1);
undefined
qjs > v.str();
"Vect(1,1)"
qjs > 

Il a bien crée l'instance v de Vect mais a négligé les coordonnées qui lui ont été transmises.

Il a cependant bien hérité de la méthode set comme on le voit ci-dessus.

Voci ce que j'ai encore testé avec quickjs :

qjs > sqrt = Math.sqrt;
undefined
qjs > sqrt(4);
2
qjs > Vect.prototype.norm = function() {
{  ...     return sqrt(this.x * this.x + this.y * this.y);
{  ...     }
function ()
qjs > v.norm()
1.4142135623730951
qjs > 

Au passage je ne comprends pas qu'il faille dans JS préfixer les fonctions mathématiques avec Math.. Grognon comme je suis, du temps où j'utilisais JS (voir mes articles Repères cartésiens, Nombres complexes) je commençais par définir sqrt, sin, cos, etc. C'est ce que j'ai fait ici dès le début.

Fonction call

Une alternative à l'héritage tel que présenté au paragraphe Héritage consiste à appeler la fonction prototype call() comme suit :

function Vect(x, y) {
    Pt.call(this, x, y);
}

De cette manière let v = new Vect(1, 1); va créer l'instance v comme au paragraphe précédent mais initialiser correctement ses propriétés x et y.

Basta

J'arrête là. C'est parce que les classes de JS ne m'intéressent pas, que je me suis fendu de cet article. J'en ai assez soupé dans le temps avec Python, C++, D, Java et Turbo Pascal.

J'ai essayé la surcharge des opérateurs, mais semble-t-il cette faculté n'est pas prévue dans JS. Si on a let u = new Vect(-1, 2); et let v = new Vect(1, 1) on ne peut pas écrire

let w = u + v;.

Accessoirement je fais de la pub à Quick JS. Pour tester du code JS dans un terminal je ne connais pas mieux. Les messages renvoyés par Fabrice Bellard sont parfois déroutants, comme par exemple la reproduction des accolades ouvrantes non encore fermées dans la marge gauche. Je pense que son intention est de nous guider pour ne pas oublier de la fermer. Dans le REPL de guile ou de Lisp, une telle attention serait bien utile, tellement le dénombrement des parenthèses est crucial. Ça me rappelle un sketch de Raymond Devos «chaque fois que j'ouvre une parenthèse, j'oublie de la fermer»..

Application

Motivation

Les dates dans Unix ou Linux c'est l'enfer. Les jours commencent à 1, mais les mois à 0. L'epoch est datée au 1er janvier 1970 (cf. time_t) en C, mais les années sont comptabilisées à partir de 1900. En JS c'est pire.

Dans mon artcle précédent Agenda minimaliste en HTML pur j'ai fait une lib date.h, date.c pour palier cet écueil. Ici je la retranscris en js.

Description et usage

Le script Day.js définit un objet Day qui est en fait une date. Je l'ai appelé ainsi pour lever toute ambiguïté avec le Date() de JS.

Exemple let date = new Day(20, 1, 2026) crée la date '20/01/2026'

Propriétés

Construteur

Méthodes

from_yday(n, y) n'est pas une méthode mais un autre constructeur qui est la réciproque de yday. Pour l'ordinal n et l'année y et renvoie l'objet Day() correspondant.

puts() est un alias de console.log(). Je donne souvent des alias de fonctions js.

Exemple : sin = Math.sin, parce que Math.sin me gonfle, évidemment qui sin c'est des maths. Je ne sais pas si c'est réglo mais au moins ça marche.

Exemple dans Quick js

Au moment où j''écris ces lignes on est le 20 janvier 2026

$ qjs -i --script Day.js
QuickJS - Type "\h" for help
qjs > let d = new Day(); // aujourd'hui
d: 20, m: 1, y: 2026
undefined
qjs > puts(d.eu());
20/1/2026
undefined
qjs > puts(d.fr());
mardi 20 janvier 2026
undefined
qjs > let n = d.yday(); // ordinal dans l'année i.e. 19
undefined
qjs > puts("n = d.yday() = " + n);
n = d.yday() = 19
undefined
qjs > n += 12; // dans 12 jours
31
qjs > let d12 = from_yday(n, 2026);
d: 1, m: 2, y: 2026
undefined
qjs > puts("Dans 12 jours on sera " + d12.fr());
Dans 12 jours on sera dimanche 1 février 2026
undefined
qjs > 

Remarque :

  1. Pour additionner un nombre de jours avec débordement sur l'année courante il faut faire un peu de mathématiques élémentaires.
  2. Ce script est volontairement franchouillard. Mais pour l'adapter à une langue il suffit d'ajouter deux arrays et une méthode. Pour l'espagnol, par exemple :
const _Day_d_es = ["domingo", "lunes", "martes", ... ];
const _Day_m_es = ["", "enero", "febrero", ... ];
this.es = function () { ...

Venons-en à mon modeste script.

script Day.js

/*
:w | !qjs --script %
:!qjs -i --script %
    
This script is designed for dates in French. To adapt it to your language, simply add two tables and a method, for example for Spanish:
const _Day_d_es = ["domingo", "lunes", "martes", ... ];
const _Day_m_es = ["", "enero", "febrero", ... ];
copy the fr() method and replace all instances of  _fr by _es in the copy.
this.es = function () { ...
    
Example of session with Quick js
$ qjs -i --script Day.js   
QuickJS - Type "\h" for help
qjs > let td = new Day(); // today is 2026-1-20
undefined
qjs > td.eu();
"20/1/2026"
qjs > let n =td.yday(); //2026-1-1 -> 0, 2026-12-31 -> 364
undefined
qjs > puts(n);
19
undefined
qjs > let d = from_yday(n+7, 2026); // 7 days after
undefined
qjs > d.eu();
"27/1/2026"
qjs > 
*/
// _Day_mdays[m] : length of month m for not leap year, or else mdays[2] = 29 
const _Day_mdays = [ 0, // unused: m ∈ ⟦1, 12⟧
    31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
    
// _Day_ydays[m] : days before the month m for not leap year, else +1
const _Day_ydays = [0, //unused 
    0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334];
    
// TODO add here your language
// french week days
const _Day_d_fr = ["dimanche", "lundi", "mardi", "mercredi", "jeudi", "vendredi",
            "samedi"];
// french months. Notice the empty string.
const _Day_m_fr = ["", "janvier", "février", "mars", "avril", "mai", "juin",
            "juillet", "août",  "septembre", "octobre", "novembre", "décembre"]
    
// check if d/m/y is valid
function _Day_valid(d, m, y) {
    let msg = "";
    if(y <= 2000 || y >= 2100)  msg += "y ∉ ⟦2001, 2099⟧\n";
    if(m < 1 || m > 12) msg = "m ∉ ⟦1, 12⟧\n";
    // len of month i.e. d ∈ ⟦1, ml⟧
    let ml = (y % 4 == 0 && m == 2) ? 29 : _Day_mdays[m];
    if(d< 0 || d > ml) msg += "d ∉ ⟦1, " + ml + "⟧\n";
    if(msg == "") return true;
    puts(msg);
    return false; 
}
    
puts = console.log;
    
function Day(d, m, y) {
    // managing default arguments
    let td = new Date(); // today
    let l = arguments.length;
    if(l < 3) { y = td.getFullYear(); }
    if(l < 2) { m = td.getMonth() + 1; }
    if(l < 1) { d = td.getDate(); }
    
    /* let d = new Day(19, 1, 2026); d.eu() -> "19/1/2026" */
    if(!_Day_valid(d, m, y)) {
        console.log("Date invalide\n");
        return undefined;
    }
    
    // setting arguments
    this.d = d; // day this.d ∈ ⟦0, 28 | 29 | 30 | 31⟧
    this.m  = m; // this.m ∈ ⟦1, 12⟧
    this.y  = y; // this.y ∈ ⟧200, 2100⟦
    
    /* let d = new Day(19, 1, 2026); d.eu() -> "19/1/2026"       */
    this.eu = function () {
        return this.d + "/" + this.m + "/" + this.y;
    }
    
    // week day  sunday -> 0, ... , saturday ->6
    this.wday = function() {
        let date = new Date(this.y, this.m - 1, this.d);
        return date.getDay();
    }
    
    // TODO add here for your language
    /* let d = new Day(19, 1, 2026); d.fr() -> "lundi 18 janvier 2026" */
    this.fr = function () {
        // one can replace the two line below by let wd = this.yday()
        let date = new Date(this.y, this.m - 1, this.d);
        let wd = date.getDay(); 
        return _Day_d_fr[wd] + " " + this.d + " " + _Day_m_fr[this.m] + " " + y;
    }
    /* yday is the ordinal of the day in the year starting at 0
    (1, 1, 2026) -> 0, (19, 1, 2026) -> 18, ...               */
    this.yday = function() {
        let yday = this.d + _Day_ydays[this.m] - 1;
        if(this.m > 2 && y % 4 == 0) yday++;
        return yday;
    }
}
    
/* converse of Day().yday */
function from_yday(n, y) {
    // algorithm picked up from source Python-3.14/from_ordinal 
    let m = (n + 50) >> 5;  //estimate month
    let prior = _Day_ydays[m]; // days before month m
    if(m > 2 && y % 4 == 0) prior++; // leap year
    // length of the estimate month
    let mlen = (m == 2 && y% 4 == 0) ? 29 : _Day_mdays[m];
    if(prior > n) { // estimate is too large
        m--; // one step backwords
        mlen = (m == 2 && y% 4 == 0) ? 29 : _Day_mdays[m];
        prior -= mlen; // updating preceding days
    }
    let d = n - prior + 1;
    return new Day(d, m, y);
}

Notes

(1)

Gayerie dit qu'on peut ajouter à un objet des propriétés avec Object.defineProperty, j'ai jeté un œil à la prose (toujours imbuvable) de MDN, mais je n'ai pas essayé.

(2)

Ce constructeur me fait penser aux fonctions du C avec des «membres» static.




Réalisé avec Qlam - LGPL