ECMA-262-5 in detail. Chapter 1. Properties and Property Descriptors.

This chapter is devoted generally to one of new concepts of the ECMA-262-5 specification — to property attributes and mechanism of their handling — property descriptors.

Usually saying that “an object has some property” we mean an association between a property name and its value. But as we know from the ES3 analysis, a property structure is more complex than just a string name. It also has set of attributes — those we already discussed in ES3, e.g. {ReadOnly}, {DontEnum} and other. So from this viewpoint a property is as an object itself.

For full understanding of this chapter I recommend reading the Chaper 7.2. OOP: ECMAScript implementation of the ECMA-262-3 series.

But let us give one after another.

In ES3 we had only the direct association between the property name and its value. Although, some implementation having their own extensions provide in ES3 concept of getters and setters, i.e. functions which are indirectly associated with property values. ECMA-262-5 standardizes this concept, and now in all we have three property types.

Also you should know that a property can be either own, i.e. contained directly by an object, or inherited, i.e. contained by one of the objects in the prototype chain.

There are named properties, which available for ECMAScript program and internal properties, which available directly only on implementation level (however, it is possible to manage some of them in ECMAScript program via special methods). We will consider them shortly.

Named properties are distinct by set of attributes. Discussed in ES3 series, property attributes such as {ReadOnly}, {DontEnum} and other, in ES5 has been renamed to mean reversed boolean state. Thus, common for both (data and accessor) named property types in ECMA-262-5 are two attributes:

  • [[Enumerable]]
  • attribute (which stands for reversed state of {DontEnum} in ES3) determines in true state that a property is enumerated by a for-in enumeration.

  • [[Configurable]]
  • attribute (in ES3 — reversed state of {DontDelete}) in false state prevents attempts to delete the property, change the property to be an accessor property, or change its attributes (other than [[Value]]).

Notice, if [[Configurable]] attribute has been set once to false, it cannot be turned back to true. As we just said, we cannot change even other attributes (except [[Value]]) e.g. [[Enumerable]] in such case.

We will discuss other property attributes specific to the corresponding named property types shortly. Let’s consider property types in detail.

These properties which we already have and successfully use in ES3. Such property has a name (which is always a string) and a direct associated with it value.

For example:

// define in the declarative form
var foo = {
  bar: 10 // direct Number type value
};

// define in the imperative form,
// also direct, but Function type value, a "method"
foo.baz = function () {
  return this.bar;
};

The same as in ES3, in case if property value is a function, such property is being called as a method. But, this direct function values should not be confused with indirect special accessor-functions which will be considered below.

In addition to general attributes of named properties, a data property has the following attributes:

  • [[Value]]
  • attribute specifies a value retrieved by reading the property.

  • [[Writable]]
  • attribute (reversed state of {ReadOnly} in ES3) in false state prevents attempts to change the property’s [[Value]] attribute using [[Put]] internal method.

The complete attributes map for a named data property with default values is:

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

So, in default state properties are constants:

// define a global constant

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

alert(MAX_SIZE); // 100

MAX_SIZE = 200; // error in strict mode, [[Writable]] = false,
delete MAX_SIZE; // error in strict mode, [[Configurable]] = false

alert(MAX_SIZE); // still 100

Unfortunately, in ES3 we had no control of property attributes what caused well known issues with augmentation of built-in prototypes. Because of dynamic mutable nature of the objects in ECMAScript, it is very convenient to plug-in the new functionality and use it, delegating to a prototype, as it would be “own” for an object. But without the control of e.g. {DontEnum} attribute in ES3, we all saw the problem with the for-in enumeration over the augmented in prototype arrays:

 // ES3

Array.prototype.sum = function () {
  // sum implementation
};

var a = [10, 20, 30];

// works fine
alert(a.sum()); // 60

// but because of for-in examines the
// prototype chain as well, the new "sum"
// property is also enumerated, because has
// {DontEnum} == false

// iterate over properties
for (var k in a) {
  alert(k); // 0, 1, 2, sum
}

ES5 provides such control using special meta-methods for manipulating the object properties:

Object.defineProperty(Array.prototype, "sum", {

  value: function arraySum() {
    //  sum implementation
  },

  enumerable: false

});

// now with using the same example this "sum"
// is no longer enumerable

for (var k in a) {
  alert(k); // 0, 1, 2
}

In the example above we specify the enumerable attribute manually and explicitly. However, as we mention, the default state for all attributes is false, so we could omit the explicit false setting.

And simple assignment operator now corresponds to the reversed default state of all attributes (actually, what we have in ES3):

// simple assignment (if we create a new property)
foo.bar = 10;

