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 :
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.getPrototypeOf(object)); // null
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 :
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.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 } }
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 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 } }
Une solution sûre est tout de même possible avec cette même méthode...
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 } } }
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 :
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 var object; object = {}; object.property = 'value'; Object.prototype.hack = function() { this.property = 'modified'; }; object.hack(); console.log(object.property); // 'modified'
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 :
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
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' } } */
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 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
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?
Partager