Модульное программирование позволяет создавать большие программы, собирая их из отдельных модулей кода, написанных разными авторами. Главная цель модульности — инкапсуляция (скрытие) внутренних деталей реализации и поддержание чистоты глобального пространства имен, чтобы модули не могли случайно повлиять на переменные, функции и классы, определенные в других модулях.
Долгое время в JavaScript не было встроенной поддержки модулей, и разработчики использовали различные подходы для организации кода: классы, объекты и замыкания. Со временем сообщество выработало практику использования функции require(), которая стала стандартом в среде Node.js. Позже в стандарте ES6 появились ключевые слова import и export для работы с модулями.
Классы естественным образом работают как модули для своих методов. Например, разные классы могут иметь методы с одинаковыми именами, но это не вызывает конфликтов, поскольку каждый метод является свойством независимого объекта-прототипа.
class SingletonSet {
has(value) { /* реализация */ }
}
class BitSet {
has(value) { /* реализация */ }
}Метод has() в SingletonSet не перезаписывает одноименный метод в BitSet, потому что они принадлежат разным объектам.
Чтобы избежать загрязнения глобального пространства имен, можно группировать функциональность в объектах:
// Вместо множества глобальных классов
const Sets = {
Singleton: class { /* ... */ },
Bit: class { /* ... */ }
};
// Использование
const set = new Sets.Bit();Этот подход похож на то, как в JavaScript организованы математические функции в объекте Math.
Классы и объекты не позволяют полностью скрыть внутренние детали реализации. Для этого используют замыкания — возможность функции сохранять доступ к переменным из своей области видимости даже после выполнения.
Рассмотрим пример модуля для работы с битовыми множествами:
const BitSet = (function() {
// Приватные функции и константы
function isValid(set, n) { /* ... */ }
function has(set, byte, bit) { /* ... */ }
const BITS = new Uint8Array([1, 2, 4, 8, 16, 32, 64, 128]);
const MASKS = new Uint8Array([~1, ~2, ~4, ~8, ~16, ~32, ~64, ~128]);
// Возвращаем публичный класс
return class BitSet extends AbstractWritableSet {
// Реализация класса, использующая приватные функции
};
}());Здесь мы используем немедленно вызываемую функцию (IIFE), которая возвращает класс. Все переменные и функции, объявленные внутри IIFE, остаются приватными и недоступными извне.
Создадим модуль с функциями для статистических вычислений:
const stats = (function() {
// Приватные вспомогательные функции
const sum = (x, y) => x + y;
const square = x => x * x;
// Публичные функции
function mean(data) {
return data.reduce(sum) / data.length;
}
function stddev(data) {
let m = mean(data);
return Math.sqrt(
data.map(x => x - m).map(square).reduce(sum) / (data.length - 1)
);
}
// Экспортируем публичный API
return { mean, stddev };
}());
// Использование
stats.mean([1, 3, 5, 7, 9]); // 5
stats.stddev([1, 3, 5, 7, 9]); // Math.sqrt(10)В этом примере функции sum() и square() являются внутренними деталями реализации и недоступны извне модуля.
Преобразование кода в модули с помощью IIFE — механический процесс. Можно представить инструмент, который:
- Берет несколько файлов с кодом
- Оборачивает каждый файл в IIFE
- Отслеживает, какие значения должны быть экспортированы
- Объединяет все в один большой файл
Пример результата работы такого инструмента:
const modules = {};
function require(moduleName) {
return modules[moduleName];
}
modules["sets.js"] = (function() {
const exports = {};
// Содержимое файла sets.js
exports.BitSet = class BitSet { /* ... */ };
return exports;
}());
modules["stats.js"] = (function() {
const exports = {};
// Содержимое файла stats.js
const sum = (x, y) => x + y;
const square = x => x * x;
exports.mean = function(data) { /* ... */ };
exports.stddev = function(data) { /* ... */ };
return exports;
}());Теперь мы можем использовать эти модули:
// Получаем ссылки на нужные модули
const stats = require("stats.js");
const BitSet = require("sets.js").BitSet;
// Используем модули
let s = new BitSet(100);
s.insert(10);
s.insert(20);
s.insert(30);
let average = stats.mean([...s]); // 20Этот подход лежит в основе работы современных инструментов сборки, таких как webpack и Parcel, а также напоминает систему модулей Node.js с использованием require().
Модульность — важнейшая концепция в современной JavaScript-разработке. Понимание того, как организовать код с помощью классов, объектов и замыканий, создает прочную основу для изучения более современных систем модулей ES6 и Node.js.
Ключевые принципы модульного программирования:
- Инкапсуляция реализации
- Чистое глобальное пространство имен
- Четкое определение публичного API
- Возможность повторного использования кода