Deep JavaScript
Please support this book: buy it or donate
(Ad, please don’t block.)

9 Property attributes: an introduction



In this chapter, we take a closer look at how the ECMAScript specification sees JavaScript objects. In particular, properties are not atomic in the spec, but composed of multiple attributes (think fields in a record). Even the value of a data property is stored in an attribute!

9.1 The structure of objects

In the ECMAScript specification, an object consists of:

9.1.1 Internal slots

The specification describes internal slots as follows. I added bullet points and emphasized one part:

There are two kinds of internal slots:

Ordinary objects have the following data slots:

9.1.2 Property keys

The key of a property is either:

9.1.3 Property attributes

There are two kinds of properties and they are characterized by their attributes:

Additionally, there are attributes that both kinds of properties have. The following table lists all attributes and their default values.

Kind of property Name and type of attribute Default value
Data property value: any undefined
writable: boolean false
Accessor property get: (this: any) => any undefined
set: (this: any, v: any) => void undefined
All properties configurable: boolean false
enumerable: boolean false

We have already encountered the attributes value, get, and set. The other attributes work as follows:

9.1.3.1 Pitfall: Inherited non-writable properties prevent creating own properties via assignment

If an inherited property is non-writable, we can’t use assignment to create an own property with the same key:

const proto = {
  prop: 1,
};
// Make proto.prop non-writable:
Object.defineProperty(
  proto, 'prop', {writable: false});

const obj = Object.create(proto);

assert.throws(
  () => obj.prop = 2,
  /^TypeError: Cannot assign to read only property 'prop'/);

For more information, see §11.3.4 “Inherited read-only properties prevent creating own properties via assignment”.

9.2 Property descriptors

A property descriptor encodes the attributes of a property as a JavaScript object. Their TypeScript interfaces look as follows.

interface DataPropertyDescriptor {
  value?: any;
  writable?: boolean;
  configurable?: boolean;
  enumerable?: boolean;
}
interface AccessorPropertyDescriptor {
  get?: (this: any) => any;
  set?: (this: any, v: any) => void;
  configurable?: boolean;
  enumerable?: boolean;
}
type PropertyDescriptor = DataPropertyDescriptor | AccessorPropertyDescriptor;

The question marks indicate that all properties are optional. §9.7 “Omitting descriptor properties” describes what happens if they are omitted.

9.3 Retrieving descriptors for properties

9.3.1 Object.getOwnPropertyDescriptor(): retrieving a descriptor for a single property

Consider the following object:

const legoBrick = {
  kind: 'Plate 1x3',
  color: 'yellow',
  get description() {
    return `${this.kind} (${this.color})`;
  },
};

Let’s first get a descriptor for the data property .color:

assert.deepEqual(
  Object.getOwnPropertyDescriptor(legoBrick, 'color'),
  {
    value: 'yellow',
    writable: true,
    enumerable: true,
    configurable: true,
  });

This is what the descriptor for the accessor property .description looks like:

const desc = Object.getOwnPropertyDescriptor.bind(Object);
assert.deepEqual(
  Object.getOwnPropertyDescriptor(legoBrick, 'description'),
  {
    get: desc(legoBrick, 'description').get, // (A)
    set: undefined,
    enumerable: true,
    configurable: true
  });

Using the utility function desc() in line A ensures that .deepEqual() works.

9.3.2 Object.getOwnPropertyDescriptors(): retrieving descriptors for all properties of an object

const legoBrick = {
  kind: 'Plate 1x3',
  color: 'yellow',
  get description() {
    return `${this.kind} (${this.color})`;
  },
};

const desc = Object.getOwnPropertyDescriptor.bind(Object);
assert.deepEqual(
  Object.getOwnPropertyDescriptors(legoBrick),
  {
    kind: {
      value: 'Plate 1x3',
      writable: true,
      enumerable: true,
      configurable: true,
    },
    color: {
      value: 'yellow',
      writable: true,
      enumerable: true,
      configurable: true,
    },
    description: {
      get: desc(legoBrick, 'description').get, // (A)
      set: undefined,
      enumerable: true,
      configurable: true,
    },
  });

Using the helper function desc() in line A ensures that .deepEqual() works.

9.4 Defining properties via descriptors

If we define a property with the key k via a property descriptor propDesc, then what happens depends:

9.4.1 Object.defineProperty(): defining single properties via descriptors

