Object properties in JavaScript
Properties determine the state of an object in JavaScript. This blog post examines in detail how they work.
Kinds of properties
JavaScript has three different kinds of properties: named data properties, named accessor properties and internal properties.
Named data properties (“properties”)
“Normal” properties of objects map string names to values. For example, the following object obj has a data property whose name is the string "prop" and whose value is the number 123.
var obj = {
prop: 123
};
You can get (read) a property:
console.log(obj.prop); // 123
console.log(obj["prop"]); // 123
And you can set (write) a property:
obj.prop = "abc";
obj["prop"] = "abc";
Named accessor properties
Alternatively, getting and setting a property value can be handled via functions. Those functions are called accessor functions. A function that handles getting is called a getter. A function that handles setting is called a setter.
var obj = {
get prop() {
return "Getter";
},
set prop(value) {
console.log("Setter: "+value);
}
}
Let’s interact with obj:
> obj.prop
'Getter'
> obj.prop = 123;
Setter: 123
Internal properties
Some properties are only used by the specification. They are called “internal”, because they are not directly accessible via the language, but they do influence its behavior. Internal properties have special names that are written in double square brackets. Two examples:
- The internal property [[Prototype]] points to the prototype of an object. It can be read via Object.getPrototypeOf(). Its value can only be set by creating a new object that has a given prototype, e.g. via Object.create() or __proto__ [1].
- The internal property [[Extensible]] determines whether or not one can add properties to an object. It can be read via Object.isExtensible(). It can be set false via Object.preventExtensions(). Once false, it cannot be become true again.
Property attributes
All of the state of a property, both its data and its meta-data, is stored in attributes. They are fields that a property has, much like an object has properties. Attribute keys are often written in double brackets.
The following attributes are specific to named data properties:
- [[Value]] hold the property’s value, its data.
- [[Writable]] holds a boolean indicating whether the value of a property can be changed.
The following attributes are specific to named accessor properties:
- [[Get]] holds the getter, a function that is called when a property is read. That function computes the result of the read access.
- [[Set]] holds the setter, a function that is called when a property is set to a value. The function receives that value as a parameter.
All properties have the following attributes:
- [[Enumerable]] holds a boolean. Making a property non-enumerable hides it from some operations (see below).
- [[Configurable]] holds a boolean. If false, you cannot delete a property, change any of its attributes (except [[Value]]) or convert between data property and accessor property.
In other words, [[Configurable]] controls the writability of a property’s meta-data.
Default values
If you don’t specify attributes, the following defaults are used:
Attribute key | Default value |
[[Value]] | undefined |
[[Get]] | undefined |
[[Set]] | undefined |
[[Writable]] | false |
[[Enumerable]] | false |
[[Configurable]] | false |
These defaults are especially important for property descriptors (see below).
Property descriptors
A property descriptor encodes the attributes of a property as an object. Each of the properties of that object corresponds to an attribute. For example, the following is the descriptor of a read-only property whose value is 123:
{
value: 123,
writable: false,
enumerable: true,
configurable: false
}
You can achieve the same goal, immutability, via accessors. Then the descriptor looks as follows:
{
get: function () { return 123 },
enumerable: true,
configurable: false
}
Functions that use property descriptors
The following functions allow you to work with property descriptors:
- Object.defineProperty(obj, propName, propDesc)
Create or change a property on obj whose name is propName and whose attributes are specified via propDesc. Return the modified object. Example:
var obj = Object.defineProperty({}, "foo", {
value: 123,
enumerable: true
// writable and configurable via defaults
});
- Object.defineProperties(obj, propDescObj)
The batch version of Object.defineProperty(). Each property of propDescObj holds a property descriptor. The names of the properties and their values tell Object.defineProperties what properties to create or change on obj.
Example:
var obj = Object.defineProperties({}, {
foo: { value: 123, enumerable: true },
bar: { value: "abc", enumerable: true }
});
- Object.create(proto, propDescObj?)
First, create an object whose prototype is proto. Then, if the optional parameter propDescObj has been specified, add properties to it – in the same manner as Object.defineProperties. Finally, return the result.
For example, the following code snippet produces the same result as the previous snippet:
var obj = Object.create(Object.prototype, {
foo: { value: 123, enumerable: true },
bar: { value: "abc", enumerable: true }
});
- Object.getOwnPropertyDescriptor(obj, propName)
Returns the descriptor of the own (non-inherited) property of obj whose name is propName. If there is no such property, undefined is returned.
> Object.getOwnPropertyDescriptor(Object.prototype, "toString")
{ value: [Function: toString],
writable: true,
enumerable: false,
configurable: true }
> Object.getOwnPropertyDescriptor({}, "toString")
undefined
Enumerability
This section explains which operations are influenced by enumerability and which aren’t. Below, we are assuming that the following definitions have been made:
var proto = Object.defineProperties({}, {
foo: { value: 1, enumerable: true },
bar: { value: 2, enumerable: false }
});
var obj = Object.create(proto, {
baz: { value: 1, enumerable: true },
qux: { value: 2, enumerable: false }
});
Note that objects (including proto) normally have at least the prototype Object.prototype [2]:
> Object.getPrototypeOf({}) === Object.prototype
true
Object.prototype is where standard methods such as toString and hasOwnProperty are defined.
Operations affected by enumerability
Enumerability only affects two operations: The for-in loop and Object.keys().
The for-in loop iterates over the names of all enumerable properties, including inherited ones (note that none of the non-enumerable properties of Object.prototype show up):
> for (var x in obj) console.log(x);
baz
foo
Object.keys() returns the names of all own (non-inherited) enumerable properties:
> Object.keys(obj)
[ 'baz' ]
If you want the names of all own properties, you need to use Object.getOwnPropertyNames() (see example below).
Operations that ignore enumerability
All other operations ignore enumerability.
Some read operations take inheritance into consideration:
> "toString" in obj
true
> obj.toString
[Function: toString]
Other read operations only work with own properties:
> Object.getOwnPropertyNames(obj)
[ 'baz', 'qux' ]
> obj.hasOwnProperty("qux")
true
> obj.hasOwnProperty("toString")
false
> Object.getOwnPropertyDescriptor(obj, "qux")
{ value: 2,
writable: false,
enumerable: false,
configurable: false }
> Object.getOwnPropertyDescriptor(obj, "toString")
undefined
Creating, deleting and defining properties only affects the first object in a prototype chain:
obj.propName = value
obj["propName"] = value
delete obj.propName
delete obj["propName"]
Object.defineProperty(obj, propName, desc)
Object.defineProperties(obj, descObj)
Best practices
The general rule is that properties created by the system are non-enumerable, while properties created by
users are enumerable:
> Object.keys([])
[]
> Object.getOwnPropertyNames([])
[ 'length' ]
> Object.keys(['a'])
[ '0' ]
That especially holds for the methods in prototype objects:
> Object.keys(Object.prototype)
[]
> Object.getOwnPropertyNames(Object.prototype)
[ hasOwnProperty',
'valueOf',
'constructor',
'toLocaleString',
'isPrototypeOf',
'propertyIsEnumerable',
'toString' ]
Thus, for your code, you should ignore enumerability. You normally shouldn’t add properties to built-in prototypes and objects, but if you do, you should make them non-enumerable to avoid breaking code.
As we have seen, non-enumerability mostly benefits for-in and ensures that legacy code using it won’t break. The non-enumerable properties create the illusion that for-in only iterates over the user-created own properties of an object. In your code, you should avoid for-in if you can [3].
If you use objects as maps from strings to values, you should only work with own properties and ignore enumerability. But there are more pitfalls for this use case [4].
Conclusion
In this post, we have examined the nature of properties, which is defined via so-called attributes. Note that actual JavaScript implementations do not necessarily organize properties via attributes, they are mainly an abstraction used by the ECMAScript specification. But one that is sometimes visible in the language itself, for example in property descriptors.
Further reading on 2ality:
- Read “JavaScript properties: inheritance and enumerability” for more information on how inheritance and enumerability affect property-related operations.
- Read “JavaScript inheritance by example” for an introduction to JavaScript inheritance.
References
- JavaScript: __proto__
- What object is not an instance of Object?
- Iterating over arrays and objects in JavaScript
- The pitfalls of using objects as maps in JavaScript
Comments
Post a Comment