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:

Internal slots, which are storage locations that are not accessible from JavaScript, only from operations in the specification.

A collection of properties. Each property associates a key with attributes (think fields in a record).

9.1.1 Internal slots

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

Internal slots correspond to internal state that is associated with objects and used by various ECMAScript specification algorithms.

Internal slots are not object properties and they are not inherited.

Depending upon the specific internal slot specification, such state may consist of: values of any ECMAScript language type or of specific ECMAScript specification type values.

Unless explicitly specified otherwise, internal slots are allocated as part of the process of creating an object and may not be dynamically added to an object.

Unless specified otherwise, the initial value of an internal slot is the value undefined .

. Various algorithms within this specification create objects that have internal slots. However, the ECMAScript language provides no direct way to associate internal slots with an object .

. Internal methods and internal slots are identified within this specification using names enclosed in double square brackets [[ ]] .

There are two kinds of internal slots:

Method slots for manipulating objects (getting properties, setting properties, etc.)

Data slots that store values.

Ordinary objects have the following data slots:

.[[Prototype]]: null | object Stores the prototype of an object. Can be accessed indirectly via Object.getPrototypeOf() and Object.setPrototypeOf() .

.[[Extensible]]: boolean Indicates if it is possible to add properties to an object. Can be set to false via Object.preventExtensions() .

.[[PrivateFieldValues]]: EntryList Is used to manage private class fields.



9.1.2 Property keys

The key of a property is either:

A string

A symbol

9.1.3 Property attributes

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

A data property stores data. Its attribute value holds any JavaScript value.

holds any JavaScript value. An accessor property consists of a getter function and/or a setter function. The former is stored in the attribute get , the latter in the attribute set .

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:

writable determines if the value of a data property can be changed.

determines if the value of a data property can be changed. configurable determines if the attributes of a property can be changed. If it is false , then: We cannot delete the property. We cannot change a property from a data property to an accessor property or vice versa. We cannot change any attribute other than value . However, one more attribute change is allowed: We can change writable from true to false . The rationale behind this anomaly is historical: Property .length of Arrays has always been writable and non-configurable. Allowing its writable attribute to be changed enables us to freeze Arrays.

determines if the attributes of a property can be changed. If it is , then: enumerable influences some operations (such as Object.keys() ). If it is false , then those operations ignore the property. Most properties are enumerable (e.g. those created via assignment or object literals), which is why you’ll rarely notice this attribute in practice. If you are still interested in how it works, see §12 “Enumerability of properties”.

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 = { proto prop : 1 , } ; // Make proto.prop non-writable: Object . defineProperty ( , 'prop' , { writable : false }) ; proto}) const obj = Object . create (proto) ; obj(proto) . throws ( assert => obj . prop = 2 , ()obj / ^ 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 { ?: any ; value ?: boolean ; writable ?: boolean ; configurable ?: boolean ; enumerable } interface AccessorPropertyDescriptor { ?: (this : any ) => any ; get(thisany ?: (this : any , v : any ) => void ; set(thisvoid ?: boolean ; configurable ?: boolean ; enumerable } type PropertyDescriptor = DataPropertyDescriptor | AccessorPropertyDescriptor ; PropertyDescriptorDataPropertyDescriptorAccessorPropertyDescriptor

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 = { legoBrick kind : 'Plate 1x3' , color : 'yellow' , get description() { return ` ${ this . kind } ( ${ this . color } )` ; } , } ;

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

