📜 ⬆️ ⬇️

Build a project without a single global variable

Imagine you have a project consisting of several modules and, for example, jQuery or any other library in a CDN. You have a great desire to not show your global variables to the user and, if possible, not to show jQuery and $. And, of course, do everything without changing the project code.
The reasons for hiding globals can be different: for beauty, for security reasons, for the difficulty of analyzing code and others. The user interacts with your code using events that he cannot break - he doesn't need anything else.

The most obvious way is to create a single namespace into which to passively export other objects, and delete jQuery and $ at the end.

After assembly, the code will be some such:
(function(window, undefined){ // include ./js/YourNamespace.js var YourNamespace = (function () { // -  return {}; }()); // include ./js/YourNamespace/SomeObject.js YourNamespace.SomeObject = (function () { // -  return function () { }; }()); // Cleanup delete window.$; delete window.jQuery; }(window)); 

This is ideal, but more often it does not. See your code, is it?
')
Under the cut is a universal solution that allows you to collect any code without a single global variable.

Active / Passive Import / Export Concepts


Active export
 (function (window) { var ModuleA = {}; // Export window.ModuleA = ModuleA; }(window)); 
ModuleA module actively exported

Passive export
 var ModuleC = (function (window) { var ModuleC = {}; // Export return ModuleC; }(window)); 
ModuleC module is passively exported

Active Import
 (function (window, $) { console.log($); }(this, jQuery)); 
JQuery module actively imported

Passive import
 (function (window) { console.log(ModuleC); }(this)); 
ModuleC module is passively imported

Assembly


We have several modules that import / export. We know what modules each uses. We can wrap each module in a closure in which to transfer a list of all the objects it uses. The truth is there is a small problem: if module A uses module B, and module B uses module A and they all connect in series, then when connecting module A wrapped in a closure, there will be an error ReferenceError - module B is not there yet. This problem is solved by forwarding the object to the closure A after loading the module B. If the module is passively exported and if we wrap it in the closure, then the global variable will disappear. This problem is solved by forwarding a local variable to the global context.
We know the list of global variables that we created during the script initialization process — we simply delete them through delete to clear globals.

Example


Let us examine an example of a project consisting of jQuery and 3 modules that import / export as it is.

Module Listings

 // Uses ModuleC, $ (function (window) { var ModuleA = { a: 'ModuleA.a', b: 2, d: function () { console.log(ModuleC.c === 'ModuleC.c'); console.log(typeof $ === 'function'); } }; // Export window.ModuleA = ModuleA; }(window)); 

 // Uses ModuleA var ModuleC = (function (window) { var ModuleC = { a: 1, b: 2, c: 'ModuleC.c', d: function () { console.log(ModuleA.a === 'ModuleA.a'); } }; // Export return ModuleC; }(window)); 

 // Uses ModuleA, ModuleC ModuleA.c(); window.setTimeout(function () { ModuleC.d(); ModuleA.d(); }, 0); 

The modules are connected in the following sequence: jQuery separately, ModuleA + ModuleC + ModuleD. Module A depends on module C, module C depends on module A (the conflict described above). Module A actively exports; Module C passively exports itself; Module D does not export anything.

We collect

 //     (function(window, undefined){ //       var Medium = { wait: function (varName, callback) {/* -   */}, ready: function (varName, varValue) {/* -   */} }; //   A,      (function (ModuleC, $) { // ..  ModuleC   ,    Medium.wait('ModuleC', function (value) {ModuleC = value;}); (function (window) { var ModuleA = { a: 'ModuleA.a', b: 2, d: function () { console.log(ModuleC.c === 'ModuleC.c'); console.log(typeof $ === 'function'); } }; // Export window.ModuleA = ModuleA; }(window)); } (undefined, $)); //   C,      (function (ModuleA) { var ModuleC = (function (window) { var ModuleC = { a: 1, b: 2, c: 'ModuleC.c', d: function () { console.log(ModuleA.a === 'ModuleA.a'); } }; // Export return ModuleC; }(window)); // ..  ModuleC  ,      ,    window.ModuleC = ModuleC; //   C  -      Medium.ready('ModuleC', ModuleC); } (ModuleA)); //   ,      (function (ModuleA,Module) { // Uses ModuleA, ModuleC ModuleA.c(); window.setTimeout(function () { ModuleC.d(); ModuleA.d(); }, 0); } (ModuleA,Module)); //      -         try { delete window.$; delete window.jQuery; delete window.ModuleA; delete window.Module; } catch (e){ // IE  window.$ = undefined; window.jQuery = undefined; window.ModuleA = undefined; window.Module = undefined; } //          ! }(window)); 

Variable capture problem


This solution only makes it difficult to analyze the code and clear the global context of unnecessary variables (for beauty).
It’s no secret that a script built in before connecting our scripts (typical userscript or extension) can intercept the ModuleA and Module objects before we remove them using watch, __ defineSetter__, ES5 set and possibly through minimal setInterval.

This problem has 3 solutions:
1. do not use active export (the chance of capture is zero, you need to change the code)
2. use the random name of the global variable (the chance of capture tends to zero, you need to change the code)
3. remove observers and timers before assembling (the chance of capture is zero)

We will use method 3, change our build:
 //     (function(window, undefined){ //  __defineSetter__, ES5 set, watch window.unwatch && window.unwatch('ModuleA'); window.unwatch && window.unwatch('Module'); try { delete window.ModuleA; delete window.Module; } catch (e){ // IE  window.ModuleA = undefined; window.Module = undefined; } //   var maxIntervalId = window.setInterval(function (){}, 1e10); var maxTimeoutlId = window.setTimeout(function (){}, 1e10); while (maxIntervalId--) { window.clearInterval(maxIntervalId); } while (maxTimeoutlId--) { window.clearTimeout(maxTimeoutlId); } //    // ... }(window)); 

Now you have mastered the most complex JavaScript technique Ninja - “Hiding global variables”!

An exception

This method will not help you if you deliberately flush globals, use event calls via onsmth = "...", i.e. do badly;) The method will not work if you have a non-object in the global.

Ninja JavaScript Builder - Ninjs


So that you do not write the implementation of this assembly method, I wrote to the tul - Ninjs. This is a JavaScript project builder according to the method described above. The builder, however well, better justifies his name - he, like a ninja, imperceptibly does his job and cleans up the tracks.

The project is on github github.com/azproduction/ninjs
Uses Node.js to build.
It does not yet have registration in npm, but it will definitely be.
While not able to prevent the capture of variables - will be.

Using

It is very easy to use Ninjs - it's enough for us to know which variables import / export our modules and where they lie. All dependencies Ninjs will solve for you.
 var ninjs = new (require('../Ninjs.js').Ninjs); ninjs .add({ //  ModuleA file: './files/ModuleA.js', //    ModuleC  ModuleB imports: ['ModuleC', 'ModuleB'], //   ModuleA exports: 'ModuleA' }) .add({ //  ModuleB file: './files/ModuleB.js', //    ModuleA    jQuery imports: 'ModuleA', //   ModuleB exports: 'ModuleB' }) .add({ //  ModuleD file: './files/ModuleD.js', //    ModuleA, ModuleB and ModuleC imports: ['ModuleA', 'ModuleB', 'ModuleC'] //    }) .add({ //  ModuleC file: './files/ModuleC.js', //    ModuleA, ModuleB imports: ['ModuleA', 'ModuleB'], //    ModuleC exports: 'ModuleC', //  ModuleC   ,     forceExports: 'ModuleC' }) //   .cleanup('ModuleA', 'ModuleB', 'ModuleC', '$', 'jQuery') //  STDOUT .print(true); //         ,    // .print(false); 

The sample code and module code is here github.com/azproduction/ninjs/tree/master/examples

Keep for updates. Suggestions, suggestions and criticism are welcome!

PS I am looking for a Ninja icon to turn it into a Ninjs logo.

Source: https://habr.com/ru/post/117229/


All Articles