DEV Community

Discussion on: How to avoid namespace pollution in Javascript

 
efpage profile image
Eckehard

Yes, I know. We should use ES-modules instead of scripts. But - they come at a price and I did not find a satisfying solution for me right now. Please let me explain:

First, we should have a look, how modules are organized in other languages like C++ or Delphi. Generally, you need to import any module explicitly into your source. Each module has a header, that defines precisely, what is exported, very similar to what you do in Javascript. Modules often are based on other Modules, so they may export a very large number of symbols that are accessible in your source then.

Generally you try to keep your interfaces narrow, but complex systems may export 10.000 symbols that - in principle - are acessible in your source.

BUT: Behind the scene, we have an intelligent linker that checks, which parts or the source was used in your code and just extracts, what is needed. In large hierarchies you may have hundreds of name clashes, which is absolutely no problem as these languages have a different way to deal with a clash.

In Javascript, such a situation would crash the whole system. So, what are the drawbacks:

a) Import/export is only supported on the top level, you cannot define a more fine grained access control. Compiled languages usually know different levels of visibility (privat, public, published, ...) outside and inside of classes. For me, the benefit though is a bit limited.

b) Putting every used symbol in an import list is kind of boring. Think of a large module with hundreds of exports. This is a job a computer could do far better than me, and I think, we shouldn't do the work for the computers, it should be the other way around.

c) Maybe you are a bit lazy and use the whole export list as import. This will pollute your namespace with lot´s of unused symbols, which is just the topic of this post.

So, I´m just wondering if there are other solutions that have less drawbacks. Maybe we could have some kind of "intelligent loader", that just loads the parts of a module when it´s needed first? Or at least a automatic import list generator? Or there is a different way to organize the code?

In the example above, DML exports lot´s of handy definitions like this:

const PI2 = 2 * Math.PI
const _style = "style"
const _bold = "font-weight: bold;"
const _italic = "font-style: italic;"
const _fs = "font-size: "
const _bigtext = "font-size: 130%;"
const _bg = "background-color: "
const _bgred = "background-color: red;"
const _bgred2 = "background-color: #f50;"
const _bgy = "background-color: #ffc;"
const _bggreen = "background-color: #695;"
const _bgblue = "background-color: blue;"
const _bgorange = "background-color: #fc0;"
const _bgsilver = "background-color: silver;"
const _bgyellow = "background-color: #ffffee;"
const _bgwhite = "background-color: white;"
const _bgblack = "background-color: black;"
const _bgtrans = "background-color: rgba(0,0,0,0.05);"
const _bgwtrans = "background-color: rgba(255,255,255,0.5);"
const _red = "color: red;"
const _blue = "color: blue;"
const _navy = "color: navy;"
const _white = "color: white;"
const _yellow = "color: yellow;"
...
Enter fullscreen mode Exit fullscreen mode

This is just for convenience, but adding an "export" to every definition will blow up the code and does not really solve the namespace issues.

I would really appreciate to find a better solution for this topic. (Please don´t tell me I´m a bad programmer... As I mentioned already: There are different ways to catch a tiger.)

Thread Thread
 
artydev profile image
artydev • Edited

I will never say to anyone he is a bad programmer,
again, I am very happy to have discovered your library :-)

Your problem could be submited to Andrea Giammarchi from WebReflections.
I am pretty sure he could respond about the possibility of a 'lazy loading'

Regards

Thread Thread
 
peerreynders profile image
peerreynders

1) It may help to organize things into fewer values

const _bg = Object.freeze({
  red: 'background-color: red;',
  red2: 'background-color: #f50;',
  y: 'background-color: #ffc;',
  green: 'background-color: #695;',
  blue: 'background-color: blue;',
  orange: 'background-color: #fc0;',
  silver: 'background-color: silver;',
  yellow: 'background-color: #ffffee;',
  white: 'background-color: white;',
  black: 'background-color: black;',
  trans: 'background-color: rgba(0,0,0,0.05);',
  wtrans: 'background-color: rgba(255,255,255,0.5);',
});

const _clr = Object.freeze({
  red: 'color: red;',
  blue: 'color: blue;',
  navy: 'color: navy;',
  white: 'color: white;',
  yellow: 'color: yellow;',
});