// the same as
Object.defineProperty(foo, "bar", {
  value: 10,
  writable: true,
  enumerable: true,
  configurable: true
});

Notice also that meta-method Object.defineProperty is not only for creating object’s properties, but also for altering them. Moreover, it returns altered object, so we can use this method to bind newly created object to needed variable name, making it in one action:

// create "foo" object and define "bar" property
var foo = Object.defineProperty({}, "bar", {
  value: 10,
  enumerable: true
});

// alter value and enumerable attribute
Object.defineProperty(foo, "bar", {
  value: 20,
  enumerable: false
});

alert(foo.bar); // 20

For getting array of own properties there are two meta-methods: Object.keys, which returns only enumerable properties, and Object.getOwnPropertyNames, which in turn returns also non-enumerable properties:

var foo = {bar: 10, baz: 20};

Object.defineProperty(foo, "x", {
  value: 30,
  enumerable: false
});

alert(Object.keys(foo)); // ["bar", "baz"]
alert(Object.getOwnPropertyNames(foo)); // ["bar", "baz", "x"]

A named accessor property associates a name (also — only a string) with one or two accessor functions: a getter and a setter. The accessor functions are used to store or retrieve a value that is associated with the property name indirectly.

As we have noticed, some implementations of ES3 already had this concept. But ES5 specifies this officially and provides slightly different syntax (from e.g. corresponding extension of SpiderMonkey) of such property kind definition.

In addition to general attributes, an accessor property has the following properties which related with a getter and a setter respectively:

  • [[Get]]
  • attribute is a function object which is called every time for retrieving indirect value related with the property name. Do not confuse this property attribute with the same name internal method of an object — the general reader of a property value. So in case of accessor property, internal [[Get]] method of an object calls [[Get]] attribute of a property of the object.

  • [[Set]]
  • attribute is being also a function, used in turn for setting the new value associated with a name. This attribute is called by the [[Put]] internal method of an object.

Notice, that the effect of the [[Set]] may, but is not required to, have an effect on the value returned by subsequent calls to the property’s [[Get]] internal method. In other words, if we set to a property e.g. a value 10, a getter then can return completely different value, e.g. 20, because association is indirect.

And the complete attributes map for a named accessor property with default values is:

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

Accordingly, if [[Set]] attribute is absent, an accessor property is read only — like in case of false [[Writable]] attribute of a data property.

An accessor property can be defined either using already mentioned above meta-method Object.defineProperty:

var foo = {};

Object.defineProperty(foo, "bar", {

  get: function getBar() {
    return 20;
  },

  set: function setBar(value) {
    // setting implementation
  }

});

foo.bar = 10; // calls foo.bar.[[Set]](10)

// independently always 20
alert(foo.bar); // calls foo.bar.[[Get]]()

Or also in the declarative view using object initialiser:

var foo = {

  get bar () {
    return 20;
  },

  set bar (value) {
    alert(value);
  }

};

foo.bar = 100;
alert(foo.bar); // 20

Notice also one important feature related with configuration of an accessor property. As we have mentioned in [[Configurable]] attribute description, if it once has been set to false a property cannot be configured anymore (except the [[Value]] attribute of a data property). It can confuse e.g. in the following case:

// configurable false by default
var foo = Object.defineProperty({}, "bar", {
  get: function () {
    return "bar";
  }
});

// trying to reconfigure the "bar"
// property => exception is thrown
try {
  Object.defineProperty(foo, "bar", {
    get: function () {
      return "baz"
    }
  });
} catch (e if e instanceof TypeError) {
  alert(foo.bar); // still "bar"
}

But exception won’t be thrown if reconfiguring attribute’s value in this case is the same. Although, it is not so useful (or even — useless) on practice, because we actually not reconfigure the getter and it still the same:

function getBar() {
  return "bar";
}

var foo = Object.defineProperty({}, "bar", {
  get: getBar
});

// no exception even if configurable is false,
// but practically such "re"-configuration is useless
Object.defineProperty(foo, "bar", {
  get: getBar
});

And as we have mentioned, the [[Value]] of a data property can be reconfigured even if [[Configurable]] is in false state; of course its [[Writable]] attribute should be set to true:

var foo = Object.defineProperty({}, "bar", {
  value: "bar",
  writable: true
});

Object.defineProperty(foo, "bar", {
  value: "baz"
});

alert(foo.bar); // "baz"

Also we can’t transform a property from the data to the accessor type and vice-versa if its [[Configuragle]] attribute if false. In true state of [[Configuragle]] attribute such transformation is possible; thus, the state of the [[Writable]] attribute is not important and can be false:

