Skip to content

Instantly share code, notes, and snippets.

@dSalieri
Last active April 18, 2023 06:44
Show Gist options
  • Save dSalieri/807a6076327d394d6b91842efeb66fce to your computer and use it in GitHub Desktop.
Save dSalieri/807a6076327d394d6b91842efeb66fce to your computer and use it in GitHub Desktop.
Как представлен bind-метод / примерная реализация метода bind

Смотрю я как-то на эту статью и даже как-то печально, что отсутствует объяснение на счет экзотического вызова функции 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;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment