← Собеседование/Функциональное программирование в JS#375 из 383← ПредыдущийСледующий →+40 XP
Полезно по теме:Маршрут: подготовка к интервьюГайд: портфолио juniorГайд: карьерный планТермин: Closure
← НазадДалее →

Функциональное программирование в JS

Краткий ответ

Функциональное программирование (FP) — это стиль, при котором программа строится из чистых функций без побочных эффектов, данные не мутируются, а функции — объекты первого класса. В JS FP нативен: функции можно передавать как аргументы, возвращать из функций, хранить в переменных. Ключевые инструменты: map/filter/reduce, composе/pipe, каррирование, частичное применение.

Полный разбор

Чистые функции

Чистая функция — детерминирована (одинаковый ввод = одинаковый вывод) и не имеет побочных эффектов:

// НЕ чистая — читает внешнее состояние, имеет побочный эффект
let tax = 0.2
function calculatePrice(price) {
  console.log('Расчёт...')  // побочный эффект
  return price * (1 + tax)  // зависит от внешней переменной
}

// Чистая — предсказуема, изолирована
function calculatePrice(price, taxRate) {
  return price * (1 + taxRate)
}
// calculatePrice(100, 0.2) всегда вернёт 120

Иммутабельность

Не мутируй входные данные — возвращай новые:

// ПЛОХО — мутация аргумента
function addUser(users, user) {
  users.push(user)  // мутируем оригинальный массив!
  return users
}

// ХОРОШО — возвращаем новый массив
function addUser(users, user) {
  return [...users, user]
}

// Обновление объекта без мутации
function updateAge(user, newAge) {
  return { ...user, age: newAge }
}

Функции высшего порядка

Функция, принимающая или возвращающая другую функцию:

// map, filter, reduce — встроенные функции высшего порядка
const numbers = [1, 2, 3, 4, 5]

const doubled = numbers.map(x => x * 2)           // [2, 4, 6, 8, 10]
const evens = numbers.filter(x => x % 2 === 0)    // [2, 4]
const sum = numbers.reduce((acc, x) => acc + x, 0) // 15

// Собственная функция высшего порядка
function repeat(n, fn) {
  return Array.from({ length: n }, (_, i) => fn(i))
}
repeat(3, i => i * i)  // [0, 1, 4]

Композиция функций

// compose: выполняет функции справа налево (математическая нотация)
// compose(f, g, h)(x) === f(g(h(x)))
function compose(...fns) {
  return (x) => fns.reduceRight((acc, fn) => fn(acc), x)
}

// pipe: выполняет функции слева направо (читается как поток данных)
// pipe(f, g, h)(x) === h(g(f(x)))
function pipe(...fns) {
  return (x) => fns.reduce((acc, fn) => fn(acc), x)
}

// Пример использования
const trim = s => s.trim()
const toLowerCase = s => s.toLowerCase()
const addExclamation = s => s + '!'

const process = pipe(trim, toLowerCase, addExclamation)
process('  Hello World  ')  // 'hello world!'

Каррирование vs частичное применение

// Каррирование: функция с N аргументами → цепочка функций с 1 аргументом
function curry(fn) {
  return function curried(...args) {
    if (args.length >= fn.length) {
      return fn.apply(this, args)
    }
    return function(...args2) {
      return curried.apply(this, args.concat(args2))
    }
  }
}

const add = curry((a, b, c) => a + b + c)
add(1)(2)(3)   // 6
add(1, 2)(3)   // 6
add(1)(2, 3)   // 6

// Частичное применение: фиксируем часть аргументов
function partial(fn, ...presetArgs) {
  return function(...laterArgs) {
    return fn(...presetArgs, ...laterArgs)
  }
}

const multiply = (a, b) => a * b
const double = partial(multiply, 2)
double(5)   // 10
double(10)  // 20

Императивный vs функциональный стиль

const orders = [
  { id: 1, amount: 150, status: 'completed', userId: 'u1' },
  { id: 2, amount: 80,  status: 'pending',   userId: 'u2' },
  { id: 3, amount: 300, status: 'completed', userId: 'u1' },
  { id: 4, amount: 50,  status: 'cancelled', userId: 'u3' },
]

// Императивно: получить сумму завершённых заказов пользователя u1
function getTotalImperative(orders, userId) {
  let total = 0
  for (let i = 0; i < orders.length; i++) {
    if (orders[i].userId === userId && orders[i].status === 'completed') {
      total += orders[i].amount
    }
  }
  return total
}

// Функционально: декларативно, читается как описание задачи
const getTotalFunctional = (orders, userId) =>
  orders
    .filter(o => o.userId === userId && o.status === 'completed')
    .map(o => o.amount)
    .reduce((sum, amount) => sum + amount, 0)

console.log(getTotalImperative(orders, 'u1'))   // 450
console.log(getTotalFunctional(orders, 'u1'))   // 450

Почему FP улучшает тестируемость

Чистые функции тестируются без моков и setup:

// Нечистая: требует mocking базы данных
async function getUser(id) {
  return await db.query('SELECT * FROM users WHERE id = ?', [id])
}

// Чистая логика легко тестируется
function formatUser(rawUser) {
  return {
    id: rawUser.id,
    name: rawUser.first_name + ' ' + rawUser.last_name,
    email: rawUser.email.toLowerCase()
  }
}
// test: formatUser({ id: 1, first_name: 'Ivan', last_name: 'Ivanov', email: 'Ivan@Example.COM' })
// ожидаем: { id: 1, name: 'Ivan Ivanov', email: 'ivan@example.com' }