export {
  _bg,
  _clr
};
Enter fullscreen mode Exit fullscreen mode

2) These type of values are typically managed by an entirely different tool, e.g. Sass (also gorko).

Thread Thread
 
efpage profile image
Eckehard • Edited

There is a solution do avoid named input (the lazy way), which is not the final word, but maybe an interesting starting point:
In DML.js, we set the export:

export  {h1, h2, ....}
Enter fullscreen mode Exit fullscreen mode

Now we import our library as dml and make it accessible in the global scope

  <script type="module">  
    import * as dml from "../lib/DML.js"

    dml.h1("h1 test");

    // Make all functions in dml global
    (function () {
      for (const [key, value] of Object.entries(dml)) {
        window[key] = value  // Store the references as variables
      }

    })()
    h1("h1 test")
  </script>
Enter fullscreen mode Exit fullscreen mode

I know this is a bad solution for many reasons. But it might be a starting point:

The solution above defines all symbols as global variables, which we wanted to avoid. I just did not find a way to define scoped (e.g. local) variables that way. If we could manage to create scoped variables, this has many advantages:

  • We could use all elements of any library without qualifiers in a local context
  • All definitions are scoped, so leaving the scope will delete the definitions
  • No name clashes, as all definitions are local.
  • We can use dynamic import, which is pretty close to lazy loading in some cases.

So, my question is: Does anybody know a way to create local variables or const dynamically in Javascript?

The final solution could be something like this:

  <script type="module">  
    import * as dml from "../lib/DML.js"

    // Make all functions in dml local
    (function () {
      for (const [key, value] of Object.entries(dml)) {
        ????????????[key] = value  // Your Idea here
      }
      h1("h1 test")  // call h1 INSIDE the function 
    })() // leave the scope
  </script>
Enter fullscreen mode Exit fullscreen mode
Thread Thread
 
artydev profile image
artydev • Edited

Perhaps using 'eval' ?
But as you know it's not recommended.

Thread Thread
 
peerreynders profile image
peerreynders • Edited

1.) It is not uncommon for style guides to discourage the use of wildcard imports even when the language supports it. Example:

Wildcard imports should not be used

On the principle that clearer code is better code, you should explicitly import the things you want to use in a module. Using import * imports everything in the module, and runs the risk of confusing maintainers. Similarly, export * from "module"; imports and then re-exports everything in the module, and runs the risk of confusing not just maintainers but also users of the module.

Also keep in mind that in C/C++ include occurs at compile-time so there is no runtime cost to dumping a boatload of unused names into the code. In JavaScript import happens at run time so potential for "more work" comes with the potential for undesirable run time consequences.

Other than that, for example, Google's style guide with regards to imports.


2.) In your example it looks like you are trying to export functions that are only capable of creating a single type of HTML element

Prior art:

import { h } from 'preact';

h('div', { id: 'foo' }, 'Hello!');
// <div id="foo">Hello!</div>

h('div', { id: 'foo' }, 'Hello', null, ['Preact!']);
// <div id="foo">Hello Preact!</div>

h(
    'div',
    { id: 'foo' },
    h('span', null, 'Hello!')
);
// <div id="foo"><span>Hello!</span></div>
Enter fullscreen mode Exit fullscreen mode

For a while React also had react-dom-factories:

import { em, h1, span } from 'react-dom-factories';

h1({ id: 'my-heading' }, span(null, em(null, 'Hell'), 'o'), ' world!');
Enter fullscreen mode Exit fullscreen mode

However support was dropped as JSX is the accepted standard among React developers.
Other helper examples: hyperscript-helpers, react-hyperscript.


3.)

So, my question is: Does anybody know a way to create local variables or const dynamically in Javascript?

Basically strict mode which is the default in ES2015 modules makes that impossible.

However for the time being one can still pull a stunt like this:

// file: with-fn.js
// source: https://twitter.com/WebReflection/status/1411677706287255552
// caveat emptor: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/with#ambiguity_contra
//
const withFnCache = new WeakMap();

function cacheWithFn(fn) {
  const wrapped = Function(
    'with(arguments[0])return(' +
      fn +
      ').apply(this,[].slice.call(arguments,1))'
  );
  withFnCache.set(fn, wrapped);
  return wrapped;
}

