Перейти к содержимому

Function.prototype.bind polyfill

Реализация bind с нуля — классический вопрос на понимание this, замыканий и взаимодействия new с bound-функцией. Важнейший нюанс: new BoundFn() должен игнорировать привязанный this и создавать экземпляр оригинальной функции с правильным прототипом.

Полный полифилл с поддержкой new, корректным .length и .name:

/**
* Полифилл Function.prototype.bind.
* Поддерживает:
* 1. Привязку this и частичное применение аргументов (partial application).
* 2. new BoundFn() — создаёт экземпляр оригинальной функции (context игнорируется).
* 3. Корректный .length = max(0, fn.length - boundArgs.length).
* 4. Корректный .name = "bound " + fn.name.
*/
Function.prototype.bindPolyfill = function (context, ...boundArgs) {
if (typeof this !== 'function') {
throw new TypeError('bindPolyfill вызван на не-функции');
}
const originalFn = this;
function BoundFn(...callArgs) {
const allArgs = [...boundArgs, ...callArgs];
// new.target задан → вызов через new: context игнорируется
if (new.target) {
return new originalFn(...allArgs);
}
return originalFn.apply(context, allArgs);
}
// Прототип BoundFn → fn.prototype для корректного instanceof
if (originalFn.prototype) {
BoundFn.prototype = Object.create(originalFn.prototype);
}
// Корректный .length
Object.defineProperty(BoundFn, 'length', {
configurable: true,
value: Math.max(0, originalFn.length - boundArgs.length),
});
// Корректный .name
Object.defineProperty(BoundFn, 'name', {
configurable: true,
value: 'bound ' + (originalFn.name ?? ''),
});
return BoundFn;
};
// usage
function greet(greeting, punctuation) {
return greeting + ', ' + this.name + punctuation;
}
const obj = { name: 'Alice' };
const hello = greet.bindPolyfill(obj, 'Hello');
console.log(hello('!')); // "Hello, Alice!"
console.log(hello('?')); // "Hello, Alice?"

Тесты new-семантики, .length, .name, instanceof:

// ─── new-семантика ────────────────────────────────────────────────────────
function Person(name, age) {
this.name = name;
this.age = age;
}
const BoundPerson = Person.bindPolyfill({ name: 'ignored' }, 'Bob');
const p = new BoundPerson(30);
console.assert(p instanceof Person); // true — прототип сохранён
console.assert(p.name === 'Bob'); // аргумент от bind применён
console.assert(p.age === 30); // аргумент от вызова применён
// context 'ignored' проигнорирован при new ✓
// ─── .length ──────────────────────────────────────────────────────────────
function f(a, b, c) {}
const g = f.bindPolyfill(null, 1); // 1 предоставлен
console.assert(g.length === 2); // max(0, 3 - 1) = 2 ✓
const h = f.bindPolyfill(null, 1, 2, 3, 4); // больше чем arity
console.assert(h.length === 0); // max(0, 3 - 4) = 0 ✓
// ─── .name ────────────────────────────────────────────────────────────────
console.assert(g.name === 'bound f'); // "bound " + fn.name ✓
// ─── Частичное применение без context ────────────────────────────────────
const add = (a, b) => a + b;
const add10 = add.bindPolyfill(null, 10);
console.assert(add10(5) === 15);
// ─── instanceof через цепочку прототипов ─────────────────────────────────
console.assert(new BoundPerson(25) instanceof Person); // ✓

Сложность

  • Время: O(1) создание BoundFn; O(k) каждый вызов, k = boundArgs + callArgs
  • Память: O(k) — boundArgs хранятся в замыкании

Итог: bind полифилл: замыкание на context и boundArgs, new.target для new-семантики, Object.create для прототипа, корректные length и name.