Bonjour à tous,

Je voudrais vous faire part de quelques-unes de mes best-practices en JS, qui vont me faire faire une nouvelle version de chacun de mes outils publiés.

Férus de JS? Ne partez pas trop vite tout de même, bien que ce que je vais énoncer ici coule de source, je suis certain que vous n'en utilisez pas autant que vous le devriez et que vous pensez trop rarement au cas où vous en rencontreriez un fourni par un script tiers.


Qu'est-ce qu'un objet vierge et comment en crée-t-on un?

Un objet vierge est un objet n'ayant aucune propriété, ni méthode, hormis __proto__, standardisé avec l'ES6.


Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
var object;  
 
object = Object.create(null);

Le prototype d'un objet vierge et l'opérateur instanceof

Un objet vierge ainsi créé a null, pour tout prototype :

Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
 
var object;  
 
object = Object.create(null);  
 
console.log(Object.getPrototypeOf(object)); // null
Un objet vierge n'est pas une instance d'Object :

Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
 
var object;  
 
object = Object.create(null);  
 
console.log(object instanceof Object); // false

L'usage de la méthode Object.prototype.hasOwnProperty().

Un objet, en JS, dès lors qu'il est créé sur base du prototype d'Object, hérite de tout un tas de méthodes.

En plus de ces méthodes natives, de nombreux scripts étendent les prototypes au moyen de polyfills.

Ces propriétés vous ont déjà certainement, un peu gêné lorsque vous vouliez itérer sur les propriétés et/ou méthodes non-héritées, devant tester si c'est une propriété qui lui est propre :

Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var object,  
    properties,  
    iterator,  
    length;  
 
object = {};  
object.property = 'value';  
properties = Object.getOwnPropertyNames(object);  
iterator = 0;  
length = properties.length;  
 
for (;iterator < length;iterator += 1) {  
    if (object.hasOwnProperty(property)) {  
        // propriété propre à l'objet  
    }  
}
Sachant qu'un script tiers pourrait très bien vous fournir des objets vierges, ce code planterait votre script lamentablement :

Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var object,  
    properties,  
    iterator,  
    length;  
 
object = Object.create(null);  
object.property = 'value';  
properties = Object.getOwnPropertyNames(object);  
iterator = 0;  
length = properties.length;  
 
for (;iterator < length;iterator += 1) {  
    if (object.hasOwnProperty(properties[iterator])) {  
        // crash  
    }  
}
Tester l'existence de la méthode hasOwnProperty est une solution bancale, en effet :

Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var object,  
    properties,  
    iterator,  
    length;  
 
object = {};  
object.property = 'value';  
properties = Object.getOwnPropertyNames(object);  
iterator = 0;  
length = properties.length;  
 
for (;iterator < length;iterator += 1) {  
    if (object.hasOwnProperty) {  
        if (object.hasOwnProperty(properties[iterator])) {  
            // propriété propre à l'objet  
        }  
    }  
}
Une solution sûre est tout de même possible avec cette même méthode...

Dans cet exemple, j'utiliserai une fonction s'appelant demethodize, c'est une méthode empruntée à SylvainPV, servant à raccourcir l'usage d'une méthode, sur une instance, tout en optimisant les performances de son appel :

Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var object,  
    properties,  
    iterator,  
    length,  
    demethodize,  
    hasOwnProperty,  
    property;  
 
object = {};  
object.property = 'value';  
properties = Object.getOwnPropertyNames(object);  
iterator = 0;  
length = properties.length;  
demethodize = Function.bind.bind(Function.call);  
hasOwnProperty = demethodize(Object.prototype.hasOwnProperty);  
 
for (;iterator < length;iterator += 1) {  
    if (hasOwnProperty(object, properties[iterator])) {  
        // propriété propre à l'objet  
    }  
}

Mais pourquoi, utiliserait-on des objets vierges, s'ils peuvent être si problématiques?

En réalité, la question est posée à l'envers, les objets problématiques, ce sont les objets classiques!

Certains d'entre vous, nombreux, je l'espère... utilisent des closures pour encapsuler leur code, afin de ne pourrir l'espace global et peut-être aussi pour que de petits plaisantins ou de mauvais scripts n'aillent pas modifier des valeurs des propriétés et variables de leurs scripts.

Cela suffit-il? assurément, non!

En effet, toute modification d'Object.prototype va altérer chacune des instances, encapsulées ou non, il peut donc être très aisé de modifier le comportement de vos objets.

Avec un objet classique :

Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
var object;  
 
