A few weeks back, I wrote a post, which discussed Groovy’s safe dereferencing operator (?.) and provided C# code snippets to demonstrate comparable functionality. I’ve also previously highlighted the Elastic Object, a compelling use case for C#’s dynamic keyword. The Elastic Object illustrates the potential of integrating custom logic into the method or property lookup process. C#’s dynamic achieves this through TryGetMember, TryInvokeMember, while Groovy utilizes invokeMethod and getProperty. Similarly, having a mechanism to handle missing methods or properties proves quite valuable. Groovy accomplishes this with MethodMissing and PropertyMissing.
Given that JavaScript is another favorite language of mine, a natural question arises: can we replicate this in JavaScript?
In short, the answer is no. Regrettably, JavaScript (at least EcmaScript 5) lacks the robust capabilities of features like invokeMethod or TryInvokeMember. Although ES6’s introduction of proxies seems like a step in the right direction, I’m uncertain about its full extent (I haven’t had the opportunity to delve deeply into them).
Currently, we can achieve partial functionality:
- As detailed in this previous article, we can utilize JavaScript’s accessor properties, functions as objects, closures, and other mechanisms to intercept access to existing properties or method invocations.
- Firefox offers the non-standard __noSuchMethod__ hook to capture invocations of non-existent methods.
This leaves us with a few gaps: intercepting access to missing properties and handling invocations of missing methods (except in Firefox).
Furthermore, while we could employ something like my intercept.js library to add interceptors to all methods and properties of an object at a specific moment, any subsequent additions of methods or properties to that object won’t inherit those interceptors. There’s no notification mechanism for object expansion, and we can only seal it to prevent modifications.
After that detour, let’s return to the post’s main point:
For Groovy’s Safe Dereferencing operator, I’ve created a safeGet function. It accepts an object and a string representing a chain of properties, enabling safe access to nested properties. If the final property in the chain is reachable, it returns its value; otherwise, it returns undefined.
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
| function safeGet(obj, propertiesStr){
if (!obj || !propertiesStr)
return undefined;
var properties = propertiesStr.split(".");
var curObj = obj;
while(curObj && properties.length){
var curProp = properties.shift();
curObj = curObj\[curProp\];
}
return curObj;
}
var person = {
name: "xuan",
country: {
name: "Asturies",
capital: {
name: "Uviéu",
population: 1100000
}
}
};
/\*
//this code is ugly...
if (person.country && person.country.capital && person.country.capital.name)
console.log(person.country.capital.name);
else
console.log("undefined");
\*/
console.log("the capital of my country is: " + safeGet(person, "country.capital.name"));
console.log("the population of my country is: " + safeGet(person, "country.population"));
|
Inspired by the “Elastic Object,” I’ve also implemented a safeSet function. It takes an object, a property chain string, and a value. This function sets the final property in the chain to the given value, creating empty objects for any missing properties along the way.
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
| function safeSet(obj, propertiesStr, value){
if (!obj || !propertiesStr)
return;
var properties = propertiesStr.split(".");
var curObj = obj;
for (var i=0; i≶properties.length-1; i++){
if (!curObj\[properties\[i\]\]){
curObj\[properties\[i\]\] = {};
}
curObj = curObj\[properties\[i\]\];
}
curObj\[properties\[i\]\] = value;
}
var p = {};
/\*
//this code is ugly...
p.country = p.country || {};
p.country.capital = p.country.capital || {};
p.country.capital.name = "Uviéu";
\*/
safeSet(p, "country.capital.name", "Uviéu");
console.log("the capital of my country is: " + p.country.capital.name);
safeSet(p, "country.capital.name", "Berlín");
console.log("the capital of my country is: " + p.country.capital.name);
|
Using these two functions, I’ve crafted my own nameSpace function. Note that this is primarily beneficial in browser environments. Namespaces seem less crucial in settings with the CommonJS module system (like Node.js).
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
| var nameSpace = (function \_nameSpaceFactory(){
var root;
if (typeof window == "object"){
root = window;
}
//well GLOBAL works only at the module level, and in node with the CommonJS module system, all this namespacing thing is not really necessary
else if (isNode()){
root = GLOBAL;
}
else
console.log("we don't know the global object for this environment, namespace function won't work");
return function \_nameSpace(nameSpaceStr){
if (!safeGet(root, nameSpaceStr))
safeSet(root, nameSpaceStr, {});
};
}());
//creates the namespace
nameSpace("org.deployToNenyures.base");
org.deployToNenyures.base.version = "0.0.1";
console.log(org.deployToNenyures.base.version);
//namespace already exists, so does nothing
nameSpace("org.deployToNenyures.base");
console.log(org.deployToNenyures.base.version);
```**Update, 2013/01/27**
You can find the source code [here](http://www.telecable.es/personales/covam1/deployToNenyures/SourceCode/safeGetSet/safeGetSet.js) and try it out either in a modern browser [here](http://www.telecable.es/personales/covam1/deployToNenyures/SourceCode/safeGetSet/safeGetSetTest.html) or using Node.js [here](http://www.telecable.es/personales/covam1/deployToNenyures/SourceCode/safeGetSet/test1.js)
Groovy's safe reference (or safe navigation) extends to method invocation, returning null if a method is absent. This resonated with me, leading me to introduce a safeInvoke function. Consistent with my other functions, it returns _undefined_ if the method doesn't exist.
Additionally, I've decided to [upload it](https://github.com/XoseLluis/safeNavigation.js) this to my GitHub account.
|