Comment se perdre en écrivant un objet en JS

Vraiment, j’adore Javascript. Mais néanmoins, il faut reconnaître que ce langage offre un tel nombre de syntaxes différentes pour réaliser une même opération que ça en devient parfois un peu bordélique.

Petit exemple avec l’écriture d’un objet. J’ai compté six façons différentes d’obtenir un objet (et j’en ai certainement oubliées).

Simple objet littéral

La syntaxe

Le truc cool avec javascript, c’est qu’on peut déclarer un simple objet à la volée (sans déclarer de classe) et de manière littérale (sans syntaxe d’instanciation). Il s’agit souvent soit d’une structure de donnée (par exemple pour passer des options à une classe), soit d’une instance unique d’objet.

1
2
3
4
5
var foo = {
    bar: 'bzzz'
};

console.debug(foo.bar); //'bzzz'

Le problème

Cette syntaxe convient parfaitement si la structure de l’objet ne doit pas assurer une intégrité absolue (on peut se passer de définir certaines propriétés) ou être reproductible avec fidélité (on peut se passer de la déclaration d’une signature par l’intermédiaire d’une classe). Cela remplit bien le rôle d’un array associatif en PHP, par exemple.

Par contre, cela ne donne pas d’assurance quant à la complétude de l’objet, et cela devient plus embêtant si on doit créer plusieurs objets du même type avec fidélité.

Object.create()

La syntaxe

Avec ES5, on sait instancier un objet depuis une définition, grâce à Object.create(). Basiquement, cela ressemble un peu à une instanciation de classe dynamique. Cela permet beaucoup de choses en pratique car plutôt que de travailler avec une définition, on peut utiliser directement le prototype d’une classe. On sait réaliser alors une sorte de clone du prototype.

1
2
var foo = Object.create({}, { bar: { value: 'bzzz' } })
console.debug(foo.bar); //'bzzz'

Le problème

Ok, si on rend le code réutilisable (par exemple, en stockant la définition dans une fonction faisant office de constructeur), on est sûr d’avoir un objet formé correctement lors de chaque instanciation. Par contre, il n’y a pas de réelle plus-value par rapport à la déclaration d’une classe si on ne travaille pas au niveau du prototype.

Objet créé par une fonction

La syntaxe

Pour remédier autrement au problème de l’intégrité, on peut imaginer utiliser une fonction qui retourne systématiquement la même structure de donnée. Une factory en somme.

1
2
3
4
5
6
7
8
function createFoo(bar){
    return {
        bar: bar
    };
}

var foo = createFoo('bzzz');
console.debug(foo.bar); //'bzzz'

Le problème

En appelant une fonction, on perd un peu la syntaxe objet, et donc la notion de ce qui est retourné par la fonction. On n’a plus « l’impression » de travailler avec un objet…

Objet littéral instancié

La syntaxe

On peut agrémenter la syntaxe précédente d’un new. Le résultat sera exactement identique, si ce n’est qu’on devient plus explicite quant au comportement de la fonction.

1
2
3
4
5
6
7
8
function Foo(bar){
    return {
        bar: bar
    };
}

var foo = new Foo('bzzz');
console.debug(foo.bar); //'bzzz'

Le problème

Le problème c’est qu’on peut être dupé par le faux typage. En effet, le résultat cette opération est bien l’instanciation d’un nouvel objet de type Object depuis l’objet littéral retourné par la fonction. Il ne s’agit pas de l’instanciation de la fonction en tant que telle, comme dans le processus d’instanciation depuis une classe.

1
console.debug(foo instanceof Foo); //false

D’autre part, c’est assez vicieux comme syntaxe. On ne sait pas vraiment ce qui est instancié: est-ce l’objet ou la fonction. Surtout que, normalement, il n’est pas possible d’instancier un autre objet.

1
2
3
4
5
var foo = {
    bar: 'bzzz'
};

var foo2 = new foo; //Uncaught TypeError: foo is not a function(…)

Objet instancié depuis une classe

La syntaxe

Comme on le sait, on peut déclarer une classe depuis une fonction. Du coup, l’objet sera effectivement une instance de la fonction. Le problème précédent est résolu.

1
2
3
4
5
6
7
function Foo(bar){
    this.bar = bar;
}

var foo = new Foo('bzzz');
console.debug(foo.bar); //'bzzz'
console.debug(foo instanceof Foo); //true

Le problème

Toutefois, on se retrouve avec une erreur un peu bizarre si on oublie le new (faut le vouloir aussi, mais bon…).

1
2
var foo = Foo('bzzz');
foo.bar;//Uncaught TypeError: Cannot read property 'bar' of undefined

Et si jamais la fonction retournait le « this », on se retrouvait avec l’objet global… argh!

Les classes ES2015

La syntaxe

ES2015, aka ES6, propose une nouvelle syntaxe de déclaration de classe plus explicite (ou en tout cas plus traditionnelle). On est obligé de passer par le new et l’objet est bien une instance de la classe.

1
2
3
4
5
6
7
8
9
class Foo{
    constructor(bar){
        this.bar = bar;
    }
}

var foo = new Foo('bzzz');
console.debug(foo.bar); //'bzzz'
console.debug(foo instanceof Foo); //true

Le problème

On s’est vraiment éloigné de l’esprit de liberté offert par les objets littéraux du premier exemple. On se retrouve avec un code plus lourd (mais plus réutilisable) qui se rapproche de l’écriture traditionnelle de la programmation orientée objet.

Conclusions

Et donc, je me pose la question: Comment bien écrire un objet en javascript?… tout dépend du contexte! ;-)

14 réflexions au sujet de « Comment se perdre en écrivant un objet en JS »

  1. Tu peux aussi utiliser les closures pour limiter ce qui est publique et privé ^^

    function Car(brand, horsePower)
    {
         this.brand = brand;
         this.hp = horsePower;
    
        this.start = function() { accelerate(); };
        this.stop = function() {};
    
        function accelerate() {};
    
        return {
          start: this.start,
          stop: this.stop
        };
    }
    

    Avec cette syntaxe, tu masques la méthode « accelerate » :)

  2. Tu présentes des approches différentes qui ne font pas du tout la même chose, avec des concepts différents.
    C’est un peu un gros bordel.

    « Et donc, je me pose la question: Comment bien écrire un objet en javascript?… tout dépend du contexte! ;-)  »
    Exactement, mais bon avec un peu plus de texte ce serait mieux:

    Structurer de la data simple: Objet littéral
    Système de collection: Classe/Prototype (ce que tu as sous la ref Objet instancié depuis une classe).
    Ensuite c’est le gout les couleurs, si tu veux manipuler des trucs en private sans te faire chier à créer une classe ES2015 ou son équivalent un poil plus verbeux ES5 une fonction instanciée (Prototype), tu utilises une factory:

    function factory(name) {
       function formatDate() {}
       return {
         name: name,
         date: formatDate()
       }
    }
    

    Comme dans tout langage ça dépend du besoin, faut pas se prendre la tête et rester dans le simple. On ne fait pas du Java mais du JavaScript, donc si je veux un objet qui _ressemble_ à une map, je fais un {}.

    Object.create osef, on s’en sert si tu veux faire des trucs plus avancés (genre dans 90% des cas osef).

    KISS bordel.

  3. Petite erreur dans ta remarque : var foo = Foo(‘bzzz’); // Uncaught TypeError: foo is not a function(…)

    function Foo(bar){ this.bar = bar; }
    var foo = Foo("baz"); // Executer sans erreur car sans new, this c'est l'objet global.
    console.log(foo,bar); // affiche "undefined baz";
    

    Autre petite remarque tout dépends de ton approche, si on parle d’instancier des objets de même type ou fonctions il y a class/prototype sinon ce sont des hash/collection que tu fais et dans ce dernier cas je ne vois pas l’intérêt d’avoir une classe ou un prototype.

    // les collections, hash etc...
    var monHash = {
      "cle" : "valeur"
    };
    
    // les class aproche prototype
    var Point = function(x,y){
       if(this instanceof Point) return new Point(x,y); // accepte Point(x,y)
       this.x= x || 0;
       this.y= y || 0;
    }
    Point.prototype.egal = function(point){ // methodes
       return this.x == point.x && this.y == point.y;
    }
    var p1 = new Point(0,2);
    
    // les class ES2015
    class Point2015{
        constructor(x,y){
            this.x= x || 0;
            this.y= y || 0;
        }
        egal(point){ // methodes
            return this.x == point.x && this.y == point.y;
       }
    }
    var p2 = new Point2015(0,2);
    

    Pour moi il n’y a que ses méthodes là pour créer les object, après si tu encapsule la création dans une function c’est la même chose.

    • corrigé! merci bcp! :) (pour info, c’est foo qui est undefined, car la fonction ne retourne rien.)

      En fait, si tu pousses ton raisonnement encore plus loin, il n’y a qu’une façon de faire un objet, car même ce que tu appelles hash/collection, n’est en fait que l’instanciation littérale de la classe Object… ;-)

      Moi je parle de différences syntaxiques, pas conceptuelles. Par exemple, la déclaration une classe en ES2015 peut s’écrire différemment, bien que conceptuellement il s’agisse exactement de la même chose qu’avec les prototypes.

      PS: Est-ce que ton code ne tombe pas dans une récursion infinie?: « if(this instanceof Point) return new Point(x,y); »

  4. Le code ne boucle pas ça test que le this c’est bien un object de Point. car quand tu fais `new MyClass` il y a instanciation de myClass puis assignation de l’instance au `this` mais quand tu fais `MyCass()`, le `this` c’est l’object global(window) donc pour tester la presence de `new` on test si c’est une instance de myClass. Si c’est pas le cas on envoie une instance de myClass.

    Bon après maintenant (ES2015) tu peux tester `new.target` pour vérifier si c’est une instanciation ou un appel de fonction.

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Vous pouvez utiliser ces balises et attributs HTML : <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>