loading...

JavaScript – Property Attributes

In addition to a name and value, properties have attributes that
specify whether they can be written, enumerated, and configured. In
ECMAScript 3, there is no way to set these attributes: all properties
created by ECMAScript 3 programs are writable, enumerable, and
configurable, and there is no way to change this. This section
explains the ECMAScript 5 API for querying and setting property
attributes. This API is particularly important to library authors
because:

  • It allows them to add methods to prototype objects and make
    them nonenumerable, like built-in methods.

  • It allows them to “lock down” their objects, defining
    properties that cannot be changed or deleted.

For the purposes of this section, we are going to consider
getter and setter methods of an accessor property to be property
attributes. Following this logic, we’ll even say that the value of a
data property is an attribute as well. Thus, we can say that a
property has a name and four attributes. The four attributes of a data
property are value,
writable, enumerable, and
configurable. Accessor properties don’t have a
value attribute or a
writable attribute: their writability is
determined by the presence or absence of a setter. So the four
attributes of an accessor property are get,
set, enumerable, and
configurable.

The ECMAScript 5 methods for querying and setting the attributes
of a property use an object called a property
descriptor
to represent the set of four attributes. A
property descriptor object has properties with the same names as the
attributes of the property it describes. Thus, the property descriptor
object of a data property has properties named value, writable, enumerable, and configurable. And the descriptor for an
accessor property has get and
set properties instead of value and writable. The writable, enumerable, and configurable properties are boolean values,
and the get and set properties are function values, of
course.

To obtain the property descriptor for a named property of a
specified object, call Object.getOwnPropertyDescriptor():

// Returns {value: 1, writable:true, enumerable:true, configurable:true}
Object.getOwnPropertyDescriptor({x:1}, "x");

// Now query the octet property of the random object defined above.
// Returns { get: /*func*/, set:undefined, enumerable:true, configurable:true}
Object.getOwnPropertyDescriptor(random, "octet");

// Returns undefined for inherited properties and properties that don't exist.
Object.getOwnPropertyDescriptor({}, "x");         // undefined, no such prop
Object.getOwnPropertyDescriptor({}, "toString");  // undefined, inherited

As its name implies, Object.getOwnPropertyDescriptor() works only
for own properties. To query the attributes of inherited properties,
you must explicitly traverse the prototype chain (see Object.getPrototypeOf() in The prototype Attribute).

To set the attributes of a property, or to create a new property
with the specified attributes, call Object.defineProperty(), passing the object
to be modified, the name of the property to be created or altered, and
the property descriptor object:

var o = {};  // Start with no properties at all
// Add a nonenumerable data property x with value 1.
Object.defineProperty(o, "x", { value : 1, 
                                writable: true,
                                enumerable: false,
                                configurable: true});

// Check that the property is there but is nonenumerable
o.x;           // => 1
Object.keys(o) // => []

// Now modify the property x so that it is read-only
Object.defineProperty(o, "x", { writable: false });

// Try to change the value of the property
o.x = 2;      // Fails silently or throws TypeError in strict mode
o.x           // => 1

// The property is still configurable, so we can change its value like this:
Object.defineProperty(o, "x", { value: 2 });
o.x           // => 2

// Now change x from a data property to an accessor property
Object.defineProperty(o, "x", { get: function() { return 0; } });
o.x           // => 0

The property descriptor you pass to Object.defineProperty() does not have to
include all four attributes. If you’re creating a new property, then
omitted attributes are taken to be false or undefined. If you’re modifying an existing
property, then the attributes you omit are simply left unchanged. Note
that this method alters an existing own property or creates a new own
property, but it will not alter an inherited property.

If you want to create or modify more than one property at a
time, use Object.defineProperties(). The first
argument is the object that is to be modified. The second argument is
an object that maps the names of the properties to be created or
modified to the property descriptors for those properties. For
example:

var p = Object.defineProperties({}, {
    x: { value: 1, writable: true, enumerable:true, configurable:true },  
    y: { value: 1, writable: true, enumerable:true, configurable:true },
    r: { 
        get: function() { return Math.sqrt(this.x*this.x + this.y*this.y) },
        enumerable:true,
        configurable:true
    }
});

This code starts with an empty object, then adds two data
properties and one read-only accessor property to it. It relies on the
fact that Object.defineProperties()
returns the modified object (as does Object.defineProperty()).

