JavaScript Object.defineProperty()

In Javascript, we can add a property to an object with:

obj.property = value;

However, sometimes we would like to have fine-grained control over properties. With Object.defineProperty(), we can decide:

Descriptor Purpose Default
value Property value undefined
writable Whether the property can be assigned false
get Getter of the property undefined
set Setter of the property undefined
enumerable Whether the property is visible to for-each loop false
configurable Whether the property can be redefined false

Object.defineProperty(obj, name, desc) expects three arguments:

  1. The obj argument is the object to which we would like to add a new property.
  2. The name argument is the name of the new property.
  3. The desc argument is the descriptor object of the new property. If enumerable, configurable, or writable are not defined in the descriptor object, then the default value false will be assumed.

Value and Writable

The simplest usage is to define a property with a value. By default, it will become a read-only property. Every assignment to the property will be silently ignored.

var t = Object.create(null);
Object.defineProperty(t, 'fahrenheit', { value: 32 });

// Get the value.
console.log(t.fahrenheit);  // Prints 32.

// Set the value.
t.fahrenheit = 5;  // Ignored silently.
console.log(t.fahrenheit);  // Continue to print 32.

Under the strict mode, assigning to a non-writable property will raise a TypeError instead:

'use strict';  // CHANGED

var t = Object.create(null);
Object.defineProperty(t, 'fahrenheit', { value: 32 });

try {
    // Set the value.
    t.fahrenheit = 5;  // Raise a TypeError exception.
} catch (ex) {
    if (ex instanceof TypeError) {
        console.log('CAUGHT EXPECTED EXCEPTION.');
    } else {
        console.trace(ex);
    }
}

To define a property that can be assigned, we have to set writable to true:

var t = Object.create(null);
Object.defineProperty(t, 'fahrenheit', {
    value: 32,
    writable: true  // CHANGED
});

t.fahrenheit = 41;
console.log(t.fahrenheit);  // Prints 41

From some perspective, the code snippet above is similar to the following simple assignment:

var t = Object.create(null);
t.fahrenheit = 32;  // Similar

t.fahrenheit = 41;
console.log(t.fahrenheit);  // Prints 41

Note

The difference between two code snippets is the enumerability of the defined property. Read Enumerable section for more information.

Get and Set

Imagine a senario: we would like to create an object to represent a temperature in both celsius and fahrenheit. Besides, we would like to maintain the correspondence between them. What can we do? Getters and setters are the answers.

We can define a get or set function with Object.defineProperty(). The get function can read values from other properties or compute the result on-the-fly. Similarly, the set function can write values to other properties. For example:

var t = Object.create(null);
Object.defineProperty(t, 'fahrenheit', {
    'get': function () {
        return (this.celsius * 9 / 5 + 32);
    },
    'set': function (x) {
        this.celsius = (x - 32) * 5 / 9;
    }
});

t.celsius = 0;
console.log(t.fahrenheit + ' F');  // Prints: "32 F"

t.celsius = 50;
console.log(t.fahrenheit + ' F');  // Prints "122 F"

t.fahrenheit = 77;
console.log(t.fahrenheit + ' F');  // Prints "77 F"
console.log(t.celsius + ' C');  // Prints "25 C"

Similar to writable: false, if the set descriptor is not specified, then the assignment operations to the property will be ignored silently:

var t = Object.create(null);
Object.defineProperty(t, 'fahrenheit', {
    'get': function () {
        return (this.celsius * 9 / 5 + 32);
    }
});

t.celsius = 0;
t.fahrenheit = 77;  // Silently ignored.
console.log(t.fahrenheit + ' F');  // Prints "32 F"
console.log(t.celsius + ' C');  // Prints "0 C"

Under the strict mode, a TypeError will be raised instead:

'use strict';  // CHANGED

var t = Object.create(null);
Object.defineProperty(t, 'fahrenheit', {
    'get': function () {
        return (this.celsius * 9 / 5 + 32);
    }
});

try {
    t.fahrenheit = 77;
} catch (ex) {
    if (ex instanceof TypeError) {
        console.log('CAUGHT EXPECTED EXCEPTION.');
    } else {
        console.trace(ex);
    }
}

Note

get and set can not be mixed with value and writable.

Enumerable

By default, when Object.defineProperty() defines a new property, the newly defined property won't be enumerable, i.e. the newly defined property won't be enumerated in the for-each loop:

var t = Object.create(null);
Object.defineProperty(t, 'fahrenheit', { value: 32 });

// Enumerate the keys.
for (var key in t) {
    // This loop won't print anything because the `fahrenheit` property is
    // NOT enumerable.
    console.log('Found: ' + key);
}

If we wish to enumerate the newly defined property, then we can set enumerable to true:

var t = Object.create(null);
Object.defineProperty(t, 'fahrenheit', {
    enumerable: true,  // CHANGED
    value: 32
});

// Enumerate the keys.
for (var key in t) {
    console.log('Found: ' + key);  // Prints: "Found: fahrenheit"
}

Configurable

By default, the property defined by Object.defineProperty() can not be redefined or reconfigured. A TypeError will be raised if you try to do so.

var t = Object.create(null);
Object.defineProperty(t, 'fahrenheit', { value: 32 });

try {
    // Redefine the property.
    Object.defineProperty(t, 'fahrenheit', { value: 50 });
} catch (ex) {
    if (ex instanceof TypeError) {
        console.log('CAUGHT EXPECTED EXCEPTION.');
    } else {
        console.trace(ex);
    }
}

If you really wish to redefine the property afterwards, then you should set configurable to true.

var t = Object.create(null);
Object.defineProperty(t, 'fahrenheit', {
    configurable: true,  // CHANGED
    value: 32
});
console.log(t.fahrenheit);  // Prints: 32

// Redefine the property.
Object.defineProperty(t, 'fahrenheit', { value: 50 });
console.log(t.fahrenheit);  // Prints: 50

Conclusion

In this post, we have learned several usages of Object.defineProperty():

  • One can define a property with value and control its immutability with writable.
  • One can define a property with getter and setter functions. The getter and setter functions are specified by get and set descriptors respectively.
  • To enumerate a property in a for-each loop, specify enumerable.
  • To allow the reconfiguration of a property, specify configurable.

I hope you enjoy this post. Feel free to let me know if you have any comments.

References