Object.create()
: Creating objects via descriptorsObject.getOwnPropertyDescriptors()
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!
In the ECMAScript specification, an object consists of:
The specification describes internal slots as follows. I added bullet points and emphasized one part:
undefined
.[[ ]]
.There are two kinds of internal slots:
Ordinary objects have the following data slots:
.[[Prototype]]: null | object
Object.getPrototypeOf()
and Object.setPrototypeOf()
..[[Extensible]]: boolean
false
via Object.preventExtensions()
..[[PrivateFieldValues]]: EntryList
The key of a property is either:
There are two kinds of properties and they are characterized by their attributes:
value
holds any JavaScript value.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.configurable
determines if the attributes of a property can be changed. If it is false
, then:
value
.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.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”.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”.
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.
Object.getOwnPropertyDescriptor()
: retrieving a descriptor for a single propertyConsider 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.
Object.getOwnPropertyDescriptors()
: retrieving descriptors for all properties of an objectconst 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.
If we define a property with the key k
via a property descriptor propDesc
, then what happens depends:
k
, a new own property is created that has the attributes specified by propDesc
.k
, defining changes the property’s attributes so that they match propDesc
.Object.defineProperty()
: defining single properties via descriptorsFirst, 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.
Object.defineProperties()
: defining multiple properties via descriptorsObject.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})`;
},
});
Object.create()
: Creating objects via descriptorsObject.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!
Object.getOwnPropertyDescriptors()
Object.getOwnPropertyDescriptors()
helps us with two use cases, if we combine it with Object.defineProperties()
or Object.create()
.
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:
writable
is true
and its attribute enumerable
is true
(because that’s how assignment creates properties).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,
});
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.
Object.getOwnPropertyDescriptors()
: cloning objectsShallow 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”.
All properties of descriptors are optional. What happens when you omit a property depends on the operation.
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,
});
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,
});
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.
const obj = {};
obj.prop = 3;
assert.deepEqual(
Object.getOwnPropertyDescriptors(obj),
{
prop: {
value: 3,
writable: true,
enumerable: true,
configurable: true,
}
});
const obj = { prop: 'yes' };
assert.deepEqual(
Object.getOwnPropertyDescriptors(obj),
{
prop: {
value: 'yes',
writable: true,
enumerable: true,
configurable: true
}
});
.length
of ArraysThe 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).
assert.deepEqual(
Object.getOwnPropertyDescriptor(Array.prototype, 'map'),
{
value: Array.prototype.map,
writable: true,
enumerable: false,
configurable: true
});
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,
}
});
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.
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.
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:
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.
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');
const obj = {
[propertyKey]: 'abc',
get count() { return 123 },
};
const desc = Object.getOwnPropertyDescriptor.bind(Object);
assert.deepEqual(
Object.getOwnPropertyDescriptors(obj),
{
[propertyKey]: {
value: 'abc',
writable: true,
enumerable: true,
configurable: true
},
count: {
get: desc(obj, 'count').get, // (A)
set: undefined,
enumerable: true,
configurable: true
}
});
Using desc()
in line A is a work-around so that .deepEqual()
works.
The next three chapters provide more details on property attributes: