In this blog post, 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:
This is how the specification describes internal slots (the emphasis is mine):
undefined
.[[ ]]
.There are two kinds of internal slots:
Internal data slot | Type |
---|---|
[[Prototype]] |
null ¦ object |
[[Extensible]] |
boolean |
[[PrivateFieldValues]] |
List of entries |
Descriptions for these data slots:
[[Prototype]]
stores the prototype of an object.
Object.getPrototypeOf()
and Object.setPrototypeOf()
[[Extensible]]
indicates if it is possible to add properties to an object.
false
via Object.preventExtensions()
.[[PrivateFieldValues]]
is used to manage private class fields.The key of a property is either:
There are two kinds of properties and they have different attributes:
value
holds any JavaScript value.get
, the latter in the attribute set
.The following table lists all property attributes.
Kind of property | Name and type of attribute | Default value |
---|---|---|
Data property | value: any |
undefined |
writable: boolean |
false |
|
Accessor property | get(): any |
undefined |
set(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.assign()
). If it is false
, then those operations ignore the property.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?(): any;
set?(v: any): void;
configurable?: boolean;
enumerable?: boolean;
}
type PropertyDescriptor = DataPropertyDescriptor | AccessorPropertyDescriptor;
The question marks indicate that each property is optional. If you omit a property when passing a descriptor to an operation, then its default value is used.
The following code retrieves the object descriptor for the data property first
:
const obj = {
first: 'Jane',
};
assert.deepEqual(
Object.getOwnPropertyDescriptor(obj, 'first'),
{
value: 'Jane',
writable: true,
enumerable: true,
configurable: true,
});
In the next example, we retrieve the property descriptor for the getter fullName
:
const desc = Object.getOwnPropertyDescriptor.bind(Object);
const jane = {
first: 'Jane',
last: 'Doe',
get fullName() {
return this.first + ' ' + this.last;
},
};
assert.deepEqual(
Object.getOwnPropertyDescriptor(jane, 'fullName'),
{
get: desc(jane, 'fullName').get, // (A)
set: undefined,
enumerable: true,
configurable: true
});
Using desc()
in line A is a work-around so that .deepEqual()
works.
You can also create new properties via property descriptors:
const car = {};
Object.defineProperty(car, 'color', {
value: 'blue',
writable: true,
enumerable: true,
configurable: true,
});
assert.deepEqual(
car,
{
color: 'blue',
});
If an own property already exists, then defining it via a descriptor changes that property. On one hand that allows us to use Object.defineProperty()
like assignment:
const car = {
color: 'blue',
};
Object.defineProperty(car, 'color', {
value: 'green',
writable: true,
enumerable: true,
configurable: true,
});
assert.deepEqual(
car,
{
color: 'green',
});
On the other hand, we can also use Object.defineProperty()
to turn a data property into a getter (and vice versa):
const car = {
color: 'blue',
};
let getterCallCount = 0;
Object.defineProperty(car, 'color', {
get() {
getterCallCount++;
return 'red';
},
});
assert.equal(car.color, 'red');
assert.equal(getterCallCount, 1);
If an inherited property is read-only, then we can’t use assignment to change it. The rationale is that overriding an inherited property by creating an own property can be seen as non-destructively changing the inherited property. Arguably, if a property is non-writable, we shouldn’t be able to do that.
Let’s look at an example:
const proto = Object.defineProperties({}, {
prop: {
value: 1,
writable: false,
}
});
const obj = Object.create(proto);
assert.throws(
() => obj.prop = 2,
/^TypeError: Cannot assign to read only property 'prop'/);
We can’t change the property via assignment. But we can still create an own property by defining it:
Object.defineProperty(obj, 'prop', {value: 2});
assert.equal(obj.prop, 2);
Accessor properties that don’t have a setter are also considered to be read-only:
const proto = Object.defineProperties({}, {
prop: {
get() {
return 1;
}
}
});
const obj = Object.create(proto);
assert.throws(
() => obj.prop = 2,
'TypeError: Cannot set property prop of #<Object> which has only a getter');
The following functions allow you to work with property descriptors:
Object.defineProperty(obj: object, key: string|symbol, propDesc: PropertyDescriptor): object
Creates or changes a property on obj
whose key is key
and whose attributes are specified via propDesc
. Returns the modified object.
const obj = {};
const result = Object.defineProperty(
obj, 'happy', {
value: 'yes',
writable: true,
enumerable: true,
configurable: true,
});
// obj was returned and modified:
assert.equal(result, obj);
assert.deepEqual(obj, {
happy: 'yes',
});
Object.defineProperties(obj: object, properties: {[k: string|symbol]: PropertyDescriptor}): object
The batch version of Object.defineProperty()
. Each property of properties
holds a property descriptor. The keys of the properties and their values tell Object.defineProperties
what properties to create or change on obj
.
const address1 = Object.defineProperties({}, {
street: { value: 'Evergreen Terrace', enumerable: true },
number: { value: 742, enumerable: true },
});
Object.create(proto: null|object, properties?: {[k: string|symbol]: PropertyDescriptor}): object
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, {
street: { value: 'Evergreen Terrace', enumerable: true },
number: { value: 742, enumerable: true },
});
assert.deepEqual(address1, address2);
Object.getOwnPropertyDescriptor(obj: object, key: string|symbol): undefined|PropertyDescriptor
Returns the descriptor of the own (non-inherited) property of obj
whose key is key
. If there is no such property, undefined
is returned.
assert.deepEqual(
Object.getOwnPropertyDescriptor(Object.prototype, 'toString'),
{
value: {}.toString,
writable: true,
enumerable: false,
configurable: true,
});
assert.equal(
Object.getOwnPropertyDescriptor({}, 'toString'),
undefined);
Object.getOwnPropertyDescriptors(obj: object): {[k: string|symbol]: PropertyDescriptor}
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 desc = Object.getOwnPropertyDescriptor.bind(Object);
const propertyKey = Symbol('propertyKey');
const obj = {
[propertyKey]: 'abc',
get count() { return 123 },
};
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.
Object.getOwnPropertyDescriptors()
Object.getOwnPropertyDescriptors()
: 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:
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;
}
};
const desc = Object.getOwnPropertyDescriptor.bind(Object);
assert.deepEqual(
Object.getOwnPropertyDescriptor(source, 'data'),
{
get: undefined,
set: desc(source, 'data').set,
enumerable: true,
configurable: true,
});
// Because there is only a setter, property `data` exists,
// 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,
});
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 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);