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

bind, call, apply

Реальная проблема: this теряется при передаче метода

В классовом React-компоненте вы передаёте метод в обработчик события: <button onClick={this.handleClick}>. Но при вызове this теряется — метод «отцепляется» от объекта. Решение: bind. В старом коде вы встретите call и apply — они позволяют вызвать функцию с явно указанным this.

Что решают call, apply, bind

Все три метода управляют значением this при вызове функции. Разница в том, когда и как:

  • call — вызывает сейчас, аргументы перечислены
  • apply — вызывает сейчас, аргументы массивом
  • bind — создаёт новую функцию с привязанным this, вызов позже
  • На основе предыдущих уроков

  • «this» — как this определяется при вызове, потеря this
  • «Функции» — функции как объекты с методами
  • «Стрелочные функции» — стрелочные функции нельзя привязать (bind игнорируется)
  • call — вызов с явным this

    function greet(greeting, punctuation) {
      return `${greeting}, ${this.name}${punctuation}`
    }
    
    const user = { name: 'Алексей' }
    
    // Первый аргумент — this, остальные — аргументы функции
    greet.call(user, 'Привет', '!')   // 'Привет, Алексей!'
    greet.call(user, 'Hello', '.')    // 'Hello, Алексей.'

    apply — то же, но аргументы массивом

    greet.apply(user, ['Привет', '!'])  // 'Привет, Алексей!'
    
    // Практический кейс: Math.max с массивом (до появления spread)
    const nums = [3, 1, 4, 1, 5, 9]
    Math.max.apply(null, nums)  // 9
    // Сейчас лучше: Math.max(...nums)

    bind — создаёт функцию с привязанным this

    const boundGreet = greet.bind(user, 'Привет')  // привязали this И первый аргумент
    boundGreet('!')   // 'Привет, Алексей!'
    boundGreet('...')  // 'Привет, Алексей...'
    
    // Классический React-паттерн:
    class Button {
      constructor(label) {
        this.label = label
        this.handleClick = this.handleClick.bind(this)  // привязка в конструкторе
      }
      handleClick() {
        console.log(`Нажата: ${this.label}`)
      }
    }

    Частичное применение (partial application)

    bind позволяет «зафиксировать» часть аргументов:

    function multiply(a, b) {
      return a * b
    }
    
    const double = multiply.bind(null, 2)  // a = 2, b — потом
    const triple = multiply.bind(null, 3)
    
    console.log(double(5))   // 10
    console.log(triple(5))   // 15
    console.log(double(10))  // 20
    
    // Полезный пример — логгер с уровнем:
    function log(level, message) {
      console.log(`[${level}] ${message}`)
    }
    
    const info  = log.bind(null, 'INFO')
    const error = log.bind(null, 'ERROR')
    const warn  = log.bind(null, 'WARN')
    
    info('Запрос получен')   // [INFO] Запрос получен
    error('База недоступна') // [ERROR] База недоступна

    Заимствование методов

    call/apply позволяют использовать методы одного объекта для другого:

    // Массивоподобный объект — есть length и индексы, но нет методов Array
    const nodeList = { 0: 'a', 1: 'b', 2: 'c', length: 3 }
    
    // Заимствуем slice у Array.prototype:
    const arr = Array.prototype.slice.call(nodeList)
    console.log(arr)  // ['a', 'b', 'c']
    
    // Современный способ:
    const arr2 = Array.from(nodeList)  // лучше

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

    Ошибка 1: bind стрелочных функций

    // Стрелочные функции нельзя привязать — bind игнорируется:
    const arrow = () => this
    const bound = arrow.bind({ name: 'Тест' })
    bound()  // this всё равно из внешнего контекста, не { name: 'Тест' }
    
    // Bind работает только с обычными функциями и методами классов

    Ошибка 2: лишний вызов bind в render

    // Сломано (React) — создаётся новая функция при каждом рендере:
    render() {
      return <button onClick={this.handleClick.bind(this)}>
    }
    
    // Исправлено — привязать в конструкторе или использовать стрелочный метод:
    class MyComponent {
      handleClick = () => { ... }  // стрелочный метод — this привязан автоматически
    }

    Ошибка 3: call с неправильным this

    // Если первый аргумент null или undefined в strict mode — this = undefined
    function show() { console.log(this) }
    show.call(null)   // null (или global в sloppy mode)
    show.call(undefined)  // undefined (или global)

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

  • React классы: this.method = this.method.bind(this) в конструкторе
  • Event listeners: element.addEventListener('click', handler.bind(this))
  • Утилиты: const boundLog = console.log.bind(console) — чтобы передавать как коллбэк
  • Тестирование: spy.call(context, args) в тестах с моками
  • setTimeout: setTimeout(this.update.bind(this), 1000) — сохранить контекст
  • Примеры

    Управление this в системе логирования

    // Система логирования — демонстрация call, apply, bind
    const logger = {
      prefix: '[App]',
      level: 'INFO',
    
      log(message, ...extra) {
        const timestamp = new Date().toISOString().slice(11, 19)
        const extras = extra.length ? ' ' + JSON.stringify(extra) : ''
        console.log(`${this.prefix} [${this.level}] ${timestamp}: ${message}${extras}`)
      }
    }
    
    const dbLogger  = { prefix: '[DB]',  level: 'DEBUG' }
    const authLogger = { prefix: '[Auth]', level: 'WARN'  }
    
    // call — вызвать чужой метод с другим this
    logger.log.call(dbLogger, 'Подключение к базе')
    // [DB] [DEBUG] 10:30:45: Подключение к базе
    
    // apply — то же, аргументы массивом
    const args = ['Ошибка токена', { userId: 42, reason: 'expired' }]
    logger.log.apply(authLogger, args)
    // [Auth] [WARN] 10:30:45: Ошибка токена [{"userId":42,"reason":"expired"}]
    
    // bind — создать специализированный логгер
    const errorLogger = { prefix: '[Error]', level: 'ERROR' }
    const logError = logger.log.bind(errorLogger)
    
    logError('Сервер недоступен')  // [Error] [ERROR] 10:30:45: Сервер недоступен
    logError('Таймаут', { ms: 5000 })  // [Error] [ERROR] 10:30:45: Таймаут [{"ms":5000}]
    
    // Частичное применение — зафиксировать уровень
    function createTaggedLog(level, message, ...extra) {
      console.log(`[${level}] ${message}`, ...extra)
    }
    
    const debugLog = createTaggedLog.bind(null, 'DEBUG')
    const errorLog = createTaggedLog.bind(null, 'ERROR')
    
    debugLog('Загрузка модуля', { name: 'auth' })
    // [DEBUG] Загрузка модуля { name: 'auth' }
    errorLog('Не удалось сохранить')
    // [ERROR] Не удалось сохранить
    
    // Заимствование: взять toString у Array для объекта
    const cart = { 0: 'Ноутбук', 1: 'Мышь', 2: 'Коврик', length: 3 }
    const items = Array.prototype.slice.call(cart)
    console.log(items)  // ['Ноутбук', 'Мышь', 'Коврик']
    console.log(items.join(', '))  // 'Ноутбук, Мышь, Коврик'

    bind, call, apply

    Реальная проблема: this теряется при передаче метода

    В классовом React-компоненте вы передаёте метод в обработчик события: <button onClick={this.handleClick}>. Но при вызове this теряется — метод «отцепляется» от объекта. Решение: bind. В старом коде вы встретите call и apply — они позволяют вызвать функцию с явно указанным this.

    Что решают call, apply, bind

    Все три метода управляют значением this при вызове функции. Разница в том, когда и как:

  • call — вызывает сейчас, аргументы перечислены
  • apply — вызывает сейчас, аргументы массивом
  • bind — создаёт новую функцию с привязанным this, вызов позже
  • На основе предыдущих уроков

  • «this» — как this определяется при вызове, потеря this
  • «Функции» — функции как объекты с методами
  • «Стрелочные функции» — стрелочные функции нельзя привязать (bind игнорируется)
  • call — вызов с явным this

    function greet(greeting, punctuation) {
      return `${greeting}, ${this.name}${punctuation}`
    }
    
    const user = { name: 'Алексей' }
    
    // Первый аргумент — this, остальные — аргументы функции
    greet.call(user, 'Привет', '!')   // 'Привет, Алексей!'
    greet.call(user, 'Hello', '.')    // 'Hello, Алексей.'

    apply — то же, но аргументы массивом

    greet.apply(user, ['Привет', '!'])  // 'Привет, Алексей!'
    
    // Практический кейс: Math.max с массивом (до появления spread)
    const nums = [3, 1, 4, 1, 5, 9]
    Math.max.apply(null, nums)  // 9
    // Сейчас лучше: Math.max(...nums)

    bind — создаёт функцию с привязанным this

    const boundGreet = greet.bind(user, 'Привет')  // привязали this И первый аргумент
    boundGreet('!')   // 'Привет, Алексей!'
    boundGreet('...')  // 'Привет, Алексей...'
    
    // Классический React-паттерн:
    class Button {
      constructor(label) {
        this.label = label
        this.handleClick = this.handleClick.bind(this)  // привязка в конструкторе
      }
      handleClick() {
        console.log(`Нажата: ${this.label}`)
      }
    }

    Частичное применение (partial application)

    bind позволяет «зафиксировать» часть аргументов:

    function multiply(a, b) {
      return a * b
    }
    
    const double = multiply.bind(null, 2)  // a = 2, b — потом
    const triple = multiply.bind(null, 3)
    
    console.log(double(5))   // 10
    console.log(triple(5))   // 15
    console.log(double(10))  // 20
    
    // Полезный пример — логгер с уровнем:
    function log(level, message) {
      console.log(`[${level}] ${message}`)
    }
    
    const info  = log.bind(null, 'INFO')
    const error = log.bind(null, 'ERROR')
    const warn  = log.bind(null, 'WARN')
    
    info('Запрос получен')   // [INFO] Запрос получен
    error('База недоступна') // [ERROR] База недоступна

    Заимствование методов

    call/apply позволяют использовать методы одного объекта для другого:

    // Массивоподобный объект — есть length и индексы, но нет методов Array
    const nodeList = { 0: 'a', 1: 'b', 2: 'c', length: 3 }
    
    // Заимствуем slice у Array.prototype:
    const arr = Array.prototype.slice.call(nodeList)
    console.log(arr)  // ['a', 'b', 'c']
    
    // Современный способ:
    const arr2 = Array.from(nodeList)  // лучше

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

    Ошибка 1: bind стрелочных функций

    // Стрелочные функции нельзя привязать — bind игнорируется:
    const arrow = () => this
    const bound = arrow.bind({ name: 'Тест' })
    bound()  // this всё равно из внешнего контекста, не { name: 'Тест' }
    
    // Bind работает только с обычными функциями и методами классов

    Ошибка 2: лишний вызов bind в render

    // Сломано (React) — создаётся новая функция при каждом рендере:
    render() {
      return <button onClick={this.handleClick.bind(this)}>
    }
    
    // Исправлено — привязать в конструкторе или использовать стрелочный метод:
    class MyComponent {
      handleClick = () => { ... }  // стрелочный метод — this привязан автоматически
    }

    Ошибка 3: call с неправильным this

    // Если первый аргумент null или undefined в strict mode — this = undefined
    function show() { console.log(this) }
    show.call(null)   // null (или global в sloppy mode)
    show.call(undefined)  // undefined (или global)

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

  • React классы: this.method = this.method.bind(this) в конструкторе
  • Event listeners: element.addEventListener('click', handler.bind(this))
  • Утилиты: const boundLog = console.log.bind(console) — чтобы передавать как коллбэк
  • Тестирование: spy.call(context, args) в тестах с моками
  • setTimeout: setTimeout(this.update.bind(this), 1000) — сохранить контекст
  • Примеры

    Управление this в системе логирования

    // Система логирования — демонстрация call, apply, bind
    const logger = {
      prefix: '[App]',
      level: 'INFO',
    
      log(message, ...extra) {
        const timestamp = new Date().toISOString().slice(11, 19)
        const extras = extra.length ? ' ' + JSON.stringify(extra) : ''
        console.log(`${this.prefix} [${this.level}] ${timestamp}: ${message}${extras}`)
      }
    }
    
    const dbLogger  = { prefix: '[DB]',  level: 'DEBUG' }
    const authLogger = { prefix: '[Auth]', level: 'WARN'  }
    
    // call — вызвать чужой метод с другим this
    logger.log.call(dbLogger, 'Подключение к базе')
    // [DB] [DEBUG] 10:30:45: Подключение к базе
    
    // apply — то же, аргументы массивом
    const args = ['Ошибка токена', { userId: 42, reason: 'expired' }]
    logger.log.apply(authLogger, args)
    // [Auth] [WARN] 10:30:45: Ошибка токена [{"userId":42,"reason":"expired"}]
    
    // bind — создать специализированный логгер
    const errorLogger = { prefix: '[Error]', level: 'ERROR' }
    const logError = logger.log.bind(errorLogger)
    
    logError('Сервер недоступен')  // [Error] [ERROR] 10:30:45: Сервер недоступен
    logError('Таймаут', { ms: 5000 })  // [Error] [ERROR] 10:30:45: Таймаут [{"ms":5000}]
    
    // Частичное применение — зафиксировать уровень
    function createTaggedLog(level, message, ...extra) {
      console.log(`[${level}] ${message}`, ...extra)
    }
    
    const debugLog = createTaggedLog.bind(null, 'DEBUG')
    const errorLog = createTaggedLog.bind(null, 'ERROR')
    
    debugLog('Загрузка модуля', { name: 'auth' })
    // [DEBUG] Загрузка модуля { name: 'auth' }
    errorLog('Не удалось сохранить')
    // [ERROR] Не удалось сохранить
    
    // Заимствование: взять toString у Array для объекта
    const cart = { 0: 'Ноутбук', 1: 'Мышь', 2: 'Коврик', length: 3 }
    const items = Array.prototype.slice.call(cart)
    console.log(items)  // ['Ноутбук', 'Мышь', 'Коврик']
    console.log(items.join(', '))  // 'Ноутбук, Мышь, Коврик'

    Задание

    В интернет-магазине есть объект formatter с методами formatPrice(price, currency) и formatCount(n, unit). Методы используют this.locale для локализации. Создай специализированные функции: formatRub (форматирует цену в рублях для locale 'ru-RU'), formatUsd (в долларах для 'en-US'), formatItems (количество со словом 'шт'). Используй bind для привязки this и частичного применения аргументов.

    Подсказка

    formatRub = formatter.formatPrice.bind(ruFormatter, — нет, нужно bind только this и добавить currency как второй аргумент частично). Попробуй: formatter.formatPrice.bind(ruFormatter) и вызов formatRub(price, "RUB"). Или частично: bind(ruFormatter, 1499.99) нет — bind(ruFormatter) создаёт функцию, которой передаёшь price и "RUB". Для одного аргумента: bind(usFormatter, — нет. Используй bind(context) для this, потом фиксируй аргументы.

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