object = {};  
object.property = 'value';  
 
Object.prototype.hack = function() { 
  this.property = 'modified'; 
}; 
 
object.hack(); 
console.log(object.property); // 'modified'
Avec un objet vierge :

Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
var object;  
 
object = Object.create(null);  
object.property = 'value';  
 
 
Object.prototype.hack = function() { 
  this.property = 'modified'; 
}; 
 
object.hack(); // TypeError: object.hack is not a function 
console.log(object.property); // 'value'

Les objets vierges et l'héritage

Si aucune modification d'Object ne peut altérer vos objets, l'héritage n'en est pas moins exploitable :

Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
var object,  
    clone1,  
    clone2;  
 
object = Object.create(null);  
clone1 = Object.create(object);  
clone2 = Object.create(clone1);  
 
Object.getPrototypeOf(clone1).property = 'value';  
 
console.log({  
    object: object,  
    clone1: clone1,  
    clone2: clone2  
});  
/*  
{  
    object: {  
        property: 'value'  
    },  
    clone1: {  
        property: 'value'  
    },  
    clone2: {  
        property: 'value'  
    }  
}  
*/
Récupération du parent :

Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
var demethodize,  
    getPrototypeOf,  
    object,  
    clone1;  
 
demethodize = Function.bind.bind(Function.call);  
getPrototypeOf = demethodize(Object.getPrototypeOf, null);  
 
object = Object.create(null);  
clone1 = Object.create(object);  
 
console.log(getPrototypeOf(clone1) === object); // true
Test d'héritage :

Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
var getPrototypeOf,  
    isCloneOf,  
    object,  
    clone1,  
    clone2;  
 
getPrototypeOf = Object.getPrototypeOf;  
 
isCloneOf = function (clone, original) {  
    var parent;  
 
    if (typeof clone !== 'object') {  
        return false;  
    }  
 
    while (parent !== null) {  
        parent = getPrototypeOf(parent || clone);  
 
        if (parent === original) {  
            return true;  
        }  
    }  
 
    return false;  
};  
 
object = Object.create(null);  
clone1 = Object.create(object);  
clone2 = Object.create(clone1);  
 
console.log(isCloneOf(clone2, object)); // true

Méfiez-vous du JSON

En effet, les objets qu'il vous fournit héritent tous d'Object, ils ne sont donc pas sûrs.

Un petit hack s'impose donc :

Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
var parseJSON;  
 
parseJSON = (function () {  
    var create,  
        getOwnPropertyNames,  
        parse,  
        reparse,  
        reparseArray,  
        reparseObject;  
 
    parse = JSON.parse;  
    create = Object.create;  
    getOwnPropertyNames = Object.getOwnPropertyNames;  
 
    reparse = function (value) {  
        if (typeof value !== 'object') {  
            return value;  
        }  
 
        if (value instanceof Array) {  
            return reparseArray(value);  
        }  
 
        return reparseObject(value);  
    };  
 
    reparseArray = function (array) {  
        var iterator,  
            length;  
 
        iterator = 0;  
        length = array.length;  
 
        for (;iterator < length;iterator += 1) {  
            array[iterator] = reparse(array[iterator]);  
        }  
 
        return array;  
    };  
 
    reparseObject = function (object) {  
        var virgin,  
            properties,  
            iterator,  
            length,  
            name;  
 
        virgin = create(null);  
        names = getOwnPropertyNames(object);  
        length = names.length;  
 
        for (;iterator < length;iterator += 1) {  
            name = names[iterator];  
 
            virgin[name] = object[name];  
        }  
 
        return virgin;  
    };  
 
    return function (data, reviver) {  
        return reparse(JSON.parse(data, typeof reviver === 'function'  
            ? function (key, value) {  
                return reparse(reviver(key, value));  
            }  
            : reviver  
        );  
    };  
}());

Conclusion

Les objets vierges sont une solution, combinée aux closures (voire d'un sandboxing), pour renforcer la fiabilité du comportement des objets créés par vos scripts.

Je vous conseille de aliaser/deméthodiser le plus tôt possible les méthodes nécessaires d'Object, afin d'être sûr de leurs valeur de retour.

Alors, oui, certains objets que vous traiterez n'en seront pas forcément mais sachant que l'on peut créer des objets vierges ou non, si un script crée des objets qui devaient hériter d'Object, à mon sens, nous n'avons d'autre choix que de considérer que son concepteur a sciemment choisi que ses objets devaient posséder les méthodes du prototype.


hasOwnProperty or not?