module.exports это значение, которое файл Node.js отдаёт любому другому файлу, загружающему его через require(). Сначала это пустой объект: добавьте ему свойства, чтобы экспортировать несколько вещей, или присвойте новое значение (функцию, класс), чтобы экспортировать одну. exports это переменная-сокращение, которая указывает на тот же объект, поэтому exports.add = ... работает, а exports = ... рвёт связь и ничего не экспортирует.
Программа вывела 5 20 3.14159, затем [ 'add', 'multiply', 'PI' ]. Все три строки в math.js добавили свойства одному и тому же объекту, через module.exports или через exports, и require('./math') вернул этот объект.
Экспорт функции, класса или объекта
Когда у модуля одна главная вещь, присвойте её прямо в module.exports. Тогда вызывающий код получает саму функцию, а не объект вокруг неё:
Программа вывела Hello, Ada и Grace (user). Если видите TypeError: greet is not a function, вызывающий код ждал не ту форму: пишите const { greet } = require(...) для свойства объекта и const greet = require(...) для присвоенного значения.
module.exports и exports: ловушка с переприсваиванием
Перед запуском вашего файла Node.js выполняет exports = module.exports. Оба имени указывают на один объект, а require возвращает module.exports. Присваивание нового значения в exports меняет только локальную переменную:
broken.js экспортировал {}, потому что exports теперь указывает на новый объект, который никто не возвращает. working.js экспортировал { version: 2 }.
| Вы пишете | Экспортируется? |
|---|---|
module.exports.x = 1 | Да |
exports.x = 1 | Да, тот же объект |
module.exports = { x: 1 } | Да, объект заменяется |
exports = { x: 1 } | Нет, значение теряется |
Самое простое правило: всегда пишите module.exports. Это работает в любом случае, и по коду видно, что он делает.
require возвращает тот же объект из кэша
Node.js выполняет код модуля один раз, при первом require, и кэширует module.exports. Каждый следующий require того же файла возвращает тот же объект, поэтому состояние внутри модуля общее для всех файлов, которые его загружают:
counter.js is running появилось один раз, b.next() вернул 3, а a === b равно true, потому что ключ кэша это полный путь к файлу, а ./counter и ./counter.js указывают на один файл. Поэтому подключение к базе данных или объект конфигурации, созданный в одном модуле, общий для всего приложения.
module.exports и ES модули
ES модули используют export и import. Формы соответствуют друг другу так:
// CommonJS // ES modules
module.exports = greet; export default greet;
module.exports.add = add; export { add };
const greet = require('./greet'); import greet from './greet.js';
const { add } = require('./math'); import { add } from './math.js';
ES модуль может через import подключить файл CommonJS: его module.exports приходит как импорт по умолчанию. Начиная с Node.js 22.12, require() тоже может загрузить ES модуль, если в нём нет top-level await. Что когда выбирать, читайте в статье CommonJS и ES модули.
Частые ошибки
exports.x и module.exports = ... в одном файле. Присваивание заменяет объект, и все exports.x, записанные до него, пропадают. Выберите один стиль на файл.
Экспорт значения, которое потом меняется. module.exports = { count } копирует число в момент загрузки; когда модуль обновляет count, вызывающий код всё ещё видит старое значение. Экспортируйте функцию, которая его возвращает, как counter.js выше.
Пропущенный ./. require('math') ищет пакет в node_modules, а не math.js рядом с вашим файлом. Локальные файлы всегда начинаются с ./ или ../.
Часто задаваемые вопросы
Можно ли использовать module.exports и export в одном файле?
Нет. Файл либо CommonJS (require, module.exports), либо ES модуль (import, export). Node.js решает это по расширению (.cjs или .mjs) или по полю "type" в package.json.
Почему require возвращает пустой объект?
Обычно модуль написал exports = ... вместо module.exports = ..., или два модуля подключают друг друга. При циклическом require один из них получает module.exports другого раньше, чем тот файл выполнится до конца.
Нужно ли расширение .js в require?
Нет. require('./math') пробует ./math, затем ./math.js, ./math.json и ./math.node, затем папку ./math с index.js. В import ES модуля расширение обязательно.
Как перезагрузить модуль, который require уже закэшировал?
Удалите его запись через delete require.cache[require.resolve('./math')] и подключите заново. Это полезно в основном в тестах и инструментах разработки; код приложения не должен на это полагаться.