Правене на `obj.someMethod()` достъпен като `obj()`, без да се пречи на `obj` да се държи като нормален екземпляр

За да избегнем недоразумения, нека първо се споразумеем за значението на определени думи. Следните значения не са общоприети, аз само ги предлагам като контекст за този въпрос.

  • функция -- екземпляр на Function. Той има процедура, свързана с него.
  • обект -- екземпляр на всеки друг клас. Няма процедура, свързана с него.
  • процедура -- блок от код, който може да бъде изпълнен. В 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 тук, просто искам да бъда разбран.

  • функционален обект -- функция, която се използва като обект: има свои собствени свойства и/или методи.
  • основен метод -- процедурата на самия функционален обект, за разлика от методите, съхранени като свойства на функционален обект.

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


Добре, сега към въпроса.

Бих искал да мога да създавам свои собствени функционални обекти със следните изисквания:

  1. Трябва да мога да използвам декларацията ES2015 class, за да опиша класа на моя функционален обект. Или поне начинът на описване на класа трябва да е достатъчно удобен. Императивното присвояване на свойства и методи на функция не е удобно.
  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

AFAIK няма начин да добавите [[Call]] метод към съществуващ обект.

Прокситата може да изглеждат потенциално решение, но проблемите са, че прокси обектът е различен обект от [[ProxyHandler]] и че [[ProxyHandler]] трябва вече да има метод [[Call]], за да може да зададе "apply" trap.

Следователно единственото жизнеспособно решение е създаването на нова функция с желаната [[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