Blog-Archiv

Sonntag, 22. Oktober 2017

Unsafe JS Namespaces

Namespaces are scopes in which names are unique. They help to organize source code into modules, packages, components, whatever. Examples for a Java namespaces are package names, like java.lang or java.util. In java.lang there can be just one class Runtime, while there could be a class of same name in another package like my.applications.namespace.

The JavaScript (JS) language does not have namespaces, thus developers handicrafted something similar. This Blog is about the JS namespace replacement, and its potential gotchas.

Nested Objects as Namespaces

Instead of namespaces, JS developers use nested objects. Everything inside a JS object is public. That means there is no encapsulation, but the possibility to have identical function- or variable-names in different contexts.

var namespace = namespace || {};
namespace.demo = namespace.demo || {};
namespace.demo.lifeModule = namespace.demo.lifeModule || {};

namespace.demo.lifeModule.love = function() {
  alert("Love is an extension of life :-)");
};

This is a typical JS namespace initialization sequence. It must be built level by level. The first line uses the var keyword, it makes sure that a variable namespace (first level) is defined in the global variable context. If it is not yet existing (going into the "||" part), a new object is created onto that name.

Then the nested object demo is set into it, representing the next namespace identifier (second level). Again a check makes sure that the space is not overwritten when already existing. The second line doesn't need the leading var keyword, because it is building into an already existing parent-object.

This code silently assumes that the variable "namespace", in case it already existed, was an object (JS functions are also objects). Should it have been an integer or a string, this instruction will fail silently, and potential following ones may display a console error.

That kind of namespace building can continue to any level. A popular concept is having

application.page.module

So above example would refer to an application named "namespace", having a page "demo", and a module "lifeModule" in that page. Inside the module is a public function called "love".

Example

Here is an example that loads two JS files, both defining the same namespace.

On the following panel you can try out what this example does.

HTML source is:

<!DOCTYPE HTML>
<html>
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1"/>
    <title>JS Namespaces</title>
  
    <script type="text/javascript" src="namespace_life_love.js">
    </script>
  
    <script type="text/javascript" src="namespace_life.js">
    </script>
  
  </head>
       
  <body>
  
    <div style="text-align: center;">
      <button onclick="namespace.demo.lifeModule.birth();">Birth</button>
      <button onclick="namespace.demo.lifeModule.love();">Love</button>
      <button onclick="namespace.demo.lifeModule.death();">Death</button>
    </div>
    
  </body>
</html>

The button "Love" has its callback in JS file

namespace_life_love.js
var namespace = namespace || {};
namespace.demo = namespace.demo || {};
namespace.demo.lifeModule = namespace.demo.lifeModule || {};

namespace.demo.lifeModule.love = function() {
  alert("Love is an extension of life :-)");
};

The buttons "Birth" and "Death" have their callbacks in a revealing module singleton in JS file

namespace_life.js
var namespace = namespace || {};
namespace.demo = namespace.demo || {};

namespace.demo.lifeModule = (function()
{
  var born;
  var dead;
  
  var birth = function() {
    if ( born )
      return error("Can not be born again when not yet dead!");
    
    born = true;
    dead = false;
    alert("Getting born ...");
  };
  
  var death = function() {
    if ( dead )
      return error("Can not die when already dead!");
    
    alert("Dying ...");
    dead = true;
    born = false;
  };
  
  var error = function(message) {
    alert(message);
  };
  
  
  var returnObject = namespace.demo.lifeModule || {};
  
  returnObject.birth = birth;
  returnObject.death = death;
  
  return returnObject;

})();

This is a "stateful" module, that means is maintains a state in its private variables born and dead. It exports its public functions in a return statement after having built its internal state.

Mind that again, like in the top-lines, it checks whether the namespace object already exists, and merges into the existing one when so. For that check, you have to repeat the namespace at this unusual place on bottom of the module!

Now, why are such namespaces unsafe?

The Gotcha

Normally a revealing module exports its public functions in this way:

var namespace = namespace || {};
namespace.demo = namespace.demo || {};

namespace.demo.lifeModule = (function()
{
  ....
  
  return {
    birth: birth,
    death: death
  };

})();

This overwrites any existing namespace-object with its return. It does not merge into the existing one. Now it depends on the load-order of the script files in HTML. In the example above it would go wrong, because namespace_life.js is loaded after namespace_life_love.js!

Another hazard is this initialization variant:

var namespace = namespace || {};
namespace.demo = namespace.demo || {};

namespace.demo.lifeModule = namespace.demo.lifeModule || (function()
{
  ....
  
  return {
    birth: birth,
    death: death
  };

})();

This tries to work around redundant initialization by testing for existence in the first line of the module definition. What happens is that the namespace_life_love.js object is already there, and the namespace_life.js functions would not be built at all!

Summary

Although a safe variant of namespace-initialization exists also for revealing modules, you are forced to repeat the namespace on bottom of the module definition. Nobody would expect it there.

Whatever you do in JS, it is unsafe. The language missed some fundamental programming language features like dependency management, typing and namespaces. In ES-6, the follow-up language of JS, namespaces and dependency management will be covered by a completely new module-concept.




Keine Kommentare: