Смотрю я как-то на эту статью и даже как-то печально, что отсутствует объяснение на счет экзотического вызова функции bind.
Давайте я расскажу чем отличается обычный вызов функции от экзотического вызова функции bind.
Смотрите, у обычной функции, которую вы обычно определяете в своем коде, при ее создании создаются для нее специфические поля [[Call]] (для любой функции) и [[Construct]] (только для обычных функций, функций-классов, функций созданных через конструктор Function). После создания, обычный вызов функции пользуется полем [[Call]], при создании экземпляра используется [[Construct]].
- Вызов функции:
a();
- Создание экземпляра:
new a();
История с функцией, которая создает функцию bind аналогичная, но есть одно но. Эта функция при создании определяет другое поведение для [[Call]] и [[Construct]]. Но помимо этих полей есть список дополнительных полей, которые содержат: функцию, которую мы на самом деле хотим вызвать, контекст для функции и список привязанных аргументов. Эти поля имеют следующие названия: [[BoundTargetFunction]], [[BoundThis]], [[BoundArguments]]. Поля [[Call]] и [[Construct]] обеспечивают взаимодействие между дополнительными полями создавая необходимый нам результат - привязка контекста к функции.
Как занимательный факт для тех кто не читает спецификацию: обычный вызов функции использует одно поле [[Call]]. Функция типа bind использует два поля [[Call]] для вызова. Первый [[Call]] используется в рамках обеспечения связи дополнительных полей, для создания корректного вызова функции с нужным контекстом. Второй вызов - это обычный вызов [[Call]], этот вызов инициирует экзотический [[Call]] функции bind. По сути происходит 2 вызова: первый от bind функции, а второй вызов делает сама функция bind. Поэтому я назвал вызов функции bind экзотическим.
И в завершение я хотел бы продемонстрировать реализацию bind функции средствами js. При написании этой процедуры я смотрел на спецификацию и постарался передать смысл реализации. Конечно реализовать 1 в 1 невозможно из-за наличия специфических внутренних полей таких как [[Call]] и [[Construct]] и ряда других особенностей. Поэтому я воспользовался Proxy объектом, который использует свои методы [[Call]] и [[Construct]] и таким образом через объект ловушек я смогу задать поведение [[Call]] и [[Construct]] как у bind метода. Стоит упомянуть чтобы корректно сшить вызовы ловушек с [[Call]] и [[Construct]] обычных вызовов я использовал Reflect API, который позволяет правильно переключиться на внутренние механизмы языка.
Пример носит академический характер и не принуждает им пользоваться (напоминаю что лучше пользоваться встроенным методом bind всвязи с его низкоуровневой реализацией)
function bindX(func, thisArg, ...args) {
if (typeof func !== "function") throw Error("function is not callable");
const bound = boundFunction(func, thisArg, args);
const length = Math.max(func.length - args.length, 0);
const name = (typeof func.name !== "string") ? "" : func.name;
Object.defineProperties(bound, {
length: {
value: length,
configurable: true,
},
name: {
value: `boundX ${name === "" ? "anonymous" : name}`,
configurable: true,
},
});
function boundFunction(targetFunction, boundThis, boundArgs) {
const proto = Object.getPrototypeOf(targetFunction);
const obj = function () {};
Object.setPrototypeOf(obj, proto);
Object.defineProperties(obj, {
"[[BoundTargetFunction]]": {
value: targetFunction,
},
"[[BoundThis]]": {
value: boundThis,
},
"[[BoundArguments]]": {
value: boundArgs,
},
});
return obj;
}
return new Proxy(bound, {
apply(target, thisArg, argumentsList) {
const targetObj = target["[[BoundTargetFunction]]"];
const boundThis = target["[[BoundThis]]"];
const boundArgs = target["[[BoundArguments]]"];
const args = [...boundArgs, ...argumentsList];
return Reflect.apply(targetObj, boundThis, args);
},
construct(target, argumentsList, newTarget) {
const targetObj = target["[[BoundTargetFunction]]"];
const boundArgs = target["[[BoundArguments]]"];
const args = [...boundArgs, ...argumentsList];
if (target === newTarget) newTarget = target;
return Reflect.construct(targetObj, args, newTarget);
},
});
}
Но можно сделать все проще, используя банальное замыкание. Этот пример не отображает спецификацию, но функциональность идентичная.
function bindX(func, thisArg, ...boundArgs){
if(typeof func !== "function") throw Error("func is not a callable");
const closure = function(...args){
if(new.target){
return Reflect.construct(func, [...boundArgs, ...args], new.target);
}
return Reflect.apply(func, thisArg, [...boundArgs, ...args])
}
Object.setPrototypeOf(closure, Object.getPrototypeOf(func));
return closure;
}