. deepEqual ( assert Object . getOwnPropertyDescriptor (legoBrick , 'color' ) , (legoBrick { 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 ) ; desc . deepEqual ( assert Object . getOwnPropertyDescriptor (legoBrick , 'description' ) , (legoBrick { get : desc(legoBrick , 'description' ) . get , // (A) desc(legoBrick 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 = { legoBrick kind : 'Plate 1x3' , color : 'yellow' , get description() { return ` ${ this . kind } ( ${ this . color } )` ; } , } ; const desc = Object . getOwnPropertyDescriptor . bind ( Object ) ; desc . deepEqual ( assert Object . getOwnPropertyDescriptors (legoBrick) , (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) desc(legoBrick 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:

If there is no property with key k , a new own property is created that has the attributes specified by propDesc .

, a new own property is created that has the attributes specified by . If there is a property with key k , defining changes the property’s attributes so that they match propDesc .

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

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

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

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

const car = { car color : 'blue' , } ; let readCount = 0 ; readCount Object . defineProperty (car , 'color' , { (car get() { ++; readCount return 'red' ; } , ; }) . equal (car . color , 'red' ) ; assert(car . equal (readCount , 1 ) ; assert(readCount

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

const car = { car color : 'blue' , } ; // Use the same attributes as assignment: Object . defineProperty ( , 'color' , { car value : 'green' , writable : true , enumerable : true , configurable : true , ; }) . deepEqual ( assert , 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 = {} ; 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 , } , ; }) . deepEqual ( assert , 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 ( legoBrick2 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? . deepEqual (legoBrick1 , legoBrick2) ; // Yes! assert(legoBrick1legoBrick2)

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 :

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

Its attribute writable is true and its attribute enumerable is true (because that’s how assignment creates properties).

is and its attribute is (because that’s how assignment creates properties). It is a data property.

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

const source = { source set data(value) { this . _data = value ; value } } ; // Property `data` exists because there is only a setter // but has the value `undefined`. . equal ( 'data' in source , true ) ; assertsource . equal (source . data , undefined ) ; assert(source

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

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

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

const target2 = {} ; target2{} Object . defineProperties ( , Object . getOwnPropertyDescriptors (source)) ; target2(source)) . deepEqual ( assert Object . getOwnPropertyDescriptor (target2 , 'data' ) , (target2 { get : undefined , set : desc(source , 'data' ) . set , desc(source 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 = { original set data(value) { this . _data = value ; value } } ; const clone = Object . create ( clone Object . getPrototypeOf (original) , (original) Object . getOwnPropertyDescriptors (original)) ; (original)) . deepEqual (original , clone) ; assert(originalclone)

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 = {} ; car{} Object . defineProperty ( , 'color' , { car value : 'red' , ; }) . deepEqual ( assert Object . getOwnPropertyDescriptor (car , 'color' ) , (car { 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 = { car color : 'yellow' , } ; . deepEqual ( assert Object . getOwnPropertyDescriptor (car , 'color' ) , (car { value : 'yellow' , writable : true , enumerable : true , configurable : true , ; }) Object . defineProperty ( , 'color' , { car value : 'pink' , ; }) . deepEqual ( assert Object . getOwnPropertyDescriptor (car , 'color' ) , (car { 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:

Properties of objects at the beginning of a prototype chain are usually writable, enumerable, and configurable.

As described in the chapter on enumerability, most inherited properties are non-enumerable, to hide them from legacy constructs such as for-in loops. Inherited properties are usually writable and configurable.

9.8.1 Own properties created via assignment

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

9.8.2 Own properties created via object literals

const obj = { prop : 'yes' } ; obj . deepEqual ( assert Object . getOwnPropertyDescriptors (obj) , (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

. deepEqual ( assert 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 { DataContainer { = 0 ; accessCount constructor(data) { this . data = data ; data } getData() { this . accessCount ++; return this . data ; } } . deepEqual ( assert Object . getOwnPropertyDescriptors (DataContainer . prototype ) , (DataContainer { constructor : { value : DataContainer , DataContainer writable : true , enumerable : false , configurable : true , } , getData : { value : DataContainer . prototype . getData , DataContainer 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' ) dcDataContainer( . deepEqual ( assert Object . getOwnPropertyDescriptors (dc) , (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:

Object.defineProperty(obj: object, key: string|symbol, propDesc: PropertyDescriptor): object [ES5] Creates or changes a property on obj whose key is key and whose attributes are specified via propDesc . Returns the modified object. const obj = {} ; obj{} const result = Object . defineProperty ( result , 'happy' , { obj value : 'yes' , writable : true , enumerable : true , configurable : true , ; }) // obj was returned and modified: . equal (result , obj) ; assert(resultobj) . deepEqual (obj , { assert(obj happy : 'yes' , ; })

Object.defineProperties(obj: object, properties: {[k: string|symbol]: PropertyDescriptor}): object [ES5] The batch version of Object.defineProperty() . Each property p of the object properties specifies one property that is to be added to obj : The key of p specifies the key of the property, the value of p is a descriptor that specifies the attributes of the property. const address1 = Object . defineProperties ({} , { address1({} street : { value : 'Evergreen Terrace' , enumerable : true } , number : { value : 742 , enumerable : true } , ; })

Object.create(proto: null|object, properties?: {[k: string|symbol]: PropertyDescriptor}): object [ES5] First, creates an object whose prototype is proto . Then, if the optional parameter properties has been provided, adds properties to it – in the same manner as Object.defineProperties() . Finally, returns the result. For example, the following code snippet produces the same result as the previous snippet: const address2 = Object . create ( Object . prototype , { address2 street : { value : 'Evergreen Terrace' , enumerable : true } , number : { value : 742 , enumerable : true } , ; }) . deepEqual (address1 , address2) ; assert(address1address2)

Object.getOwnPropertyDescriptor(obj: object, key: string|symbol): undefined|PropertyDescriptor [ES5] Returns the descriptor of the own (non-inherited) property of obj whose key is key . If there is no such property, undefined is returned. . deepEqual ( assert Object . getOwnPropertyDescriptor ( Object . prototype , 'toString' ) , { value : {} . toString , {} writable : true , enumerable : false , configurable : true , ; }) . equal ( assert Object . getOwnPropertyDescriptor ({} , 'toString' ) , ({} undefined ) ;

Object.getOwnPropertyDescriptors(obj: object): {[k: string|symbol]: PropertyDescriptor} [ES2017] Returns an object where each property key 'k' of obj is mapped to the property descriptor for obj.k . The result can be used as input for Object.defineProperties() and Object.create() . const propertyKey = Symbol ( 'propertyKey' ) ; propertyKey const obj = { obj : 'abc' , [propertyKey] return 123 } , get count() { } ; const desc = Object . getOwnPropertyDescriptor . bind ( Object ) ; desc . deepEqual ( assert Object . getOwnPropertyDescriptors (obj) , (obj) { : { [propertyKey] value : 'abc' , writable : true , enumerable : true , configurable : true } , count : { get : desc(obj , 'count' ) . get , // (A) desc(obj set : undefined , enumerable : true , configurable : true } ; }) Using desc() in line A is a work-around so that .deepEqual() works.

9.10 Further reading

The next three chapters provide more details on property attributes: