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;};
// usagefunction 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); // больше чем arityconsole.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.