Сделать `obj.someMethod()` доступным как `obj()`, не препятствуя тому, чтобы `obj` вел себя как обычный экземпляр

Чтобы избежать недоразумений, давайте сначала договоримся о значении некоторых слов. Следующие значения не являются общепринятыми, я предлагаю их только в качестве контекста для этого вопроса.

  • функция -- экземпляр Function. С ним связана процедура.
  • object — экземпляр любого другого класса. Не имеет связанной с ним процедуры.
  • процедура — блок кода, который можно выполнить. В JS процедуры можно найти только в сочетании с функциями.
  • метод — функция, хранящаяся в свойстве другого объекта. Методы выполняются в контексте своих основных объектов.

В JavaScript обычно объекты настраиваются с помощью свойств и методов, а функции — нет. Другими словами, если вы видите это в коде:

foo.bar   // Accessing a property on an instance
foo.baz() // Calling a method on an instance

тогда foo почти наверняка является объектом, а не функцией.

Но технически функции ничем не отличаются от объектов (на самом деле функции в JS считаются объектами, это просто я различаю их для ясности).

И если вы сделаете это:

const foo = function () { /*...*/ }
foo()     // Calling the function's procedure
foo.bar   // Accessing a property on the function
foo.baz() // Calling a method on the function

... это будет работать нормально.

Я хотел бы ввести еще два термина, чтобы вы могли понять, что я имею в виду. Я не пытаюсь изменить здесь правила JavaScript, просто хочу, чтобы меня поняли.

  • function-object – функция, которая используется как объект: имеет свои собственные свойства и/или методы.
  • основной метод — процедура самого объекта-функции, в отличие от методов, хранящихся в качестве свойств объекта-функции.

Многие библиотеки JS предлагают объекты-функции, например jQuery и LoDash. т. е. вы можете сделать как $(), так и $.map().


Ладно, теперь к делу.

Я хотел бы иметь возможность создавать свои собственные объекты-функции со следующими требованиями:

  1. Я должен иметь возможность использовать объявление class ES2015 для описания класса моего объекта-функции. Или, по крайней мере, способ описания класса должен быть достаточно удобным. Императивное назначение свойств и методов функции неудобно.
  2. основной метод моего объекта-функции должен выполняться в том же контексте, что и другие его методы.
  3. основной метод и другие методы должны быть связаны с одним и тем же экземпляром. Например, если у меня есть основной метод и обычный метод, которые оба выполняют console.log(this.foo), то назначение myFunctionObject.foo должно изменить поведение как основного метода, так и другого метода.

Другими словами, я хочу иметь возможность сделать это:

class Foo {
  constructor   () { this.bar = 'Bar'; }
  mainMethod    () { console.log(this.bar); }
  anotherMethod () { console.log(this.bar); }
}

... но экземпляр Foo должен иметь foo.mainMethod(), доступный просто как foo().

И выполнение foo.bar = 'Quux' должно изменить поведение как foo(), так и foo.anotherMethod().

Может быть, есть способ добиться этого с помощью class Foo extends Function?


person Andrey Mikhaylov - lolmaus    schedule 11.10.2015    source источник


Ответы (2)


Если вы ничего не переопределите, new Foo вернет тип объекта, а не тип функции. Вы потенциально можете сделать

class Foo {
  constructor(){
    const foo = () => foo.mainMethod();
    Object.setPrototypeOf(foo, this);

    foo.bar = 'Bar';

    return foo;
  }
  mainMethod    () { console.log(this.bar); }
  anotherMethod () { console.log(this.bar); }
}

так что new Foo будет возвращать функцию, которая была явно настроена, чтобы она возвращала функцию с правильной конфигурацией цепочки прототипов. Обратите внимание, что я явно устанавливаю foo.bar вместо this.bar, чтобы hasOwnProperty по-прежнему работало, как и ожидалось, во всем коде.

Вы также можете обойти это, настроив его через базовый класс, например.

class CallableClass {
  constructor(callback){
      const fn = (...args) => callback(...args);
      Object.setPrototypeOf(fn, this);
      return fn;
  }
}

class Foo extends CallableClass {
  constructor(){
    super(() => this.mainMethod());
    this.bar = 'Bar';
  }
  mainMethod    () { console.log(this.bar); }
  anotherMethod () { console.log(this.bar); }
}

Я пишу это в реальном ES6, но вам придется изменить аргументы rest/spread на обычную функцию, если вы хотите, чтобы она работала в Node v4. например

const foo = function(){ callback.apply(undefined, arguments); };

Недостатком обоих этих подходов является то, что они полагаются на Object.setPrototypeOf, который не работает в IE‹=10, поэтому все будет зависеть от того, какая поддержка вам нужна. Общее понимание состоит в том, что Object.setPrototypeOf также очень медленный, поэтому я не уверен, что у вас есть масса вариантов.

Основной альтернативой, которую предоставляет ES6 для этого случая, будут прокси, но они пока не очень хорошо поддерживаются.

person loganfsmyth    schedule 11.10.2015
comment
(Недостаток обоих...) и влияние на производительность использования .setPrototypeOf(). - person Amit; 11.10.2015
comment
Я бы рекомендовал Object.setPrototypeOf(foo, Object.getPrototypeOf(this)), чтобы не у каждой функции был свой прототип. Или, точнее, Object.setPrototypeOf(foo, new.target.prototype). - person Bergi; 11.10.2015
comment
Когда я вызываю new Foo()(), он регистрирует undefined вместо Bar. - person ; 12.10.2015
comment
@torazaburo Совершенно верно, моя ошибка в том, что я что-то изменил, не протестировав это :) Было this.mainMethod() вместо foo.mainMethod() в функции стрелки в первом примере. - person loganfsmyth; 12.10.2015

Насколько я знаю, нет способа добавить метод [[Call]] к существующему объекту.

Прокси-серверы могут показаться потенциальным решением, но проблема заключается в том, что прокси-объект — это объект, отличный от [[ProxyHandler]], и что [[ProxyHandler]] уже должен иметь метод [[Call]], чтобы иметь возможность установить ловушку "apply".

Следовательно, единственное жизнеспособное решение — создать новую функцию с желаемым [[Call]], заменить ее [[Prototype]] и заставить конструктор возвращать эту функцию вместо «настоящего» экземпляра.

class Foo {
  constructor   () {
    var instance = function(){ return instance.mainMethod(); };
    Object.setPrototypeOf(instance, Foo.prototype);
    return Object.assign(instance, {bar: 'Bar'});
  }
  mainMethod    () { console.log(this.bar); }
  anotherMethod () { console.log(this.bar); }
}
person Oriol    schedule 11.10.2015
comment
Хм, мне нужно было обновить страницу, прежде чем отвечать, я не видел, чтобы похожий ответ появился после того, как я открыл эту страницу несколько часов назад. Ну, я оставлю свой, потому что другой не работает. - person Oriol; 12.10.2015