← JavaScript/Модули ES6#90 из 383← ПредыдущийСледующий →+25 XP
Полезно по теме:Гайд: как учить JavaScriptПрактика: JS базаПрактика: async и сетьТермин: Closure

Модули ES6

Какую проблему решают модули

До ES6 весь JavaScript выполнялся в глобальной области видимости. Представь большой React-проект: если все функции объявлены глобально, случайно назвать два formatDate в разных файлах — и они перезапишут друг друга. Скрипты нужно подключать в строгом порядке. Сопровождать это — кошмар.

Модули ES6 изолируют каждый файл в собственной области видимости. Зависимости явные: сразу видно, что откуда берётся.

На основе предыдущих уроков

  • «Функции» — функции можно экспортировать
  • «Классы» — классы тоже экспортируются (export default class)
  • «Объекты» — именованный экспорт похож на деструктуризацию объекта
  • Именованный экспорт

    // utils/format.js — несколько экспортов из одного файла
    export function formatPrice(amount, currency = '₽') {
      return amount.toLocaleString('ru-RU') + ' ' + currency
    }
    
    export function formatDate(date) {
      return new Date(date).toLocaleDateString('ru-RU')
    }
    
    export const TAX_RATE = 0.2
    // Импорт именованных экспортов — фигурные скобки обязательны
    import { formatPrice, formatDate, TAX_RATE } from './utils/format.js'
    
    // Переименование при импорте
    import { formatPrice as price } from './utils/format.js'
    
    // Импорт всего под неймспейсом
    import * as Format from './utils/format.js'
    Format.formatPrice(1000)  // '1 000 ₽'

    Экспорт по умолчанию (default)

    // services/UserService.js — один default export на файл
    export default class UserService {
      constructor(apiClient) {
        this.api = apiClient
      }
    
      async getUser(id) {
        return this.api.get(`/users/${id}`)
      }
    }
    // Импорт default — без фигурных скобок, имя любое
    import UserService from './services/UserService.js'
    import MyUserService from './services/UserService.js'  // тоже работает

    Оба типа вместе

    // api/client.js
    export default class ApiClient { ... }      // основной экспорт
    export function createClient(config) { ... } // вспомогательный
    export const DEFAULT_TIMEOUT = 5000
    import ApiClient, { createClient, DEFAULT_TIMEOUT } from './api/client.js'

    Barrel-файл (index.js) — единая точка входа

    // utils/index.js — агрегирует все утилиты
    export { formatPrice, formatDate } from './format.js'
    export { validateEmail, validatePhone } from './validate.js'
    export { default as ApiClient } from './api.js'
    // Теперь можно импортировать из одного места
    import { formatPrice, validateEmail, ApiClient } from './utils'
    // Вместо трёх отдельных импортов

    Динамический импорт — ленивая загрузка

    // Загружается только когда нужно (code splitting)
    async function openEditor() {
      // Тяжёлая библиотека загрузится только при клике на "Редактировать"
      const { default: Editor } = await import('./components/RichEditor.js')
      const editor = new Editor('#container')
    }
    
    // Условная загрузка
    const { Chart } = await import(
      isMobile ? './charts/MobileChart.js' : './charts/DesktopChart.js'
    )

    Типичные ошибки

    1. Путают именованный и default экспорт:

    // Файл: export default function greet() {}
    
    // Сломано:
    import { greet } from './greet.js'  // SyntaxError — default нужен без скобок
    
    // Исправлено:
    import greet from './greet.js'

    2. Круговые зависимости (circular imports):

    // a.js импортирует из b.js, а b.js импортирует из a.js
    // Это может привести к undefined при инициализации — избегай!

    3. Изменяют импортированные примитивы — они readonly:

    // Сломано:
    import { count } from './counter.js'
    count = 10  // TypeError: Assignment to constant variable
    
    // Исправлено — импортируй функцию, которая изменяет:
    import { count, increment } from './counter.js'
    increment()  // меняет внутри модуля

    В реальных проектах

  • React: каждый компонент — модуль: import Button from './components/Button'
  • Node.js: import express from 'express' — импорт npm-пакета
  • Vite/Webpack: собирают сотни модулей в один bundle для браузера
  • Barrel-файлы: src/components/index.ts — в любом крупном проекте на React/Vue
  • Примеры

    Симуляция модульной архитектуры: утилиты, сервисы, компоненты

    // Симуляция модульной структуры (в реальном проекте — отдельные файлы)
    
    // === utils/format.js ===
    const Format = {
      price: (amount, currency = '₽') =>
        amount.toLocaleString('ru-RU') + ' ' + currency,
    
      date: (dateStr) =>
        new Date(dateStr).toLocaleDateString('ru-RU', {
          day: '2-digit', month: 'long', year: 'numeric',
        }),
    
      truncate: (str, maxLen = 50) =>
        str.length > maxLen ? str.slice(0, maxLen) + '...' : str,
    
      pluralize: (n, one, few, many) => {
        const mod10 = n % 10, mod100 = n % 100
        if (mod10 === 1 && mod100 !== 11) return `${n} ${one}`
        if (mod10 >= 2 && mod10 <= 4 && (mod100 < 10 || mod100 >= 20)) return `${n} ${few}`
        return `${n} ${many}`
      },
    }
    
    // === utils/validate.js ===
    const Validate = {
      email: (email) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email),
      phone: (phone) => /^\+?[\d\s\-()]{10,}$/.test(phone),
      required: (value) => value !== null && value !== undefined && String(value).trim() !== '',
    }
    
    // === services/ProductService.js ===
    const products = [
      { id: 1, name: 'MacBook Pro 14"', price: 189990, category: 'laptops', stock: 5 },
      { id: 2, name: 'AirPods Pro',     price: 24990,  category: 'audio',   stock: 20 },
      { id: 3, name: 'Magic Mouse',     price: 8990,   category: 'acc',     stock: 15 },
    ]
    
    const ProductService = {
      getAll: () => products,
      getById: (id) => products.find(p => p.id === id) ?? null,
      getByCategory: (cat) => products.filter(p => p.category === cat),
      search: (query) => products.filter(p =>
        p.name.toLowerCase().includes(query.toLowerCase())
      ),
    }
    
    // === main.js — "импортируем" через деструктуризацию ===
    const { price, date, truncate, pluralize } = Format
    const { email: validateEmail } = Validate
    const { getAll, search } = ProductService
    
    // Используем
    console.log(price(189990))                     // '189 990 ₽'
    console.log(date('2024-01-15'))                // '15 января 2024 г.'
    console.log(truncate('Очень длинное название товара в каталоге', 25))  // 'Очень длинное название то...'
    console.log(pluralize(3, 'товар', 'товара', 'товаров'))  // '3 товара'
    
    console.log(validateEmail('ivan@mail.ru'))     // true
    console.log(validateEmail('notanemail'))       // false
    
    const results = search('pro')
    console.log(`Найдено: ${results.length} товар(а)`)
    results.forEach(p => console.log(`  ${p.name} — ${price(p.price)}`))

    Модули ES6

    Какую проблему решают модули

    До ES6 весь JavaScript выполнялся в глобальной области видимости. Представь большой React-проект: если все функции объявлены глобально, случайно назвать два formatDate в разных файлах — и они перезапишут друг друга. Скрипты нужно подключать в строгом порядке. Сопровождать это — кошмар.

    Модули ES6 изолируют каждый файл в собственной области видимости. Зависимости явные: сразу видно, что откуда берётся.

    На основе предыдущих уроков

  • «Функции» — функции можно экспортировать
  • «Классы» — классы тоже экспортируются (export default class)
  • «Объекты» — именованный экспорт похож на деструктуризацию объекта
  • Именованный экспорт

    // utils/format.js — несколько экспортов из одного файла
    export function formatPrice(amount, currency = '₽') {
      return amount.toLocaleString('ru-RU') + ' ' + currency
    }
    
    export function formatDate(date) {
      return new Date(date).toLocaleDateString('ru-RU')
    }
    
    export const TAX_RATE = 0.2
    // Импорт именованных экспортов — фигурные скобки обязательны
    import { formatPrice, formatDate, TAX_RATE } from './utils/format.js'
    
    // Переименование при импорте
    import { formatPrice as price } from './utils/format.js'
    
    // Импорт всего под неймспейсом
    import * as Format from './utils/format.js'
    Format.formatPrice(1000)  // '1 000 ₽'

    Экспорт по умолчанию (default)

    // services/UserService.js — один default export на файл
    export default class UserService {
      constructor(apiClient) {
        this.api = apiClient
      }
    
      async getUser(id) {
        return this.api.get(`/users/${id}`)
      }
    }
    // Импорт default — без фигурных скобок, имя любое
    import UserService from './services/UserService.js'
    import MyUserService from './services/UserService.js'  // тоже работает

    Оба типа вместе

    // api/client.js
    export default class ApiClient { ... }      // основной экспорт
    export function createClient(config) { ... } // вспомогательный
    export const DEFAULT_TIMEOUT = 5000
    import ApiClient, { createClient, DEFAULT_TIMEOUT } from './api/client.js'

    Barrel-файл (index.js) — единая точка входа

    // utils/index.js — агрегирует все утилиты
    export { formatPrice, formatDate } from './format.js'
    export { validateEmail, validatePhone } from './validate.js'
    export { default as ApiClient } from './api.js'
    // Теперь можно импортировать из одного места
    import { formatPrice, validateEmail, ApiClient } from './utils'
    // Вместо трёх отдельных импортов

    Динамический импорт — ленивая загрузка

    // Загружается только когда нужно (code splitting)
    async function openEditor() {
      // Тяжёлая библиотека загрузится только при клике на "Редактировать"
      const { default: Editor } = await import('./components/RichEditor.js')
      const editor = new Editor('#container')
    }
    
    // Условная загрузка
    const { Chart } = await import(
      isMobile ? './charts/MobileChart.js' : './charts/DesktopChart.js'
    )

    Типичные ошибки

    1. Путают именованный и default экспорт:

    // Файл: export default function greet() {}
    
    // Сломано:
    import { greet } from './greet.js'  // SyntaxError — default нужен без скобок
    
    // Исправлено:
    import greet from './greet.js'

    2. Круговые зависимости (circular imports):

    // a.js импортирует из b.js, а b.js импортирует из a.js
    // Это может привести к undefined при инициализации — избегай!

    3. Изменяют импортированные примитивы — они readonly:

    // Сломано:
    import { count } from './counter.js'
    count = 10  // TypeError: Assignment to constant variable
    
    // Исправлено — импортируй функцию, которая изменяет:
    import { count, increment } from './counter.js'
    increment()  // меняет внутри модуля

    В реальных проектах

  • React: каждый компонент — модуль: import Button from './components/Button'
  • Node.js: import express from 'express' — импорт npm-пакета
  • Vite/Webpack: собирают сотни модулей в один bundle для браузера
  • Barrel-файлы: src/components/index.ts — в любом крупном проекте на React/Vue
  • Примеры

    Симуляция модульной архитектуры: утилиты, сервисы, компоненты

    // Симуляция модульной структуры (в реальном проекте — отдельные файлы)
    
    // === utils/format.js ===
    const Format = {
      price: (amount, currency = '₽') =>
        amount.toLocaleString('ru-RU') + ' ' + currency,
    
      date: (dateStr) =>
        new Date(dateStr).toLocaleDateString('ru-RU', {
          day: '2-digit', month: 'long', year: 'numeric',
        }),
    
      truncate: (str, maxLen = 50) =>
        str.length > maxLen ? str.slice(0, maxLen) + '...' : str,
    
      pluralize: (n, one, few, many) => {
        const mod10 = n % 10, mod100 = n % 100
        if (mod10 === 1 && mod100 !== 11) return `${n} ${one}`
        if (mod10 >= 2 && mod10 <= 4 && (mod100 < 10 || mod100 >= 20)) return `${n} ${few}`
        return `${n} ${many}`
      },
    }
    
    // === utils/validate.js ===
    const Validate = {
      email: (email) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email),
      phone: (phone) => /^\+?[\d\s\-()]{10,}$/.test(phone),
      required: (value) => value !== null && value !== undefined && String(value).trim() !== '',
    }
    
    // === services/ProductService.js ===
    const products = [
      { id: 1, name: 'MacBook Pro 14"', price: 189990, category: 'laptops', stock: 5 },
      { id: 2, name: 'AirPods Pro',     price: 24990,  category: 'audio',   stock: 20 },
      { id: 3, name: 'Magic Mouse',     price: 8990,   category: 'acc',     stock: 15 },
    ]
    
    const ProductService = {
      getAll: () => products,
      getById: (id) => products.find(p => p.id === id) ?? null,
      getByCategory: (cat) => products.filter(p => p.category === cat),
      search: (query) => products.filter(p =>
        p.name.toLowerCase().includes(query.toLowerCase())
      ),
    }
    
    // === main.js — "импортируем" через деструктуризацию ===
    const { price, date, truncate, pluralize } = Format
    const { email: validateEmail } = Validate
    const { getAll, search } = ProductService
    
    // Используем
    console.log(price(189990))                     // '189 990 ₽'
    console.log(date('2024-01-15'))                // '15 января 2024 г.'
    console.log(truncate('Очень длинное название товара в каталоге', 25))  // 'Очень длинное название то...'
    console.log(pluralize(3, 'товар', 'товара', 'товаров'))  // '3 товара'
    
    console.log(validateEmail('ivan@mail.ru'))     // true
    console.log(validateEmail('notanemail'))       // false
    
    const results = search('pro')
    console.log(`Найдено: ${results.length} товар(а)`)
    results.forEach(p => console.log(`  ${p.name} — ${price(p.price)}`))

    Задание

    Ты строишь утилитарный слой для e-commerce приложения. Создай три объекта-"модуля": 1. `MathUtils` — `sum(arr)`, `average(arr)`, `clamp(val, min, max)` 2. `StringUtils` — `capitalize(str)`, `truncate(str, len)`, `slugify(str)` (пробелы → дефисы, строчные) 3. `ArrayUtils` — `unique(arr)`, `groupBy(arr, key)`, `sortBy(arr, key)` Деструктурируй функции из "модулей" и используй их для обработки данных каталога товаров.

    Подсказка

    average: MathUtils.sum(arr) / arr.length. clamp: Math.min(Math.max(val, min), max). capitalize: str[0].toUpperCase() + str.slice(1). truncate: str.length > len ? str.slice(0, len) + "..." : str. slugify: str.toLowerCase().replace(/\s+/g, "-"). sortBy: [...arr].sort((a, b) => a[key] > b[key] ? 1 : -1).

    Загружаем среду выполнения...
    Загружаем AI-помощника...