I’ve been recently studying the enhancements to Properties introduced in ECMAScript 5. There are numerous resources available on this subject, such as this one by John Resign, and I don’t want to simply reiterate what has already been thoroughly explained elsewhere. For a comprehensive understanding, the MDN documentation on defineProperty, getOwnPropertyDescriptor, and so forth is an indispensable resource.
A key concept to understand is that while JavaScript objects resemble dictionaries, their internal workings are more intricate than just key-value pairs. Even in EcmaScript 3, instead of directly referencing a value, we have a reference to an object. This object not only contains the actual value but also supplementary fields, like [[enumerable]], that function as metadata. EcmaScript 5 grants us read/write access to these metadata fields and introduces getters and setters into the equation. It’s crucial to remember that properties come in two varieties: data properties, which hold a value, and accessor properties, which comprise a getter-setter function pair.
While reviewing the defineProperty
documentation and understanding its capability to redefine existing properties, a practical and evident use case came to mind: property interception. Essentially, we can redefine an existing property, be it a data property or an accessor property, by substituting it with an accessor property. This replacement property would then have a way to interact with the original property. A compelling aspect of this technique is its ability to be implemented directly on the object itself, eliminating the need for a separate proxy object as seen in Java or .Net’s dynamic proxies (or the proposed proxies for ECMAScript 6).
The code snippet provided below outlines closures for the getters and setters. These closures capture the existing property descriptor if it was an accessor property or the value store if it was a data property.
|
|
To illustrate its usage, let’s consider intercepting a property setter, enabling us to modify values before they are assigned:
|
|
We have four interceptor functions: beforeGet, afterGet, beforeSet, and afterSet. While I struggle to find a practical use for afterGet
, I included it for completeness. Both beforeGet
and afterGet
are invoked with the object owning the property set as this
. They receive two arguments: a string representing the key and a descriptor-like object with get
and set
methods. These methods allow access to the property’s stored value, circumventing the standard getter and setter (crucial to avoid a stack overflow from recursive calls). beforeSet
and afterSet
take an additional argument: the new value being set. Returning a value from beforeGet
or beforeSet
signals our intention to override the default behavior. beforeGet
can return a different value to be used, while beforeSet
can return a value to be set instead of the provided one.
The code can be accessed here: here and here.
Update: The initial version, with its beforeGet-afterGet
and beforeSet-afterSet
function pairs, led to a somewhat awkward usage pattern. I’ve since revised it to employ single getInterceptor
and setInterceptor
functions. The updated code is available at now in github and also incorporates method interception.
A crucial aspect to remember about Properties is that, unlike many other ECMAScript 5 additions (like Function.bind
or Array.forEach
), they lack a straightforward way to be emulated in older engines (refer to this es5 shim for a good analysis). Therefore, carefully consider their use in your code.
Another noteworthy point is our inability to append custom attributes to properties. We’re limited to the predefined ones: value
, get
, set
, writable
, enumerable
, and configurable
. Attempting to add a “description” attribute, for instance, won’t result in an error, but it won’t be added either. Furthermore, every call to getOwnPropertyDescriptor
generates a new object representing the descriptor rather than returning the actual descriptor stored within the Object. This suggests that property descriptors aren’t stored as regular objects but as a more optimized structure, necessitating a mapping to a conventional object when accessed. Consequently, the following expression evaluates to false:
Object.getOwnPropertyDescriptor(o, "sayBye") == Object.getOwnPropertyDescriptor(o, "sayBye");
This limitation is unfortunate, as I had envisioned the elegance of using custom property attributes to simulate advanced Object-Oriented enums, building upon the ideas presented in what I wrote some months ago.