Properties (and interception) in ECMAScript 5

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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
//@obj: object being intercepted
//@key: string name of the property being intercepted
//@beforeGet, afterGet; functions with this signature: function(key, safeDesc)
//@beforeSet, afterSet; functions with this signature: function(key, newValue, safeDesc)
//these 4 interception functions expect to be launched with "this" pointing to the object's this
//warning, you can't do a this\[key\] inside those functions, we would get a stack overflow...
//that's the reason why we're passing that safeDesc object
//the beforeGet function could want to modify the returned value, if it returns something, we just return it and skip the normal get and afterGet
//the beforeSet function could want to prevent the set action, if it returns true, it means it want to prevent the assignation
//We're providing the afterGet functionality though indeed I can't think of any useful application for it
function interceptProperty(obj, key, beforeGet, afterGet, beforeSet, afterSet){
 var emptyFunc = function(){};
 beforeGet = beforeGet || emptyFunc;
 afterGet = afterGet || emptyFunc;
 beforeSet = beforeSet || emptyFunc;
 afterSet = afterSet || emptyFunc;
 //skip functions from interception
 if (obj\[key\] === undefined || typeof obj\[key\] == "function"){
  return;
 }
 
 var desc = Object.getOwnPropertyDescriptor(obj, key);
 
 if (desc.get || desc.set){
  console.log("intercepting accessor property: " + key);
  Object.defineProperty(obj, key, {
   get : desc.get ? function(){ 
    //if the beforeGet function returns something, it's that value what we want to return
    //well, in fact we could apply that logic of changing the value to return in the afterGet function better than in the beforeGet
    var result = beforeGet.call(this, key, desc);
    result = result || desc.get.call(this);
    afterGet.call(this, key, desc);
    return result;
   }: undefined,  
   set : desc.set ? function(newValue){ 
    //if the beforeSet function returns something, use it instead of newValue
    newValue = beforeSet.call(this, key, newValue, desc) || newValue;
    desc.set.call(this, newValue);
    afterSet.call(this, key, newValue, desc);
   }: undefined,
     enumerable : true,  
     configurable : true
  });
 }
 else{
  console.log("intercepting data property: " + key);
  var \_value = obj\[key\];
  desc = {
   get : function(){
    return \_value;
   },
   set : function(newValue){
    \_value = newValue;
   }
  };
  Object.defineProperty(obj, key, {
   get : function(){ 
    \_value = beforeGet.call(this, key, desc) || \_value;
    afterGet.call(this, key, desc);
    return \_value;
   },  
   set : function(newValue){ 
    \_value = beforeSet.call(this, key, newValue, desc) || newValue;
    afterSet.call(this, key, newValue, desc);
   },
     enumerable : true,  
     configurable : true
  });
 }
}

To illustrate its usage, let’s consider intercepting a property setter, enabling us to modify values before they are assigned:

1
2
3
4
5
6
7
 var \_beforeSet = function \_beforeSet(key, newValue, safeDesc){
  if(newValue == "julian"){
   console.log("rewriting value to assign");
   return "iyán";
  } 
 };
 interceptProperty(p1, "name", null, null, \_beforeSet);

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.

Licensed under CC BY-NC-SA 4.0
Last updated on Mar 19, 2024 10:31 +0100