First, let us create a new property via a descriptor:

const car = {};

Object.defineProperty(car, 'color', {
  value: 'blue',
  writable: true,
  enumerable: true,
  configurable: true,
});

assert.deepEqual(
  car,
  {
    color: 'blue',
  });

Next, we change the kind of a property via a descriptor; we turn a data property into a getter:

const car = {
  color: 'blue',
};

let readCount = 0;
Object.defineProperty(car, 'color', {
  get() {
    readCount++;
    return 'red';
  },
});

assert.equal(car.color, 'red');
assert.equal(readCount, 1);

Lastly, we change the value of a data property via a descriptor:

const car = {
  color: 'blue',
};

// Use the same attributes as assignment:
Object.defineProperty(
  car, 'color', {
    value: 'green',
    writable: true,
    enumerable: true,
    configurable: true,
  });

assert.deepEqual(
  car,
  {
    color: 'green',
  });

We have used the same property attributes as assignment.

9.4.2 Object.defineProperties(): defining multiple properties via descriptors

Object.defineProperties() is the multi-property version of `Object.defineProperty():

const legoBrick1 = {};
Object.defineProperties(
  legoBrick1,
  {
    kind: {
      value: 'Plate 1x3',
      writable: true,
      enumerable: true,
      configurable: true,
    },
    color: {
      value: 'yellow',
      writable: true,
      enumerable: true,
      configurable: true,
    },
    description: {
      get: function () {
        return `${this.kind} (${this.color})`;
      },
      enumerable: true,
      configurable: true,
    },
  });

assert.deepEqual(
  legoBrick1,
  {
    kind: 'Plate 1x3',
    color: 'yellow',
    get description() {
      return `${this.kind} (${this.color})`;
    },
  });

9.5 Object.create(): Creating objects via descriptors

Object.create() creates a new object. Its first argument specifies the prototype of that object. Its optional second argument specifies descriptors for the properties of that object. In the next example, we create the same object as in the previous example.

const legoBrick2 = Object.create(
  Object.prototype,
  {
    kind: {
      value: 'Plate 1x3',
      writable: true,
      enumerable: true,
      configurable: true,
    },
    color: {
      value: 'yellow',
      writable: true,
      enumerable: true,
      configurable: true,
    },
    description: {
      get: function () {
        return `${this.kind} (${this.color})`;
      },
      enumerable: true,
      configurable: true,
    },
  });

// Did we really create the same object?
assert.deepEqual(legoBrick1, legoBrick2); // Yes!

9.6 Use cases for Object.getOwnPropertyDescriptors()

Object.getOwnPropertyDescriptors() helps us with two use cases, if we combine it with Object.defineProperties() or Object.create().

9.6.1 Use case: copying properties into an object

Since ES6, JavaScript already has had a tool method for copying properties: Object.assign(). However, this method uses simple get and set operations to copy a property whose key is key:

target[key] = source[key];

That means that it only creates a faithful copy of a property if:

The following example illustrates this limitation. Object source has a setter whose key is data.

const source = {
  set data(value) {
    this._data = value;
  }
};

// Property `data` exists because there is only a setter
// but has the value `undefined`.
assert.equal('data' in source, true);
assert.equal(source.data, undefined);

If we use Object.assign() to copy property data, then the accessor property data is converted to a data property:

const target1 = {};
Object.assign(target1, source);

assert.deepEqual(
  Object.getOwnPropertyDescriptor(target1, 'data'),
  {
    value: undefined,
    writable: true,
    enumerable: true,
    configurable: true,
  });

// For comparison, the original:
const desc = Object.getOwnPropertyDescriptor.bind(Object);
assert.deepEqual(
  Object.getOwnPropertyDescriptor(source, 'data'),
  {
    get: undefined,
    set: desc(source, 'data').set,
    enumerable: true,
    configurable: true,
  });

Fortunately, using Object.getOwnPropertyDescriptors() together with Object.defineProperties() does faithfully copy the property data:

const target2 = {};
Object.defineProperties(
  target2, Object.getOwnPropertyDescriptors(source));

assert.deepEqual(
  Object.getOwnPropertyDescriptor(target2, 'data'),
  {
    get: undefined,
    set: desc(source, 'data').set,
    enumerable: true,
    configurable: true,
  });
9.6.1.1 Pitfall: copying methods that use super

A method that uses super is firmly connected with its home object (the object it is stored in). There is currently no way to copy or move such a method to a different object.

9.6.2 Use case for Object.getOwnPropertyDescriptors(): cloning objects

Shallow cloning is similar to copying properties, which is why Object.getOwnPropertyDescriptors() is a good choice here, too.

To create the clone, we use Object.create():

const original = {
  set data(value) {
    this._data = value;
  }
};

const clone = Object.create(
  Object.getPrototypeOf(original),
  Object.getOwnPropertyDescriptors(original));

assert.deepEqual(original, clone);

For more information on this topic, see §6 “Copying objects and Arrays”.

9.7 Omitting descriptor properties

All properties of descriptors are optional. What happens when you omit a property depends on the operation.

9.7.1 Omitting descriptor properties when creating properties

When we create a new property via a descriptor, then omitting attributes means that their default values are used:

const car = {};
Object.defineProperty(
  car, 'color', {
    value: 'red',
  });
assert.deepEqual(
  Object.getOwnPropertyDescriptor(car, 'color'),
  {
    value: 'red',
    writable: false,
    enumerable: false,
    configurable: false,
  });

9.7.2 Omitting descriptor properties when changing properties

If instead, we change an existing property, then omitting descriptor properties means that the corresponding attributes are not touched:

const car = {
  color: 'yellow',
};
assert.deepEqual(
  Object.getOwnPropertyDescriptor(car, 'color'),
  {
    value: 'yellow',
    writable: true,
    enumerable: true,
    configurable: true,
  });
Object.defineProperty(
  car, 'color', {
    value: 'pink',
  });
assert.deepEqual(
  Object.getOwnPropertyDescriptor(car, 'color'),
  {
    value: 'pink',
    writable: true,
    enumerable: true,
    configurable: true,
  });

9.8 What property attributes do built-in constructs use?

The general rule (with few exceptions) for property attributes is:

9.8.1 Own properties created via assignment

const obj = {};
obj.prop = 3;

assert.deepEqual(
  Object.getOwnPropertyDescriptors(obj),
  {
    prop: {
      value: 3,
      writable: true,
      enumerable: true,
      configurable: true,
    }
  });

9.8.2 Own properties created via object literals

const obj = { prop: 'yes' };

assert.deepEqual(
  Object.getOwnPropertyDescriptors(obj),
  {
    prop: {
      value: 'yes',
      writable: true,
      enumerable: true,
      configurable: true
    }
  });

9.8.3 The own property .length of Arrays

The own property .length of Arrays is non-enumerable, so that it isn’t copied by Object.assign(), spreading, and similar operations. It is also non-configurable:

> Object.getOwnPropertyDescriptor([], 'length')
{ value: 0, writable: true, enumerable: false, configurable: false }
> Object.getOwnPropertyDescriptor('abc', 'length')
{ value: 3, writable: false, enumerable: false, configurable: false }

.length is a special data property, in that it is influenced by (and influences) other own properties (specifically, index properties).

9.8.4 Prototype properties of built-in classes

assert.deepEqual(
  Object.getOwnPropertyDescriptor(Array.prototype, 'map'),
  {
    value: Array.prototype.map,
    writable: true,
    enumerable: false,
    configurable: true
  });

9.8.5 Prototype properties and instance properties of user-defined classes

class DataContainer {
  accessCount = 0;
  constructor(data) {
    this.data = data;
  }
  getData() {
    this.accessCount++;
    return this.data;
  }
}
assert.deepEqual(
  Object.getOwnPropertyDescriptors(DataContainer.prototype),
  {
    constructor: {
      value: DataContainer,
      writable: true,
      enumerable: false,
      configurable: true,
    },
    getData: {
      value: DataContainer.prototype.getData,
      writable: true,
      enumerable: false,
      configurable: true,
    }
  });

Note that all own properties of instances of DataContainer are writable, enumerable, and configurable:

const dc = new DataContainer('abc')
assert.deepEqual(
  Object.getOwnPropertyDescriptors(dc),
  {
    accessCount: {
      value: 0,
      writable: true,
      enumerable: true,
      configurable: true,
    },
    data: {
      value: 'abc',
      writable: true,
      enumerable: true,
      configurable: true,
    }
  });

9.9 API: property descriptors

The following tool methods use property descriptors:

9.10 Further reading

The next three chapters provide more details on property attributes: