Связанная через .bind() функция имеет свойство, которое отличает её от обычных: this внутри не поддаётся переопределению. Никакие последующие .call(), .apply(), повторные .bind() не меняют контекст, зафиксированный при первом связывании. Именно поэтому такую привязку называют жёсткой (hard binding).

.call() и .apply() не меняют bound-функцию

function whoAmI() { return this.name; }

const boundToAlice = whoAmI.bind({ name: 'Аня' });

boundToAlice();                              // 'Аня'
boundToAlice.call({ name: 'Борис' });        // всё равно 'Аня'
boundToAlice.apply({ name: 'Дима' });        // всё равно 'Аня'

// повторный bind тоже не работает
const rebound = boundToAlice.bind({ name: 'Ева' });
rebound();                                    // всё равно 'Аня'

Связанная функция игнорирует новые попытки переопределить контекст. Это гарантия: если ты дал кому-то bound-функцию, она гарантированно будет работать с исходным контекстом, независимо от того, как её позовут.

Свойство удобное для контрактных API: библиотека может выдать обработчик, зная, что вызвавший его код не сможет случайно (или намеренно) сменить контекст выполнения.

Единственное исключение: new

Есть один способ обойти жёсткую привязку — вызвать bound-функцию через new:

function Point(x, y) {
  this.x = x;
  this.y = y;
}

const boundPoint = Point.bind({ x: 100, y: 200 });

// как обычный вызов — this = объект из bind
boundPoint(1, 2);   // ничего не возвращает, но this внутри был бы { x: 100, y: 200 }

// через new — this = НОВЫЙ объект, привязка игнорируется
const p = new boundPoint(1, 2);
console.log(p.x);   // 1 (не 100)
console.log(p.y);   // 2 (не 200)
console.log(p instanceof Point);   // true

Логика проста: new в любом случае должен создать новый объект. Если бы new уважал bound-контекст, свойство «конструктор всегда возвращает новый экземпляр» сломалось бы. Это компромисс дизайна языка: гарантия new сильнее гарантии .bind().

На практике это редкий случай — вызывать через new связанную функцию мало кто задумывается. Но знать про эту дыру полезно для написания надёжного кода: если абсолютно важна привязка, лучше вернуть закрытую arrow-функцию, которую точно нельзя вызвать через new.

Полифилл bind — как это работает под капотом

Метод .bind() появился в ES5 (2009). До его массового распространения (в основном из-за IE) писали полифилы. Учебная упрощённая версия помогает понять, что .bind() делает:

Function.prototype.myBind = function(context, ...boundArgs) {
  const originalFn = this;

  return function(...callArgs) {
    // если вызвали через new, this — новый объект, игнорируем context
    if (this instanceof originalFn) {
      return originalFn.apply(this, [...boundArgs, ...callArgs]);
    }
    // обычный вызов — используем связанный context
    return originalFn.apply(context, [...boundArgs, ...callArgs]);
  };
};

Проверка this instanceof originalFn отвечает как раз за случай с new. Реальный .bind() в V8 использует внутренние оптимизации и делает то же чуть точнее (например, правильно устанавливает .prototype-цепочку), но идея та же.

Практический вывод

Жёсткая привязка полезна там, где важна гарантия контекста:

  • Event handlers, где хочется быть уверенным, что метод класса вызовется с правильным this
  • Callbacks, передаваемые в сторонние библиотеки, где не контролируешь, как они будут вызваны
  • API, возвращающие функции с прикреплённым состоянием (partial application)

Не полагайся на неё, если функцию потенциально могут вызвать через new — для этого лучше вернуть arrow-функцию или проверить new.target внутри самой функции.