ES5, Chap. 1 — Les propriétés et descripteurs de propriétés en JavaScript

Ce billet fait partie de la collection ES5 dans le détail et en constitue le Chapitre 1.

Même si la nouvelle version de l'environnement semble similaire en surface, elle est différente de plus près.
Même si la nouvelle version de l'environnement semble similaire en surface, elle est différente de plus près.

Il est dédié à l'un des nouveaux concepts introduit par la spécification ECMA-262-5 de JavaScript à propos des attributs de propriétés et des mécanismes de leur gestion ; les descripteurs de propriété.

Introduction

Habituellement, quand on dit « qu'un objet a plusieurs propriétés », on parle d'une association pour chaque propriété de l'objet entre un nom de propriété et une valeur de propriété. Mais ce n'est pas tout. Comme nous l'avons vu dans ES3 dans le détail, la structure d'une propriété est plus complexe qu'un simple nom sous forme de chaine de caractères et une valeur. Il y a aussi un jeu d'attributs internes comme {ReadOnly}, {DontEnum} et d'autres. De ce point de vu donc, une simple propriété est elle-même un objet (même quand elle représente une valeur primitive).

Pour une compréhension complète de ce billet, je vous recommande la lecture du chapitre sur les types en JavaScript ainsi que celui sur les constructeurs et les prototypes.

Nouvelles méthodes de l'API

Pour travailler avec les propriétés et leurs attributs standardisés en ES5 dans les nouvelles méthodes de l'API, résumons cela par ce petit pend de code :

Code JavaScript

// meilleur héritage de prototype
Object.create(parentProto, properties);

// accès au prototype
Object.getPrototypeOf(o);

// Définition des propriétés avec des attributs spécifiques
Object.defineProperty(o, propertyName, descriptor);
Object.defineProperties(o, properties);

// Analyse des propriétés
Object.getOwnPropertyDescriptor(o, propertyName);

// Gestion des objets statiques (ou « gelés »)
Object.freeze(o);
Object.isFrozen(o);

// Gestion des objets non-extensibles
Object.preventExtensions(o);
Object.isExtensible(o);

// Gestion des objets scélés non-extensibles
// et objets non-configurables
Object.seal(o);
Object.isSealed(o);

// Liste des propriétés
Object.keys(o);
Object.getOwnPropertyNames(o);

Penchons nous sur ces points les uns après les autres.

Types de propriété

En ES3 nous avons seulement une association directe entre le nom de la propriété et sa valeur. Cependant, plusieurs implémentations ont leur propre extensions fournis avec un concept d'accesseurs et de mutateurs, c.-à-d. des fonctions qui permettaient indirectement d'associer des valeurs de propriété. ECMA-262-5 standardise se concept en JavaScript et à présent nous avons trois types de propriétés

Vous devriez également savoir qu'une propriété peut-être possédée par son propre objet, ou hérité d'un des objets dans la chaîne des prototypes.

Il y a des propriétés nommées, qui sont disponibles dans un programme JavaScript et des propriétés internes, seulement accessibles au niveau de l'implémentation (cependant, il est possible de gérer la plupart d'entre elles via des méthodes spéciales). Nous allons en parler brièvement.

Attributs de propriétés

