Причины для сокрытия глобалов могут быть разные: для красоты, из соображений безопасности, для затруднения анализа кода и другие. Пользователь взаимодействует с вашим кодом, используя события, которые он не сможет сломать — больше ему ничего и не нужно.
Самый очевидный способ — создать единственный namespace в который пассивно экспортировать прочие объекты, а jQuery и $ в конце удалить.
После сборки код будет какой-то такой:
(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));
Это идеальный вариант, но чаще бывает не так. Посмотрите ваш код, такой ли он?
Под катом универсальное решение, позволяющее собрать любой код без единой глобальной переменной.
Понятия активный/пассивный импорт/экспорт
Активный экспорт
Модуль ModuleA активно экспортируется(function (window) { var ModuleA = {}; // Export window.ModuleA = ModuleA; }(window));
Пассивный экспорт
Модуль ModuleC пассивно экспортируетсяvar ModuleC = (function (window) { var ModuleC = {}; // Export return ModuleC; }(window));
Активный импорт
Модуль jQuery активно импортируется(function (window, $) { console.log($); }(this, jQuery));
Пассивный импорт
Модуль ModuleC пассивно импортируется(function (window) { console.log(ModuleC); }(this));
Сборка
Имеем несколько модулей, которые как попало импортируют/экспортируют. Мы знаем какие модули использует каждый. Мы можем обернуть каждый модуль в замыкание в которое передать список всех объектов, которые он использует. Правда тут есть небольшая проблема: если модуль А использует модуль Б, а модуль Б использует модуль А и все они подключаются последовательно, то при подключении модуля А, обернутого в замыкание будет ошибка ReferenceError — модуля Б ещё нет. Эта проблема решается пробросом объекта в замыкание А после загрузки модуля Б. Если модуль пассивно экспортирует и если мы его обернем в замыкание, то пропадет глобальная переменная. Эта проблема решается пробросом локальной переменной в глобальный контекст.
Мы знаем список глобальных переменных, которые мы создали в процессе инициализации скрипта — просто удаляем их через delete для очистки глобалов.
Пример
Разберем пример проекта, состоящий из jQuery и 3 модулей, которые как попало импортируют/экспортируют.
Листинги модулей
// 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);
Модули подключаются в такой последовательности: jQuery отдельно, ModuleA+ModuleC+ModuleD. Модуль А зависит от модуля С, модуль С зависит от модуля А (конфликт, описанный выше). Модуль А активно экспортирует, модуль С пассивно экспортирует себя, Модуль Д ничего не экспортирует.
Собираем
// Глобальное замыкание нашей сборки (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));
Проблема захвата переменной
Данное решение позволяет только затруднить анализ кода и очистить глобальный контекст от лишних переменных (для красоты).
Не секрет, что скрипт, встроенный до подключения наших скриптов (типичный userscript или extension) может перехватить объект ModuleA и ModuleС до того как мы их удалим, используя watch, __defineSetter__, ES5 set и возможно через минимальный setInterval.
У этой проблемы есть 3 решения:
1. не использовать активный экспорт (шанс захвата равен нулю, необходимо менять код)
2. использовать случайное имя глобальной переменной (шанс захвата стремится к нулю, необходимо менять код)
3. удалить наблюдателей и таймеры перед началом сборки (шанс захвата равен нулю)
Будем использовать способ 3, изменим нашу сборку:
// Глобальное замыкание нашей сборки (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));
Теперь и вы овладели самой сложной техникой JavaScript Ninja — «Сокрытие глобальных переменных»!
Исключение
Данный метод не поможет вам, если вы специально сорите в глобалы, используете вызов событий через onsmth="...", т.е. делаете плохо ;) Метод не сработает если у вас в глобале находится не объект.
Ninja JavaScript Builder — Ninjs
Чтобы вам не писать реализацию данного метода сборки я написал тузлу — Ninjs. Это сборщик проекта на JavaScript по методу, описанному выше. Билдер как ни есть лучше оправдывает своё название — он как нинзя незаметно делает своё дело и подчищает следы.
Проект находится на github github.com/azproduction/ninjs
Использует Node.js для сборки.
Пока не имеет регистрации в npm, но обязательно будет.
Пока не умеет предотвращать захват переменных — будет.
Использование
Использовать Ninjs очень просто — нам достаточно знать какие переменные импортируют/экспортируют наши модули и где они лежат. Все зависимости Ninjs разрешит за вас.
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);
Код примера и код модулей находится тут github.com/azproduction/ninjs/tree/master/examples
Следите за обновлениями. Предложения, пожелания и критика приветствуются!
PS Ищу Ninja иконку для превращения её в логотип Ninjs.