We saw the ECMAScript 5 method Object.create() in Creating Objects. We learned there that the first argument
to that method is the prototype object for the newly created object.
This method also accepts a second optional argument, which is the same
as the second argument to Object.defineProperties(). If you pass a set
of property descriptors to Object.create(), then they are used to add
properties to the newly created object.

Object.defineProperty() and
Object.defineProperties() throw
TypeError if the attempt to create or modify a property is not
allowed. This happens if you attempt to add a new property to a
nonextensible (see The extensible Attribute) object. The
other reasons that these methods might throw TypeError have to do with
the attributes themselves. The writable attribute
governs attempts to change the value attribute.
And the configurable attribute governs attempts
to change the other attributes (and also specifies whether a property
can be deleted). The rules are not completely straightforward,
however. It is possible to change the value of a nonwritable property
if that property is configurable, for example. Also, it is possible to
change a property from writable to nonwritable even if that property
is nonconfigurable. Here are the complete rules. Calls to Object . define Property() or Object.defineProperties() that attempt to
violate them throw TypeError:

  • If an object is not extensible, you can edit its existing
    own properties, but you cannot add new properties to it.

  • If a property is not configurable, you cannot change its
    configurable or enumerable attributes.

  • If an accessor property is not configurable, you cannot
    change its getter or setter method, and you cannot change it to a
    data property.

  • If a data property is not configurable, you cannot change it
    to an accessor property.

  • If a data property is not configurable, you cannot change
    its writable attribute from false to true, but you can change it from
    true to false.

  • If a data property is not configurable and not writable, you
    cannot change its value. You can change the value of a property
    that is configurable but nonwritable, however (because that would
    be the same as making it writable, then changing the value, then
    converting it back to nonwritable).

Example 6-2 included an extend() function that copied properties
from one object to another. That function simply copied the name and
value of the properties and ignored their attributes. Furthermore, it
did not copy the getter and setter methods of accessor properties, but
simply converted them into static data properties. Example 6-3 shows a new version of extend() that uses Object.getOwnPropertyDescriptor() and
Object.defineProperty() to copy all
property attributes. Rather than being written as a function, this
version is defined as a new Object method and is added as a
nonenumerable property to Object.prototype.

Example 6-3. Copying property attributes

/*
 * Add a nonenumerable extend() method to Object.prototype.
 * This method extends the object on which it is called by copying properties
 * from the object passed as its argument.  All property attributes are
 * copied, not just the property value.  All own properties (even non-
 * enumerable ones) of the argument object are copied unless a property
 * with the same name already exists in the target object.
 */
Object.defineProperty(Object.prototype,
    "extend",                  // Define Object.prototype.extend
    {
        writable: true,
        enumerable: false,     // Make it nonenumerable
        configurable: true,
        value: function(o) {   // Its value is this function
            // Get all own props, even nonenumerable ones
            var names = Object.getOwnPropertyNames(o);
            // Loop through them
            for(var i = 0; i < names.length; i++) {
                // Skip props already in this object
                if (names[i] in this) continue;
                // Get property description from o
                var desc = Object.getOwnPropertyDescriptor(o,names[i]);
                // Use it to create property on this
                Object.defineProperty(this, names[i], desc);
            }
        }
    });

Legacy API for Getters and Setters

The object literal syntax for accessor properties described in
Property Getters and Setters allows us to define accessor
properties in new objects, but it doesn’t allow us to query the
getter and setter methods or to add new accessor properties to
existing objects. In ECMAScript 5 we can use Object.getOwnPropertyDescriptor() and
Object.defineProperty() to do
these things.

Most JavaScript implementations (with the major exception of
the IE web browser) supported the object literal get and set syntax even before the adoption of
ECMAScript 5. These implementations
support a nonstandard legacy API for querying and setting getters
and setters. This API consists of four methods available on all
objects. __lookupGetter__() and
__lookupSetter__() return the
getter or setter method for a named property. And __defineGetter__() and __defineSetter__() define a getter or
setter: pass the property name first and the getter or setter method
second. The names of each of these methods begin and end with double
underscores to indicate that they are nonstandard methods. These
nonstandard methods are not documented in the reference
section.

Comments are closed.

loading...