Les propriétés nommées sont distinguées par des attributs. Les attributs de propriétés évoqués dans la série d'article sur ES3 comme {ReadOnly}, {DontEnum} et les autres ont ici été renommées par leur état booléen contraire. Il y a deux attributs commun aux deux types de propriété (de données et d'accession) nommées en ECMA-262-5 :

  • [[Enumerable]] Cet attribut (qui est l'état inverse de l'ancien {DontEnum} en ES3) détermine grâce à son état true que la propriété est énumérable dans une énumération for-in.

  • [[Configurable]] Cet attribut (qui est l'état inverse de l'ancien {DontDelete} en ES3) prévient grâce à son état false toute tentative de suppression de la propriété, changement de type ou changement de ses attributs (à l'exception de [[Value]]).

Notez que si l'attribut [[Configurable]] a été mis à false une fois, il ne peut plus être remis à true. Comme dit précédemment, il n'est plus possible non plus de changer d'autres attributs comme [[Enumerable]] par exemple. On peut toujours changer l'attribut [[Value]] et [[Writable]] (mais seulement de true vers false et pas l'inverse).

Nous discuterons des autres attributs de propriétés spécifiques rapidement. Considérons les types de propriétés en détail.

Propriété nommée de données

Ces propriétés étaient déjà largement utilisées en ES3. Une propriété à un nom (qui est toujours une chaine de caractères en ES5) et une valeur directement associée.

Code JavaScript

// définition sous forme déclarative
var tallneck = {
  climbHold: 10 // valeur de type Number directe
};

// définition sous forme impérative
// directe également, mais avec une valeur de type Function : une « méthode »
tallneck.radar = function () {
  return this.climbHold;
};

C'est exactement le même cas de figure qu'en ES3, dans le cas ou la valeur de la propriété est une fonction, cette propriété est appelée une méthode. Mais cette valeur de fonction directe ne doit pas être confondue avec le cas spécial indirect des fonctions accesseurs dont nous allons discuter ci-dessous :

  • [[Value]] Cet attribut spécifie la valeur retournée en lisant la propriété.

  • [[Writable]] Cet attribut (qui est l'état inverse de l'ancien {ReadOnly} en ES3) prévient grâce à son état false toute tentative de changement de la valeur de la propriété via l'utilisation de la méthode interne [[Put]].

Voici la liste complète des attributs pour une propriété de données avec ces valeurs par défaut :

Pseudo-code

defaultDataPropertyAttributes = {
  [[Value]]: undefined,
  [[Writable]]: false,
  [[Enumerable]]: false,
  [[Configurable]]: false
};

Donc, dans leur état par défaut les propriétés sont des constantes :

Code JavaScript

// Définir une constante globale

Object.defineProperty(this, 'MAX_SIZE', {
    value: 100
});

console.log(MAX_SIZE); // `100`

MAX_SIZE = 200; // erreur en mode strict car `[[Writable]]` est à `false`
delete MAX_SIZE; // erreur en mode strict car `[[Configurable]]` est à `false`

console.log(MAX_SIZE); // toujours `100`

Malheureusement en ES3, nous n'avions pas de contrôle sur les attributs de propriétés causant des problèmes connus avec l'augmentation des prototypes pré-conçu. De part la nature de mutabilité dynamique des objets en JavaScript, il est vraiment pratique d'ajouter des nouvelles fonctionnalités et de les utiliser, les déléguants au prototype pour qu'elles soient « possédée » par l'objet. Par exemple, sans contrôle sur l'ancien attribut {DontEnum} en ES3, nous avions un problème avec les énumérations for-in sur les prototypes augmentés des tableaux :

Code JavaScript

// ES3

Array.prototype.sum = function () {
  // implémentation de la somme
};

var arrows = [10, 20, 30];

// marche bien
console.log(arrows.sum()); // `60`

// mais a cause de la lecture dans le `for-in`
// de la chaîne du prototype, la nouvelle propriété `sum`
// était aussi énumérée car
// `{DontEnum}`est à `false`

// itérer sur une propriété
for (var arrow in arrows) {
    console.log(arrow); // `0`, `1`, `2`, `sum`
}

ES5 fournit ce contrôle en utilisant les meta-méthodes spéciales pour manipuler les propriétés d'objet :

Code JavaScript

// ES5

Object.defineProperty(Array.prototype, 'sum', {
    value: function sum() {
        // implémentation de la somme
    },
    enumerable: false
});

var arrows = [10, 20, 30];

// marche bien
console.log(arrows.sum()); // `60`

// maintenant en utilisant le même exemple avec `sum`
// il n'est plus énuméré

for (var arrow in arrows) {
    console.log(arrow); // `0`, `1`, `2`
}

Dans l'exemple ci-dessus nous spécifions l'attribut enumerable manuellement et explicitement. Cependant, comme nous l'avons mentionné, l'état par défaut de tous les attributs est false, donc nous n'avons pas besoin de spécifier manuellement les valeurs à false.

Code JavaScript

// l'affectation par méta-fonction (si vous créez une nouvelle propriété)...
Object.defineProperty(tallneck, 'climbHold', {
    value: 10
});

// ...est la même chose que
Object.defineProperty(tallneck, 'climbHold', {
    value: 10,
    writable: false,
    enumerable: false,
    configurable: false
});

Et un simple opérateur d'affectation corresponds maintenant à l'état par défaut inversé des attributs (en fait, comme c'était le cas en ES3) :

Code JavaScript

// l'affectation simple (si vous créez une nouvelle propriété)...
tallneck.climbHold = 10;

// est la même chose que
Object.defineProperty(tallneck, 'climbHold', {
    value: 10,
    writable: true,
    enumerable: true,
    configurable: true
});

Notez également que cette méta-méthode Object.defineProperty n'est pas réservée qu'à la création de propriété d'objet, mais aussi à leur altération. De plus, elle retourne l'objet altéré, ainsi nous pouvons utiliser cette méthode pour lier les objets nouvellement créés à un nom de variable en une seule fois :

Code JavaScript

// créer l'objet `tallneck` et définir la propriété `climbHold` property
var tallneck = Object.defineProperty({}, 'climbHold', {
  value: 10,
  enumerable: true
});

// altérer les attributs `value` et `enumerable`
Object.defineProperty(tallneck, 'climbHold', {
    value: 20,
    enumerable: false
});

console.log(tallneck.climbHold); // `20`

Pour obternir un tableau des propriétés possédées il y a deux meta-méthodes :

  • Object.keys qui retourne seulement des propriétés énumérables, et
  • Object.getOwnPropertyNames qui retourne aussi bien les propriétés enunérables que les propriétés non-énumérables :

Code JavaScript

var tallneck = {
    climbHold: 10,
    radar: 20
};

Object.defineProperty(tallneck, 'x', {
    value: 30,
    enumerable: false
});

console.log(Object.keys(tallneck)); // `['climbHold', 'radar']`
console.log(Object.getOwnPropertyNames(tallneck)); // `['climbHold', 'radar', 'x']`

Propriété nommée d'accession

Une propriété nommée d'accession est associée à un nom (une chaîne de caractère en ES5) avec une ou deux fonctions d'accessions : un accesseur et un mutateur.

Les fonctions d'accession sont utilisées pour stocker ou retrouver une valeur associée à un nom indirectement.

Comme nous l'avons noté, plusieurs implémentations ES3 possède déjà ce concept. Mais en ES5 la syntaxe est officielle et sensiblement différente de ce qui pouvait déjà exister (comme par ex. avec les extensions de SpiderMonkey).

En plus des attributs généraux, une propriété d'accession a les attributs suivant lié à un accesseur et à un mutateur comme suit :

  • [[Get]] Cet attribut est un objet fonction qui est appelé chaque fois qu'une valeur indirecte existe dans le nom de la propriété. Ne confondez pas cette attribut de propriété avec la méthode interne de même nom des objets en eux-même, le lecteur général de la propriété d'une valeur. Donc dans le cas d'une propriété d'accession, la méthode interne [[Get]] d'un objet appel l'attribut [[Get]] de la propriété d'un objet pour retourner sa valeur.

  • [[Set]] Cet attribut est également un objet fonction qui associe la nouvelle valeur à attacher au nom de propriété. Cet attribut appel la méthode interne [[Put]] d'un objet.

Notez que [[Set]] peut, mais pas obligatoirement, avoir un effet sur la propriété retournée par la méthode interne [[Get]] de la propriété. En d'autres mots, si nous affectons une valeur comme par ex. 10, un accesseur peut retourner une valeur différente comme par ex. 20 car l'affectation est indirecte et [[Set]] peut avoir changer cette valeur.

Et la liste complète des attributs par défaut pour une propriété nomée d'accession est :

Pseudo-code

defaultAccessorPropertyAttributes = {
  [[Get]]: undefined,
  [[Set]]: undefined,
  [[Enumerable]]: false,
  [[Configurable]]: false
};

De fait, si [[Set]] est absent, une propriété d'accession est en lecture seule, comme dans le cas de l'attribut [[Writable]] à false pour les propriétés nommées de données.

Une propriété d'accession peut être définie aussi bien par la méta-méthode Object.defineProperty mentionnée plus haut :

Code JavaScript

var thunderjaw = {};

Object.defineProperty(thunderjaw, 'discLauncher', {
    get: function getDisc() {
        return 2;
    },
    set: function setDisc(value) {
        // implementation de la mutation
    }
});

thunderjaw.discLauncher = 1; // appel du pseudo-code `thunderjaw.discLauncher.[[Set]](1)`

// indépendemment c'est toujours `2`
console.log(thunderjaw.discLauncher); // appel du pseudo-code `thunderjaw.discLauncher.[[Get]]()`

Que par sa forme déclarative en utilisant un initialiseur d'objet :

Code JavaScript

var thunderjaw = {
    get discLauncher () {
        return 2;
    },
    set discLauncher (value) {
        console.log(value);
    }
};

thunderjaw.discLauncher = 100; // affiche `100` dans la console
console.log(thunderjaw.discLauncher); // `2`

Notez qu'il y a aussi une fonctionnalité importante en lien avec la configuration d'une propriété d'accession. Comme nous l'avons mentionné dans la description de l'attribut [[Configurable]], une fois qu'elle est mise à false une propriété ne peut plus être changée (sauf pour l'attribut [[Value]] de la propriété nommée de données). Cela peut être perturbant dans le cas suivant :

Code JavaScript

// `configurable` à `false` par défaut
var thunderjaw = Object.defineProperty({}, 'discLauncher', {
    get: function () {
        return 'disc';
    }
});

// tentative de reconfigurer `discLauncher`
// une exception est levée
try {
    Object.defineProperty(thunderjaw, 'discLauncher', {
        get: function () {
            return 'launcher'
        }
    });
} catch (e) {
    if (e instanceof TypeError) {
        console.log(thunderjaw.discLauncher); // toujours `'disc'`
    }
}

Mais l'exception ne sera pas lancée si la valeur reconfigurée de l'attribut de ce cas est la même. Ce qui est cependant, en pratique, pas très utile.

Code JavaScript

function getDiscLancher() {
    return 'disc';
}

var thunderjaw = Object.defineProperty({}, 'discLancher', {
    get: getDiscLancher
});

// pas d'exception même si `configurable` est a `false`,
// mais en pratique cette re-configuration est inutile
Object.defineProperty(thunderjaw, 'discLancher', {
    get: getDiscLancher
});

Et comme nous l'avons mentionné,la [[Value]] de la propriété nommée de données peut-être reconfigurée même si [[Configurable]] est a false ; bien sur l'attribut [[Writable]] devra être à true. Également, si mis a l'état true, l'attribut [[Writable]] pourra être mis à false, mais pas l'inverse pour une propriété non-configurable :

Code JavaScript

var thunderjaw = Object.defineProperty({}, 'discLancher', {
    value: 'disc',
    writable: true,
    configurable: false // valeur par défaut
});

Object.defineProperty(thunderjaw, 'discLancher', {
    value: 'launcher'
});

console.log(thunderjaw.discLancher); // `'launcher'`

// changer writable
Object.defineProperty(thunderjaw, 'discLancher', {
    value: 'discLauncher',
    writable: false // changé de `true` vers `false`, OK
});

console.log(thunderjaw.discLancher); // `'discLauncher'`

// essayer de changer `writable` de nouveau
Object.defineProperty(thunderjaw, 'discLancher', {
    value: 'discLauncher',
    writable: true // Erreur !
});

De même, nous ne pouvons pas transformer une propriété du type données vers le type accession et inverssement si l'attribut [[Configurable]] est false. Dans l'état true de l'attribut [[Configurable]] une telle transformation est possible et l'état de l'attribut [[Writable]] importe peu, aussi il peut être à false :

Code JavaScript

// `writable` est a `false` par défaut
var thunderjaw = Object.defineProperty({}, 'discLancher', {
    value: 'disc',
    configurable: true
});

Object.defineProperty(thunderjaw, 'discLancher', {
    get: function () {
        return 'launcher';
    }
});

console.log(thunderjaw.discLancher); // OK, `'launcher'`

Un autre fait évident, c'est qu'une propriété ne peut pas être en même temps de données et d'accession. Cela siginifie que la présence d'attributs mutuellement exclusifs lancera une exception :

Code JavaScript

// erreur, `get` et `writable` en même temps
var thunderjaw = Object.defineProperty({}, 'discLancher', {
    get: function () {
        return 'disc';
    },
    writable: true
});

// une autre erreur, les attributs `value` et `set`
// ne peuvent être présent ensemble
var tallneck = Object.defineProperty({}, 'climbHold', {
    value: 'radar',
    set: function (v) {}
});

Rappelons nous également : cet usage des mutateurs et des accesseurs font plus de sens quand nous avons besoin d'encapsuler des calculs complexes en utilisant une fonction utilitaire de données, rendant l'usage de cette propriété plus pratique. C.-à-d. comme s'il s'agissait d'une propriété de données.

Pour des choses non abstraite, l'utilisation des propriété d'accessions n'est pas réellement utile :

Code JavaScript

var thunderjaw = {};

// Pas très utile
Object.defineProperty(thunderjaw, 'disc', {
    get: function getDisc() {
        return this.launcher;
    },
    set: function setDisc(value) {
        this.launcher = value;
    }
});

thunderjaw.disc = 2;

console.log(thunderjaw.disc); // `2`
console.log(thunderjaw.launcher); // `2`

En plus d'utiliser des accesseur et mutateur pour une entité non-abstraite, nous avons créer une propriété propre launcher. On voit dans ce cas qu'une simple propriété de données est suffisante en plus d'améliorer les performances.

Dans les cas où on a réellement besoin d'utiliser les propriétés d'accessions pour améliorer l'abstraction en encapsulant la logique dans une fonction utilitaire, nous aurions alors à faire à ce type d'exemple :

Code JavaScript

var thunderjaw = {};

// contexte encapsulé
(function () {

    // divers états internes
    var data = [];

    Object.defineProperty(thunderjaw, "disc", {
        get: function getDisc() {
            return "Nous avons " + data.length + " discs : " + data;
        },
        set: function setDisc(value) {
            // appel de l'accession en premier
            console.log('Alert du mutateur "disc" : ' + this.disc);

            data = Array(value).join("disc-").concat("disc").split("-");

            // bien sur au besoin nous pouvons mettre à jour
            // également diverse propriétés publiques
            this.launcher = 'Mise à jour du mutateur "disc" : ' + value;
        },
        configurable: true,
        enumerable: true

    });

})();

thunderjaw.launcher = 100;
console.log(thunderjaw.launcher); // `100`

// d'abord l'accession va être appelé dans le mutateur :
// `'Nous avons 0 discs :'`
thunderjaw.disc = 2;

// Accesseurs
console.log(thunderjaw.disc); // `'Nous avons 2 discs : disc, disc, disc'`
console.log(thunderjaw.launcher); // `'Mise à jour du mutateur "disc" : 2'`

Bien sur cette exemple n'a aucune utilité pratique, mais il montre l'utilité principale des accessions, augmenter l'abstraction en encapsulant des données auxiliaires internes.

Et une autre fonctionnalité liée aux accesseurs de propriétés est l'affectation à une propriété d'accession héritée. Comme nous l'avons vu dans la série ES3, les propriétés héritées (données) sont disponibles à la lecture, mais à l'affectation (à une propriété de données), elle crée toujours sa propre propriété :

Code JavaScript

Object.prototype.discLancher = 1;

var thunderjaw = {};

// lecture d'une propriété héritée
console.log(thunderjaw.discLancher); // `1`
console.log(thunderjaw.hasOwnProperty("discLancher")); // `false`

// mais avec une affectation
// créé toujours sa propre propriété
thunderjaw.discLancher = 2;

// lecture d'une propriété héritée
console.log(thunderjaw.discLancher); // `2`
console.log(thunderjaw.hasOwnProperty("discLancher")); // `true`

À la différence des propriétés de données, les accessions héritées sont disponibles pour modifications via l'affectation à travers un objet qui hérite de ces propriétés :

Code JavaScript

var arrows = 10;

var aloy = {
  get quiver() {
    return arrows;
  },
  set quiver(item) {
    arrows = item;
  }
};

console.log(aloy.hasOwnProperty('quiver')); // `true`

console.log(aloy.quiver); // `10`

aloy.quiver = `20`; // affecte sa propre propriété

console.log(aloy.quiver); // `20`

var player = Object.create(aloy); // `player` hérite de `aloy`

console.log(player.quiver); // `20`, lecture héritée

player.quiver = 30; // affecte l'*héritée*, mais pas une à elle sienne.

console.log(player.quiver); // `30`
console.log(aloy.quiver); // `30`
console.log(player.hasOwnProperty('quiver')); // `false`

Cependant si nous définissons player, toujours hérité de aloy, mais en spécifiant son propre quiver, l'affectation sera dans ce cas mis à sa propre propriété :

Code JavaScript

var player = Object.create(aloy);

player.quiver = 30; // affecte l'héritée

Object.defineProperty(player, 'quiver', {
  value: 100,
  writable: true
});

player.quiver = 60; // affecte la sienne.

console.log(player.quiver); // `60`
console.log(aloy.quiver); // `30`
console.log(player.hasOwnProperty('quiver')); // `true`

Une autre chose à noter est que si nous essayons de masquer par affectation une propriété héritée en lecture seule, et si nous sommes en mode strict, l'erreur TypeError est lancée. Ceci est fait indépendemment du fait qu'une propriété est de données ou d'accession. Cependant, si nous masquons la propriété non plus par une affectation, mais par Object.defineProperty, tout est bon :

Code JavaScript

'use strict';

var thunderjaw = Object.defineProperty({}, 'discLauncher', {
    value: 2,
    writable: false
});

// `redmaw` hérite de `thunderjaw`

var redmaw = Object.create(thunderjaw);

console.log(redmaw.discLauncher); // `2`, hérité

// essayer de masquer la propriété `discLauncher`
// et obtenir une erreur en mode
// strict, ou juste échoué silencieusement
// en mode non strict ES5 ou en ES3

redmaw.discLauncher = 1; // `TypeError`

console.log(redmaw.discLauncher); // toujours `2`, si en mode non strict

// cependant le masquage fonctionne
// si on utilise `Object.defineProperty`

Object.defineProperty(redmaw, 'discLauncher', { // OK
    value: 1
});

console.log(redmaw.discLauncher); // et maintenant `1`

Pour en savoir plus à propos du mode strict, lisez le chapitre suivant de la série ES5 sur le mode strict.

Propriété interne

Les propriétés internes ne sont pas une partie des spécifications ECMAScript. Elles sont mises en place par les spécifications du JavaScript pour du fonctionnement interne. Nous en avons déjà discuté dans le Chapitre 7 de la série ES3.

ES5 fournit diversent nouvelles propriétés internes. Vous pouvez trouver des détails dans la section 8.6.2 de la spécification ECMA-262-5. Et, parceque nous avons déjà discuté de ces concepts dans un article ES3, nous ne parlerons ici que des propriétés internes supplémentaires.

Par exemple, les objets en ES5 peuvent être scélés (« sealed »), gelés (« frozen ») ou juste non-extensible, c.-à-d. statiques. La propriété interne [[Extensible]] est liée à ces trois états. Ils peuvent être gérés en utilisant des meta-méthodes spéciales :

Code JavaScript

var faroMachine = {
    gears: 10
};

console.log(Object.isExtensible(faroMachine)); // `true`

Object.preventExtensions(faroMachine);
console.log(Object.isExtensible(faroMachine)); // `false`

faroMachine.weapons = 20; // erreur en mode strict
console.log(faroMachine.weapons); // `undefined`

Notez qu'une fois que la propriété interne [[Extensible]] est mise à false, elle ne peut plus être remise à true.

Mais même depuis des objets non extensibles, diverses propriétés ne peuvent pas être retirées. Pour empécher cela, la meta-méthode Object.seal peut aider car en plus de mettre [[Extensible]] à false, elle met également [[Configurable]] à false sur toutes les propriétés de l'objet :

Code JavaScript

var faroMachine = {
    gears: 10
};

console.log(Object.isSealed(faroMachine)); // `false`

Object.seal(faroMachine);
console.log(Object.isSealed(faroMachine)); // `true`

delete faroMachine.gears; // erreur en mode strict
console.log(faroMachine.gears); // `10`

Si vous souhaitez rendre un objet completement static, c.-à-d., le geler pour empécher de changer les valeurs des propriétés existantes, vous pouvez utiliser la meta-méthode correspondante Object.freeze. Cette méthode va en plus de mettres les propriétés internes [[Configurable]] et [[Extensible]] à false mettre la propriété [[Writable]] à false empéchant les données de propriété de changer :

Code JavaScript

var faroMachine = {
    gears: 10
};

print(Object.isFrozen(faroMachine)); // `false`

Object.freeze(faroMachine);
print(Object.isFrozen(faroMachine)); // `true`

delete faroMachine.gears; // erreur en mode strict
faroMachine.gears = 20; // erreur en mode strict

print(faroMachine.gears); // `10`

Les états scellés ou gelés ne peuvent pas être remis à true.

De la même manière qu'en ES3, nous avons la possibilité d'examiner la propriété interne [[Class]], toujours via la valeur de la méthode Object.prototype.toString :

Code JavaScript

var getClass = Object.prototype.toString;

console.log(
    getClass.call(1), // `'[object Number]'`
    getClass.call({}), // `'[object Object]'`
    getClass.call([]), // `'[object Array]'`
    getClass.call(function () {}) // `'[object Function]'`
    // etc.
);

À la différence de ES3, ECMA-365-5 fournit la possibilité de lire la propriété interne [[Prototype]] via la meta méthode Object.getPrototypeOf. Nous pouvons également créer un objet en spécifiant le prototype souhaité grace à la meta-méthode Object.create :

Code JavaScript

// création de l'objet `plague` avec les propriétés
// `sum` et `length` possédées depuis le `[[Prototype]]`
// `Array.prototype`

var plague = Object.create(Array.prototype, {
  sum: {
    value: function sum() {
      // implémentation de la somme
    }
  },
  // non énumérable mais pas en lecture seule !
  length: {
    value: 0,
    enumerable: false,
    writable: true
  }
});

plague.push(1, 2, 3);

console.log(plague.length); // `3`
console.log(plague.join("-")); `"1-2-3"`

// ni `sum` ou `length` ne peuvent
// être énumérés

for (var machine in plague) {
  console.log(machine); // `0`, `1`, `2`
}

// Récupérer le prototype de `plague`
var plaguePrototype = Object.getPrototypeOf(plague);

console.log(plaguePrototype === Array.prototype); // `true`

Mais malheureusement, même avec cette approche vous ne pourrez toujours pas créer un objet héritant de la « classe » Array.prototype avec toutes les fonctionnalités d'un tableau normal et incluant la méthode interne [[DefineOwnProperty]] (voir 15.4.5.1) qui gère, par exemple, la propriété length. Regardez l'exemple ci-après :

Code JavaScript

plague[5] = 10;
console.log(plague.length); // toujours `3`

Le seul moyen d'hériter complètement de Array.prototype et en même temps d'avoir toutes les méthodes internes surchargées d'un tableau normal (c.-à-d. un objet dont la [[Class]] est "[object Array]") est d'appliquer la propriété non standard __proto__. Aussi le code suivant n'est pas fonctionnelle sur toutes les implémentations :

Code JavaScript

var plague = [];
plague.__proto__= { machine: 10 };
plague.__proto__.__proto__= Array.prototype;

console.log(plague instanceof Array); // `true`

console.log(plague.machine); // `10`

console.log(plague.length); // `0`

plague.push(20);

plague[3] = 30;
console.log(plague.length); // `4`

console.log(plague); // `20`,``,``,`30`

plague.length = 0;
console.log(plague); // tableau vide

Et malheureusement, contrairement à la propriété non standard __proto__ qui est une extension de diverses implémentation ES3, ES5 ne fournit pas la possiblité d'associer un prototype à un objet (seulement de le lire).

Descripteur de propriété et types d'identifieur de propriété

Comme nous l'avons vu, ES5 permet un contrôle des attributs des propriétés. Ce jeu d'attributs de propriété et leurs valeurs sont appelés en ES5 un descripteur de propriété.

En fonction de son type de nom de propriété, un descripteur peut être soit un descripteur de propriété de données ou un descripteur de propriété d'accession.

Les spécifications définissent aussi le concept de descripteur de propriété générique, c.-à-d. un descripteur qui n'est ni un descripteur d'accession, ni un descripteur de données et qui possède tous les attributs de propriété. Mais qu'en est-t-il au niveau de l'implémentation ?

C'est en fonction des valeurs par défaut spécifiées pour les attributs, si un descripteur est vide, une propriété de donnée est créée. Bien sur, une propriété de données est aussi créée si sont object descripteur contient la propriété writable ou value. Dans le cas ou un descripteur d'objet possède la propriété get ou set, une _propriété _d'accession est alors définie. Pour obtenir l'objet descripteur d'une propriété il y a la meta-méthode Object.getOwnPropertyDescriptor :

Code JavaScript

// On définit plusieur propriété en même temps

Object.defineProperties(aloy, {
    weapon: {}, // descripteur « vide »,
    armor: { get: function () {} }
});

var weaponProperty = Object.getOwnPropertyDescriptor(aloy, 'weapon');
var hasOwn = Object.prototype.hasOwnProperty;

console.log(
    weaponProperty.value, // `undefined`
    hasOwn.call(weaponProperty, 'value'), // `true`

    weaponProperty.get, // `undefined`
    hasOwn.call(weaponProperty, 'get'), // `false`

    weaponProperty.set, // `undefined`
    hasOwn.call(weaponProperty, 'set'), // `false`
);

console.log(aloy.weapon); // `undefined` (`null` dans certaine implémentation)
console.log(aloy.nonExisting); // `undefined`

// par contre la propriété `armor` est une propriété d'accession

var armorProperty = Object.getOwnPropertyDescriptor(aloy, 'armor');

console.log(
    armorProperty.value, // `undefined`
    hasOwn.call(armorProperty, 'value'), // `false`

    armorProperty.get, // `function`
    hasOwn.call(armorProperty, 'get'), // `true`

    armorProperty.set, // undefined
    hasOwn.call(armorProperty, 'set'), // `false`
);

Le type de l'identifieur de propriété Property Identifier est utilisé pour associé son propre nom a son descripteur. Ainsi, les propriétés peuvent être des valeurs du type Property Identifier sous forme de paire (name, descriptor) :

Code JavaScript

aloy.focus = 10;

Pseudo-code

// une propriété est un objet
// de type `Property Identifier`

focusProperty = {
  name: 'focus',
  descriptor: {
    value: 10,
    writable: true,
    enumerable: true,
    configurable: true
  }
};

Conclusion

Dans ce premier chapitre nous avons décrit en profondeur un des nouveaux concepts de la spécification ECMA-262-5. Le prochain chapitre sera dédié à l'une des nouveautés majeur de ES5, le mode strict.

Références

Lectures additionnelles :

Ce texte est une libre ré-écriture française de l'excellent billet Тонкости ECMA-262-5. Часть 1. Свойства и дескрипторы свойств de Dmitry Soshnikov.

Lire dans une autre langue