// writable false by default
var foo = Object.defineProperty({}, "bar", {
  value: "bar",
  configurable: true
});

Object.defineProperty(foo, "bar", {
  get: function () {
    return "baz";
  }
});

alert(foo.bar); // OK, "baz"

Another obvious fact, that a property cannot be data and accessor types at the same time. That means that presence of some mutually exclusive attributes throws an exception:

// error, "get" and "writable" at the same time
var foo = Object.defineProperty({}, "bar", {
  get: function () {
    return "baz";
  },
  writable: true
});

// also error: mutually exclusive  "value" and "set" attributes
var baz = Object.defineProperty({}, "bar", {
  value: "baz",
  set: function (v) {}
})

I would like also to remind, that using setters and getters is defensible and has a sense only if we need to encapsulate some complex calculations having auxiliary helper data, and making usage of this property convenient — i.e. just it would be a simple data property. I already wrote about it in section devoted to encapsulation on example with element.innerHTML property — where we abstractly statement that “now html of this element is the following”, while in setter function for the innerHTML property there is difficult calculations and checks which cause then rebuilding of the DOM tree and updating the user interface.

There is no any sense to use setters/getters for absolutely non-abstract entities, such as:

// don't do this

Object.defineProperty(foo, "bar", {

  get: function getBar() {
    return this.baz;
  },

  set: function setBar(value) {
    this.baz = value;
  }
});

foo.bar = 10;

alert(foo.bar); // 10
alert(foo.baz); // 10

Not only that we use accessors for non-abstract entity, but we also have created own “baz” property. Of course in such cases should be used simple data property which increases performance. Although, I can imagine for what purpose such situation could be needed — e.g. for aliasing (the same semantically entities but with different names — e.g. size and length of some collection), but practically it is not needed often.

The cases which really deserve using accessors should be related, repeat, with increasing of an abstraction, encapsulating auxiliary helper data. The simplest example:

// encapsulated context
(function () {

  // some internal state
  var data = [];

  Object.defineProperty(foo, "bar", {

    get: function getBar() {
      return "We have " + data.length + " bars: " + data;
    },

    set: function setBar(value) {

      // call getter first
      alert(this.bar);

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

      // of course if needed we can update
      // also some public property
      this.baz = 'updated from "bar" setter: ' + value;

    },

    configurable: true,
    enumerable: true

  });

})();

foo.baz = 100;
alert(foo.baz); // 100

// first getter will be called inside the setter:
// We have 0 bars:
foo.bar = 3;

// getting
alert(o.bar); // We have 3 bars: bar, bar, bar
alert(o.baz); // updated from "bar" setter: 3

Of course this example isn’t so practical, but it shows the main purpose of accessors — increasing of an abstraction encapsulating auxiliary internal data.

The internal properties are not part of the ECMAScript language. They are defined by the specification purely for expository purposes. We already discussed them for ES3 specification.

ES5 in addition provides some new internal properties. You can find detailed description of all these properties in section 8.6.2. of the ECMA-262-5 specification. And because of we already discussed this concept in ES3 article, here we mention only some new internal properties.

For example, objects in ES5 can be sealed, frozen or just non-extensible, i.e. static. With all three states an internal [[Extensible]] property is related. It can be managed using special meta-methods:

var foo = {bar: 10};

alert(Object.isExtensible(foo)); // true

Object.preventExtensions(foo);
alert(Object.isExtensible(foo)); // false

foo.baz = 20; // error in "strict" mode
alert(foo.baz); // undefined

Note, that [[Extensible]] internal property once setting to false cannot be turned back to true.

But even from non-extensible object some properties can be removed. To prevent this case a meta-method Object.seal can help which besides the [[Extensible]] internal property also set to false [[Configurable]] attribute of all properties of an object:

var foo = {bar: 10};

alert(Object.isSealed(foo)); // false

Object.seal(foo);
alert(Object.isSealed(foo)); // true

delete foo.bar; // error in strict mode
alert(foo.bar); // 10

If we want to make an object completely static i.e. to freeze it, preventing also changing values of existing properties, then we can use corresponding Object.freeze meta-method. This method besides mentioned [[Configurable]] attribute and [[Extensible]] internal property, also for data properties sets attribute [[Writable]] to false:

var foo = {bar: 10};

print(Object.isFrozen(foo)); // false

Object.freeze(foo);
print(Object.isFrozen(foo)); // true

delete foo.bar; // error in strict mode
foo.bar = 20; // error in strict

print(foo.bar); // 10

Both sealed and frozen states cannot be turned off back.

The same as in ES3 we have ability to examine a [[Class]] internal property — still via the default value of Object.prototype.toString method:

var getClass = Object.prototype.toString;

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

In contrast with ES3, ECMA-262-5 provides ability for reading the internal [[Prototype]] property — via the meta-method Object.getPrototypeOf. Also in the current version of the specification we can create object with specifying needed prototype object — using a meta-method Object.create:

// create "foo" object with two own
// properties "sum" and "length" and which has
// Array.prototype as its [[Prototype]] property

var foo = Object.create(Array.prototype, {
  sum: {
    value: function arraySum() {
      // sum implementation
    }
  },
  // non-enumerable but writable!
  // else array methods won't work
  length: {
    value: 0,
    enumerable: false,
    writable: true
  }
});

foo.push(1, 2, 3);

alert(foo.length); // 3
alert(foo.join("-")); "1-2-3"

// neither "sum", nor "length"
// are enumerable

for (var k in foo) {
  alert(k); // 0, 1, 2
}

// getting prototype of "foo"
var fooPrototype = Object.getPrototypeOf(foo);

alert(fooPrototype === Array.prototype); // true

But unfortunately, using even such approach we still cannot create an inherited from Array.prototype “class” with all functionality of normal arrays including overloaded [[DefineOwnProperty]] internal method (see 15.4.5.1) which handles e.g. length property. Having example above:

foo[5] = 10;
alert(foo.length); // still 3

Still the only way to inherit from Array.prototype and at the same time to have all related overloaded internal methods, is to use normal array (i.e. an object which [[Class]] is “Array”) and apply non-standard __proto__ property. So this is available not for all implementations:

var foo = [];
foo.__proto__= {bar: 10};
foo.__proto__.__proto__= Array.prototype;

alert(foo instanceof Array); // true

alert(foo.bar); // 10

alert(foo.length); // 0

foo.push(20);

foo[3] = 30;
alert(foo.length); //4

alert(foo); // 20,,,30

foo.length = 0;
alert(foo); // empty array

And unfortunately, in contrast with non-standard __proto__ extension of some ES3 implementations, ES5 does not provide ability for setting an object’s prototype.

As we saw ES5 allows control of the property attributes. The set of property’s attributes and their values is called in ES5 as a property descriptor.

Related with corresponding named property type, a descriptor can be either a data property descriptor or an accessor property descriptor.

Specification also defines concept of a generic property descriptor, i.e. a descriptor that is neither a data descriptor nor an accessor descriptor. And also a fully populated property descriptor that is either an accessor descriptor or a data descriptor and that has all of the fields that correspond to the property attributes. But that relates mostly to the implementation level.

Thus, because of specified default values for the attributes, if corresponding descriptor is empty, a data property is created. Obviously, a data property is also created if descriptor object contains either writable or value properties. In case if a descriptor object has either a get or a set property, accordingly an accessor property is defined. For getting the descriptor object of a property there is meta-method Object.getOwnPropertyDescriptor:

// define several properties at once
Object.defineProperties(foo, {
  bar: {}, // "empty" descriptor,
  baz: {get: function () {}}
});

var barProperty = Object.getOwnPropertyDescriptor(foo, "bar");
var hasOwn = Object.prototype.hasOwnProperty;

alert([
  barProperty.value, // undefined
  hasOwn.call(barProperty, "value"), // true

  barProperty.get, // undefined
  hasOwn.call(barProperty, "get"), // false

  barProperty.set, // undefined
  hasOwn.call(barProperty, "set"), // false
]);

alert(foo.bar); // undefined (correct), in Rhino 1.73 - null
alert(foo.nonExisting); // undefined and in Rhino too

// in contrast "baz" property is an accessor property

var bazProperty = Object.getOwnPropertyDescriptor(foo, "baz");

alert([
  bazProperty.value, // undefined
  hasOwn.call(bazProperty, "value"), // false

  bazProperty.get, // function
  hasOwn.call(bazProperty, "get"), // true

  bazProperty.set, // undefined
  hasOwn.call(bazProperty, "set"), // false
]);

And Property Identifier type is used to associate a property name with its descriptor. So, properties being values of the Property Identifier type are pairs of the form (name, descriptor):

Abstractly:

foo.bar = 10;

// property is an object of
// the Property Identifier type

var barProperty = {
  name: "bar",
  descriptor: {
    value: 10,
    writable: true,
    enumerable: true,
    configurable: true
  }
};