Связанные уроки курса

  • Функции — основа FP, первоклассные объекты в JS
  • Методы массивов — map, filter, reduce как функции высшего порядка
  • Каррирование — детальный разбор с реализацией curry()
  • Как отвечать на собеседовании

    Начни с определений: «чистая функция», «иммутабельность», «функции первого класса». Покажи compose/pipe — это любимый вопрос на JS-собеседованиях. Объясни разницу между каррированием (f(a)(b)(c)) и частичным применением (partial(f, a)(b, c)). Подчеркни практическую ценность: FP-код легче тестировать, переиспользовать и рассуждать о нём.

    Красные флаги ответа

  • Путаница между каррированием и частичным применением — это разные вещи, хотя оба фиксируют аргументы
  • Мутация входных данных в «функциональных» примерах — нарушает принцип иммутабельности и делает код непредсказуемым
  • Незнание compose/pipe — базовые инструменты FP, без них нельзя строить функциональные пайплайны
  • Примеры

    Compose, pipe, partial application, curry и рефакторинг императивного кода в функциональный стиль

    // ===== COMPOSE И PIPE =====
    console.log('=== compose / pipe ===')
    
    function compose(...fns) {
      return (x) => fns.reduceRight((acc, fn) => fn(acc), x)
    }
    
    function pipe(...fns) {
      return (x) => fns.reduce((acc, fn) => fn(acc), x)
    }
    
    // Строковые трансформации
    const trim        = s => s.trim()
    const toLowerCase = s => s.toLowerCase()
    const capitalize  = s => s.charAt(0).toUpperCase() + s.slice(1)
    const addDot      = s => s.endsWith('.') ? s : s + '.'
    
    // pipe читается слева направо как поток
    const normalizeText = pipe(trim, toLowerCase, capitalize, addDot)
    console.log(normalizeText('  привет МИР  '))  // 'Привет мир.'
    console.log(normalizeText('HELLO'))            // 'Hello.'
    
    // compose читается справа налево (математически)
    const normalizeCompose = compose(addDot, capitalize, toLowerCase, trim)
    console.log(normalizeCompose('  HELLO  '))    // 'Hello.'
    
    // ===== ЧАСТИЧНОЕ ПРИМЕНЕНИЕ =====
    console.log('\n=== Частичное применение ===')
    
    function partial(fn, ...presetArgs) {
      return function(...laterArgs) {
        return fn(...presetArgs, ...laterArgs)
      }
    }
    
    // Базовые функции
    const multiply = (a, b) => a * b
    const add      = (a, b) => a + b
    const pow      = (base, exp) => Math.pow(base, exp)
    
    // Специализированные функции через partial
    const double   = partial(multiply, 2)
    const triple   = partial(multiply, 3)
    const add10    = partial(add, 10)
    const square   = partial(pow, undefined) // не подходит — нужен другой подход
    
    console.log(double(5))   // 10
    console.log(triple(5))   // 15
    console.log(add10(25))   // 35
    
    // Практичный пример: partial для fetch
    function fetchData(baseUrl, endpoint) {
      return `GET ${baseUrl}${endpoint}`  // упрощённо
    }
    
    const fetchFromAPI = partial(fetchData, 'https://api.example.com')
    console.log(fetchFromAPI('/users'))      // GET https://api.example.com/users
    console.log(fetchFromAPI('/products'))   // GET https://api.example.com/products
    
    // ===== КАРРИРОВАНИЕ =====
    console.log('\n=== Каррирование ===')
    
    function curry(fn) {
      return function curried(...args) {
        if (args.length >= fn.length) {
          return fn.apply(this, args)
        }
        return function(...args2) {
          return curried.apply(this, args.concat(args2))
        }
      }
    }
    
    const curriedAdd = curry((a, b, c) => a + b + c)
    console.log(curriedAdd(1)(2)(3))   // 6
    console.log(curriedAdd(1, 2)(3))   // 6
    console.log(curriedAdd(1)(2, 3))   // 6
    console.log(curriedAdd(1, 2, 3))   // 6
    
    // Практичный пример: каррированный filter
    const curriedFilter = curry((predicate, arr) => arr.filter(predicate))
    const isEven = x => x % 2 === 0
    const isPositive = x => x > 0
    
    const filterEvens    = curriedFilter(isEven)
    const filterPositive = curriedFilter(isPositive)
    
    console.log(filterEvens([1, 2, 3, 4, 5]))      // [2, 4]
    console.log(filterPositive([-2, -1, 0, 1, 2])) // [1, 2]
    
    // ===== ИМПЕРАТИВНЫЙ VS ФУНКЦИОНАЛЬНЫЙ =====
    console.log('\n=== Рефакторинг: императивный → функциональный ===')
    
    const employees = [
      { name: 'Алиса', dept: 'engineering', salary: 120000, senior: true },
      { name: 'Боб',   dept: 'marketing',   salary: 80000,  senior: false },
      { name: 'Карла', dept: 'engineering', salary: 150000, senior: true },
      { name: 'Денис', dept: 'engineering', salary: 95000,  senior: false },
      { name: 'Ева',   dept: 'marketing',   salary: 90000,  senior: true },
    ]
    
    // Императивно: средняя зарплата senior-инженеров
    function avgSeniorEngineerSalaryImperative(employees) {
      let total = 0
      let count = 0
      for (let i = 0; i < employees.length; i++) {
        const e = employees[i]
        if (e.dept === 'engineering' && e.senior) {
          total += e.salary
          count++
        }
      }
      return count > 0 ? total / count : 0
    }
    
    // Функционально: декларативно, без промежуточных переменных
    const avgSeniorEngineerSalaryFP = (employees) => {
      const salaries = employees
        .filter(e => e.dept === 'engineering' && e.senior)
        .map(e => e.salary)
    
      return salaries.length > 0
        ? salaries.reduce((sum, s) => sum + s, 0) / salaries.length
        : 0
    }
    
    const imp = avgSeniorEngineerSalaryImperative(employees)
    const fp  = avgSeniorEngineerSalaryFP(employees)
    console.log('Императивно:', imp)   // 135000
    console.log('Функционально:', fp)  // 135000

    Функциональное программирование в JS

    Краткий ответ

    Функциональное программирование (FP) — это стиль, при котором программа строится из чистых функций без побочных эффектов, данные не мутируются, а функции — объекты первого класса. В JS FP нативен: функции можно передавать как аргументы, возвращать из функций, хранить в переменных. Ключевые инструменты: map/filter/reduce, composе/pipe, каррирование, частичное применение.

    Полный разбор

    Чистые функции

    Чистая функция — детерминирована (одинаковый ввод = одинаковый вывод) и не имеет побочных эффектов:

    // НЕ чистая — читает внешнее состояние, имеет побочный эффект
    let tax = 0.2
    function calculatePrice(price) {
      console.log('Расчёт...')  // побочный эффект
      return price * (1 + tax)  // зависит от внешней переменной
    }
    
    // Чистая — предсказуема, изолирована
    function calculatePrice(price, taxRate) {
      return price * (1 + taxRate)
    }
    // calculatePrice(100, 0.2) всегда вернёт 120

    Иммутабельность

    Не мутируй входные данные — возвращай новые:

    // ПЛОХО — мутация аргумента
    function addUser(users, user) {
      users.push(user)  // мутируем оригинальный массив!
      return users
    }
    
    // ХОРОШО — возвращаем новый массив
    function addUser(users, user) {
      return [...users, user]
    }
    
    // Обновление объекта без мутации
    function updateAge(user, newAge) {
      return { ...user, age: newAge }
    }

    Функции высшего порядка

    Функция, принимающая или возвращающая другую функцию:

    // map, filter, reduce — встроенные функции высшего порядка
    const numbers = [1, 2, 3, 4, 5]
    
    const doubled = numbers.map(x => x * 2)           // [2, 4, 6, 8, 10]
    const evens = numbers.filter(x => x % 2 === 0)    // [2, 4]
    const sum = numbers.reduce((acc, x) => acc + x, 0) // 15
    
    // Собственная функция высшего порядка
    function repeat(n, fn) {
      return Array.from({ length: n }, (_, i) => fn(i))
    }
    repeat(3, i => i * i)  // [0, 1, 4]

    Композиция функций

    // compose: выполняет функции справа налево (математическая нотация)
    // compose(f, g, h)(x) === f(g(h(x)))
    function compose(...fns) {
      return (x) => fns.reduceRight((acc, fn) => fn(acc), x)
    }
    
    // pipe: выполняет функции слева направо (читается как поток данных)
    // pipe(f, g, h)(x) === h(g(f(x)))
    function pipe(...fns) {
      return (x) => fns.reduce((acc, fn) => fn(acc), x)
    }
    
    // Пример использования
    const trim = s => s.trim()
    const toLowerCase = s => s.toLowerCase()
    const addExclamation = s => s + '!'
    
    const process = pipe(trim, toLowerCase, addExclamation)
    process('  Hello World  ')  // 'hello world!'

    Каррирование vs частичное применение

    // Каррирование: функция с N аргументами → цепочка функций с 1 аргументом
    function curry(fn) {
      return function curried(...args) {
        if (args.length >= fn.length) {
          return fn.apply(this, args)
        }
        return function(...args2) {
          return curried.apply(this, args.concat(args2))
        }
      }
    }
    
    const add = curry((a, b, c) => a + b + c)
    add(1)(2)(3)   // 6
    add(1, 2)(3)   // 6
    add(1)(2, 3)   // 6
    
    // Частичное применение: фиксируем часть аргументов
    function partial(fn, ...presetArgs) {
      return function(...laterArgs) {
        return fn(...presetArgs, ...laterArgs)
      }
    }
    
    const multiply = (a, b) => a * b
    const double = partial(multiply, 2)
    double(5)   // 10
    double(10)  // 20

    Императивный vs функциональный стиль

    const orders = [
      { id: 1, amount: 150, status: 'completed', userId: 'u1' },
      { id: 2, amount: 80,  status: 'pending',   userId: 'u2' },
      { id: 3, amount: 300, status: 'completed', userId: 'u1' },
      { id: 4, amount: 50,  status: 'cancelled', userId: 'u3' },
    ]
    
    // Императивно: получить сумму завершённых заказов пользователя u1
    function getTotalImperative(orders, userId) {
      let total = 0
      for (let i = 0; i < orders.length; i++) {
        if (orders[i].userId === userId && orders[i].status === 'completed') {
          total += orders[i].amount
        }
      }
      return total
    }
    
    // Функционально: декларативно, читается как описание задачи
    const getTotalFunctional = (orders, userId) =>
      orders
        .filter(o => o.userId === userId && o.status === 'completed')
        .map(o => o.amount)
        .reduce((sum, amount) => sum + amount, 0)
    
    console.log(getTotalImperative(orders, 'u1'))   // 450
    console.log(getTotalFunctional(orders, 'u1'))   // 450

    Почему FP улучшает тестируемость

    Чистые функции тестируются без моков и setup:

    // Нечистая: требует mocking базы данных
    async function getUser(id) {
      return await db.query('SELECT * FROM users WHERE id = ?', [id])
    }
    
    // Чистая логика легко тестируется
    function formatUser(rawUser) {
      return {
        id: rawUser.id,
        name: rawUser.first_name + ' ' + rawUser.last_name,
        email: rawUser.email.toLowerCase()
      }
    }
    // test: formatUser({ id: 1, first_name: 'Ivan', last_name: 'Ivanov', email: 'Ivan@Example.COM' })
    // ожидаем: { id: 1, name: 'Ivan Ivanov', email: 'ivan@example.com' }

    Связанные уроки курса

  • Функции — основа FP, первоклассные объекты в JS
  • Методы массивов — map, filter, reduce как функции высшего порядка
  • Каррирование — детальный разбор с реализацией curry()
  • Как отвечать на собеседовании

    Начни с определений: «чистая функция», «иммутабельность», «функции первого класса». Покажи compose/pipe — это любимый вопрос на JS-собеседованиях. Объясни разницу между каррированием (f(a)(b)(c)) и частичным применением (partial(f, a)(b, c)). Подчеркни практическую ценность: FP-код легче тестировать, переиспользовать и рассуждать о нём.

    Красные флаги ответа

  • Путаница между каррированием и частичным применением — это разные вещи, хотя оба фиксируют аргументы
  • Мутация входных данных в «функциональных» примерах — нарушает принцип иммутабельности и делает код непредсказуемым
  • Незнание compose/pipe — базовые инструменты FP, без них нельзя строить функциональные пайплайны
  • Примеры

    Compose, pipe, partial application, curry и рефакторинг императивного кода в функциональный стиль

    // ===== COMPOSE И PIPE =====
    console.log('=== compose / pipe ===')
    
    function compose(...fns) {
      return (x) => fns.reduceRight((acc, fn) => fn(acc), x)
    }
    
    function pipe(...fns) {
      return (x) => fns.reduce((acc, fn) => fn(acc), x)
    }
    
    // Строковые трансформации
    const trim        = s => s.trim()
    const toLowerCase = s => s.toLowerCase()
    const capitalize  = s => s.charAt(0).toUpperCase() + s.slice(1)
    const addDot      = s => s.endsWith('.') ? s : s + '.'
    
    // pipe читается слева направо как поток
    const normalizeText = pipe(trim, toLowerCase, capitalize, addDot)
    console.log(normalizeText('  привет МИР  '))  // 'Привет мир.'
    console.log(normalizeText('HELLO'))            // 'Hello.'
    
    // compose читается справа налево (математически)
    const normalizeCompose = compose(addDot, capitalize, toLowerCase, trim)
    console.log(normalizeCompose('  HELLO  '))    // 'Hello.'
    
    // ===== ЧАСТИЧНОЕ ПРИМЕНЕНИЕ =====
    console.log('\n=== Частичное применение ===')
    
    function partial(fn, ...presetArgs) {
      return function(...laterArgs) {
        return fn(...presetArgs, ...laterArgs)
      }
    }
    
    // Базовые функции
    const multiply = (a, b) => a * b
    const add      = (a, b) => a + b
    const pow      = (base, exp) => Math.pow(base, exp)
    
    // Специализированные функции через partial
    const double   = partial(multiply, 2)
    const triple   = partial(multiply, 3)
    const add10    = partial(add, 10)
    const square   = partial(pow, undefined) // не подходит — нужен другой подход
    
    console.log(double(5))   // 10
    console.log(triple(5))   // 15
    console.log(add10(25))   // 35
    
    // Практичный пример: partial для fetch
    function fetchData(baseUrl, endpoint) {
      return `GET ${baseUrl}${endpoint}`  // упрощённо
    }
    
    const fetchFromAPI = partial(fetchData, 'https://api.example.com')
    console.log(fetchFromAPI('/users'))      // GET https://api.example.com/users
    console.log(fetchFromAPI('/products'))   // GET https://api.example.com/products
    
    // ===== КАРРИРОВАНИЕ =====
    console.log('\n=== Каррирование ===')
    
    function curry(fn) {
      return function curried(...args) {
        if (args.length >= fn.length) {
          return fn.apply(this, args)
        }
        return function(...args2) {
          return curried.apply(this, args.concat(args2))
        }
      }
    }
    
    const curriedAdd = curry((a, b, c) => a + b + c)
    console.log(curriedAdd(1)(2)(3))   // 6
    console.log(curriedAdd(1, 2)(3))   // 6
    console.log(curriedAdd(1)(2, 3))   // 6
    console.log(curriedAdd(1, 2, 3))   // 6
    
    // Практичный пример: каррированный filter
    const curriedFilter = curry((predicate, arr) => arr.filter(predicate))
    const isEven = x => x % 2 === 0
    const isPositive = x => x > 0
    
    const filterEvens    = curriedFilter(isEven)
    const filterPositive = curriedFilter(isPositive)
    
    console.log(filterEvens([1, 2, 3, 4, 5]))      // [2, 4]
    console.log(filterPositive([-2, -1, 0, 1, 2])) // [1, 2]
    
    // ===== ИМПЕРАТИВНЫЙ VS ФУНКЦИОНАЛЬНЫЙ =====
    console.log('\n=== Рефакторинг: императивный → функциональный ===')
    
    const employees = [
      { name: 'Алиса', dept: 'engineering', salary: 120000, senior: true },
      { name: 'Боб',   dept: 'marketing',   salary: 80000,  senior: false },
      { name: 'Карла', dept: 'engineering', salary: 150000, senior: true },
      { name: 'Денис', dept: 'engineering', salary: 95000,  senior: false },
      { name: 'Ева',   dept: 'marketing',   salary: 90000,  senior: true },
    ]
    
    // Императивно: средняя зарплата senior-инженеров
    function avgSeniorEngineerSalaryImperative(employees) {
      let total = 0
      let count = 0
      for (let i = 0; i < employees.length; i++) {
        const e = employees[i]
        if (e.dept === 'engineering' && e.senior) {
          total += e.salary
          count++
        }
      }
      return count > 0 ? total / count : 0
    }
    
    // Функционально: декларативно, без промежуточных переменных
    const avgSeniorEngineerSalaryFP = (employees) => {
      const salaries = employees
        .filter(e => e.dept === 'engineering' && e.senior)
        .map(e => e.salary)
    
      return salaries.length > 0
        ? salaries.reduce((sum, s) => sum + s, 0) / salaries.length
        : 0
    }
    
    const imp = avgSeniorEngineerSalaryImperative(employees)
    const fp  = avgSeniorEngineerSalaryFP(employees)
    console.log('Императивно:', imp)   // 135000
    console.log('Функционально:', fp)  // 135000

    Задание

    Реализуй функции pipe(...fns), partial(fn, ...args) и refactorToFP(orders) — рефакторинг императивного кода. pipe должен последовательно применять функции слева направо. partial должен частично применять аргументы. refactorToFP должен вернуть массив имён пользователей, чьи заказы на сумму более 100, отсортированных по алфавиту, без дублей.

    Подсказка

    pipe: используй fns.reduce((acc, fn) => fn(acc), x). partial: возвращай функцию, которая вызывает fn(...presetArgs, ...laterArgs). refactorToFP: цепочка .filter().map().filter((v, i, arr) => arr.indexOf(v) === i).sort()

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