function withFn(fn) {
  const w = withFnCache.get(fn);
  return w ? w : cacheWithFn(fn);
}

export { withFn };
Enter fullscreen mode Exit fullscreen mode
// file: shared.js
const append = DocumentFragment.prototype.append;

function toFragment(children) {
  const fragment = document.createDocumentFragment();
  append.apply(fragment, Array.isArray(children) ? children : [children]);
  return fragment;
}

function createElement(tagName, children) {
  const element = document.createElement(tagName);
  element.appendChild(toFragment(children));
  return element;
}

export { createElement };
Enter fullscreen mode Exit fullscreen mode
// file: libA.js
import { createElement } from './shared.js';

function h1(...children) {
  return createElement('h1', children);
}

export { h1 };
Enter fullscreen mode Exit fullscreen mode
// file: libB.js
import { createElement } from './shared.js';

function em(...children) {
  return createElement('em', children);
}

export { em };
Enter fullscreen mode Exit fullscreen mode
<!DOCTYPE html>
<!-- index.html -->
<html>
  <head>
    <title>Faking implicit imports</title>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
  </head>
  <body>
    <script type="module">
     import { withFn } from './with-fn.js';
     import * as libA from './libA.js';
     import * as libB from './libB.js';

     const imports = Object.assign({}, libA, libB);
     const app = withFn(appFn);

     document
       .querySelector('body')
       .appendChild(app(imports, 'Test ', 'this!'));

     function appFn(first, last) {
       // `h1` and `em` aren't declared anywhere
       return h1(first, em(last));
     }
    </script>
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode

// file: with-imports.js
function withImports(imports, fn) {
  const h = Function(
    'imports',
    'args',
    `with(imports)return(${fn}).apply(this, args)`
  );
  return invokeWithImports;

  function invokeWithImports(...args) {
    return h(imports, args);
  }
}

export { withImports };
Enter fullscreen mode Exit fullscreen mode
<!DOCTYPE html>
<!-- index.html -->
<html>
  <head>
    <title>Faking implicit imports</title>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
  </head>
  <body>
    <script type="module">
     import { withImports } from './with-imports.js';
     import * as libA from './libA.js';
     import * as libB from './libB.js';

     const imports = Object.assign({}, libA, libB);
     const app = withImports(imports, appFn);

     document.querySelector('body').appendChild(app('Test ', 'this!'));

     function appFn(first, last) {
       // `h1` and `em` aren't declared anywhere
       return h1(first, em(last));
     }
    </script>
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode
Thread Thread
 
efpage profile image
Eckehard

I like any kind of dirty tricks, but 'eval' is really nasty. As this may cause serious security issues I would not try to use it. But I´m sure there will be a solution without.

Thread Thread
 
efpage profile image
Eckehard

Andrea Giammarchi reminded me, that rollup.js might be perfect. That´s indeed a smart solution!

Thread Thread
 
artydev profile image
artydev • Edited

Great, Andrea is an awesome guy
In fact I have published your TodoList using rollup and it's tree shaking feature :-)

Thread Thread
 
peerreynders profile image
peerreynders • Edited

MDN: Function:

Calling the constructor directly can create functions dynamically but suffers from security and similar (but far less significant) performance issues to Global_Objects/eval. However, unlike eval, the Function constructor creates functions that execute in the global scope only.

Also functions created with Function don't default to strict mode so it is possible to use the with statement which has been not welcome for the last decade.

The suggested alternative leads us back to

<!DOCTYPE html>
<!-- index.html -->
<html>
  <head>
    <title>Giving up on faking implicit imports</title>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
  </head>
  <body>
    <script type="module">
     import * as libA from './libA.js';
     import * as libB from './libB.js';
     const h = Object.assign({}, libA, libB);

     document.querySelector('body').appendChild(app('Test ', 'this!'));

     function app(first, last) {
       return h.h1(first, h.em(last));
     }
    </script>
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode

that rollup.js might be perfect.

It would let you work with smaller modules which are less prone to having a large number of exports that are then stitched together at build time - so wildcard imports would be much less problematic. In my judgement rollup.js is probably the most sane bundler solution at this time (even when esbuild is faster; Parcel is way too magical for my taste).