Saturday 22 December 2012

safeGet, safeSet and nameSpace in JavaScript

Some weeks ago I wrote a post praising Groovy's safe dereferencing operator (?.) and showed some code to achieve similar functionality in C#. Sometime ago I also mentioned a very cool application of C#'s dynamic, the Elastic Object. Elastic Object shows how powerful it is to be able to hook our code in the method or property look up process. All this power is brought to us in C#'s dynamic via TryGetMember, TryInvokeMember, or in Groovy by invokeMethod and getProperty. Related to this (it's a subset indeed) having a hook for when a method or property are missing is also pretty useful stuff, and is something that we can achieve in Groovy with MethodMissing and PropertyMissing.

With JavaScript being my other all times favourite language, the obvious question that springs up is:
can I do something similar in JavaScript?

Long in short, the answer is No. Unfortunately JavaScript (at least EcmaScript 5) does not boast something that powerful as the aforementioned invokeMethod, TryInvokeMember... I think the proxies to be introduced in ES6 are a step on that direction, though I'm not sure how far (honestly, I haven't had time to dig deep into them).
Right now you can get part of the functionality, but not all of it:

  • As explained in this previous article we can leverage JavaScript accessor properties, functions as objects, closures and so on to intercept the access to any existing property or invokation of any existing method.
  • Firefox provides the non standard __noSuchMethod__ hook to trap invokations of non existing methods

So, this leaves us with some missing pieces:
intercepting access to missing properties, intercepting invokations of missing methods (except in Firefox)...
One additional miss is that though we could use something like my intercept.js library to add interceptors to all the existing methods and properties in an object in a given moment, if later on the object is expanded with new methods/properties, those ones will be lacking the interceptor, and there's no way to be notified when an object is expanded (we just can seal it to prevent it from being expanded...

Well, after all this digression, back to what prompted this post:

As for Groovy's Safe Dereferencing operator, I've written a safeGet function that receives an Object and a string with a list of properties (yes, it's a way of chaining several property safe accesses). If we can reach the last property in the chain, it's returned, otherwise we get undefined.

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"));

Regarding the "Elastic Object", I've created a safeSet function, which likewise, receives an object, a string with a list of properties, and a value, and sets the last property to that value. For any missing property in the chain, the function creates an empty Object, where the following property will get added...

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);

I've used both functions to write my own nameSpace function (bear in mind that this is mainly useful for the browser, in environments implementing the CommonJS module system (like node), namespaces don't seem much necessary).

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 here and test it either with a modern browser here or with node here

Groovy's safe reference (or safe navigation) also works with method invocation, returning null if the method does not exist, it makes sense to me, so I've added a safeInvoke function that in line with my other functions will return undefined if such method does not exist.
I've also decided to upload it to my GitHub account.

1 comment:

  1. The site is really beneficial for everyone to know about this topic. I think if you read blog than you will get some more information from blog. This is really useful blog.How to get rid of Discord Javascript Error | newscutzy.com

    ReplyDelete