In this first chapter we closely acquainted with one of the new concepts of the ECMA-262-5 specification. The next chapters will be dedicated to the new details of execution contexts such as Lexical Environments, Environment Records and other. As always, if you have questions or additions, we can discuss them in comments.

  • 4.3 Definitions;
  • 8.6 The Object Type;
  • 8.10 The Property Descriptor and Property Identifier Specification Types;
  • 8.12 Algorithms for Object Internal Methods;
  • 15.2.3 Properties of the Object Constructor.


Written by: Dmitry A. Soshnikov.
Published on: 2010-04-28


Tags: , , , , , ,

 
 
 

5 Comments:

  1. Gravatar of Anton Anton
    29. April 2010 at 16:15

    Надеюсь это все-таки появится на русском, все-таки не так легко читается


  2. Gravatar of Dmitry A. Soshnikov Dmitry A. Soshnikov
    29. April 2010 at 16:32

    @Anton

    Посмотрим. Возможно позже, когда я допишу все части этой новой серии на английском; параллельно вряд ли сейчас буду писать.

    Но, этим может заняться кто-нибудь другой — естественно заинтересованный в JS и в этом цикле статей. Отсюда тогда будут ссылки. Постепенно, я смогу проверить качество и точность перевода в техническом плане.


  3. Gravatar of kangax kangax
    30. April 2010 at 08:07

    Nice overview :)

    Few things you might want to add:

    “all implementations creates a data property if corresponding descriptor is empty, and an accessor property if descriptor object has either a get or a set property”

    Of course data descriptor is created _not only_ when corresponding object is empty, but also when “writable” or “value” properties are present (you only mention empty and get/set).

    I find it easy to think about it in this way:

    1) If object is empty or value/writable properties are present, data descriptor is created

    2) If get/set properties are present, accessor descriptor is created

    Btw, does generic descriptor (which should be created if object is empty) serve any useful purpose?

    You can also mention that `TypeError` is supposed to be thrown when creating a property descriptor out of an object that has both “value”/”writable” and “get”/”set” properties (as per 8.10.5 — ToPropertyDescriptor).

    Another “gotcha” I didn’t see in the article is what happens when you try to overwrite accessor property. For example:

    var o = Object.defineProperty({ }, 'x', {
      get: function(){ return 'bar' }
    });
    
    Object.defineProperty(o, 'x', {
      get: function(){ return 'baz' }
    });
    
    o.x; // "bar"

    It might not be obvious, but property can only be overwritten when its [[Configurable]] is not `false`. Since in this example we do not set [[Configurable]] explicitly and its default value is `false`, the property can not be overwritten and an error is thrown. Defining it with [[Configurable]] == true obviously “solves” the problem:

    var o = Object.defineProperty({ }, 'x', {
      get: function(){ return 'bar' },
      configurable: true
    });
    
    Object.defineProperty(o, 'x', {
      get: function(){ return 'baz' }
    });
    
    o.x; // "baz"

  4. Gravatar of Jens Jens
    30. April 2010 at 11:37

    Great article and… thank you!


  5. Gravatar of Dmitry A. Soshnikov Dmitry A. Soshnikov
    30. April 2010 at 14:56

    @Jens

    thanks.

    @kangax

    thanks for additions.

    Of course data descriptor is created _not only_ when corresponding object is empty, but also when “writable” or “value” properties are present (you only mention empty and get/set).

    Yes, of course, but this case seems obvious. The main goal is to specify what happens when we have an empty descriptor object. Although, I added this case too.

    You can also mention that `TypeError` is supposed to be thrown when creating a property descriptor out of an object that has both “value”/”writable” and “get”/”set” properties (as per 8.10.5 — ToPropertyDescriptor).

    Yes, added.

    It might not be obvious, but property can only be overwritten when its [[Configurable]] is not `false`. Since in this example we do not set [[Configurable]] explicitly and its default value is `false`, the property can not be overwritten and an error is thrown. Defining it with [[Configurable]] == true obviously “solves” the problem:

    Yup, also a good addition. Although, it is mentioned in the [[Configurable]] attribute definition, it is good to specify this case explicitly.

    By the way, the error is not thrown if reconfiguring attribute’s value (a getter in this case) is the same (because of SameValue check in 8.12.9). But practically such “re”-configuration seems useless.

    I mentioned all that cases, though.

    Btw, does generic descriptor (which should be created if object is empty) serve any useful purpose?

    No, don’t think so. At least — not for ECMAScript program. Of course, it serves a useful purpose on implementation level. See again “8.12.9 [[DefineOwnProperty]]”, where checks such as IsGenericDescriptor and other are everywhere. There also said, that default value for absent attributes is false, so for the ECMAScript program it doesn’t matter.

    Dmitry.


Leave a Reply

Code: For code you can use tags [js], [text], [ruby] and other.

XHTML: You can use these tags: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>