var obj = новый CallableObject (); obj (аргументы);
Вызываемый объект - это структура данных, которая ведет себя и как объект, и как функция. Вы можете получать доступ и назначать свойства obj.bar
, вызывать методы obj.foo()
, а также напрямую вызывать объект obj()
, как если бы это была функция.
Прямой вызов подобен вызову метода obj
, который имеет доступ к свойствам объекта через свой this
контекст.
Если у вас есть опыт работы с Python, вы поймете, что существует встроенный протокол Python, использующий метод класса __call__
. К любому методу, назначенному __call__
, можно получить доступ как obj.__call__()
или как obj()
.
Вызываемые объекты также можно рассматривать как функции с отслеживанием состояния. Функции по своей сути представляют собой единичные процедуры без сохранения состояния. Вызываемые объекты - это созданные процедуры с отслеживанием состояния.
В JavaScript почти все является объектом, включая функции, поэтому мы, конечно, можем это сделать, но как? Он не встроен в такой язык, как Python, но есть несколько способов заставить его работать.
Соревнование
Вдохновленные Python, мы хотим создать класс / конструктор, который мы можем использовать для создания вызываемых объектов, которые перенаправляют их вызовы методу с именем _call
. Нам нужно это перенаправление, чтобы мы могли унаследовать его от нашего класса и легко переопределить и расширить метод _call
новыми функциями, не беспокоясь о внутренней работе вызываемого объекта.
Для этого нам нужно наследовать от конструктора Function, который наследуется от Object и позволяет нам создавать как объект, так и динамическую функцию.
Наше главное препятствие - дать объекту функции ссылку на самого себя.
Чтобы иметь ссылку на метод _call
, функциональная часть нашего объекта функции, сгенерированная нашим классом / конструктором Callable, должна иметь ссылку на себя.
Решения
Мы хотим создать расширяемый Callable
класс, который поддерживает правильное и правильное наследование в JavaScript и позволяет нам вызывать объекты, которые он конструирует, как функции со ссылкой на самих себя, перенаправляя эти вызовы на переопределяемый метод _call
.
Связанный путь
Попробуйте на repl.it
Поскольку мы наследуем от Function, мы можем создавать динамические функции из строк, используя super в нашем конструкторе. Таким образом, строка, которую мы передаем super
, будет телом нашей функции. Мы хотим, чтобы эта функция имела доступ к собственному объекту и вызывала метод _call
, передавая свои аргументы. Делаем это с помощью привязки.
Метод привязки позволит нам установить this
контекст функции на все, что мы захотим, заключив эту функцию в прозрачную связанную функцию. Итак, мы привязываем функцию к самой себе с помощью this.bind(this)
.
Теперь наш вызываемый объект имеет ссылку на себя, за исключением того, что объект, который мы возвращаем из нашего конструктора, возвращаемый this.bind
, является обернутой версией нашего исходного объекта. Таким образом, все наши свойства будут прикреплены к этому новому объекту функции в оболочке, и наша функция будет иметь ссылку на старый объект без оболочки, переданный для привязки.
Простое решение - прикрепить ссылку на новый обернутый объект на старом объекте как _bound
. А тело нашей функции в строке, переданной в super
, просто вызывает _call
, используя ссылку this._bound
.
Плюсы
- Не полагается на устаревшие или современные функции.
- Нет необходимости изменять прототипы.
Минусы
- Требуется упаковка объекта функции в связанную функцию.
Путь Callee
Попробуйте на repl.it
Мы снова используем вызов super
для создания динамической функции, но в этот раз мы получаем ссылку на саму функцию, используя преимущества другой неявной переменной внутри функции, объекта arguments.
У объекта arguments есть свойство arguments.callee, которое является ссылкой на вызываемую функцию. Мы используем эту ссылку в качестве первого аргумента для apply
, который связывает контекст функций this
с самим собой.
Итак, внутри тела функции, строки, переданной в super
, нам нужно только вызвать метод _call
на arguments.callee
.
Плюсы
- Очень простой.
- Нет необходимости изменять прототипы.
Минусы
arguments
иarguments.callee
недоступны в‘strict mode’
, подробнее см. MDN.- В современном коде следует избегать
arguments
для оператора спред / остаток.
Закрытие и путь прототипа
Попробуйте на repl.it
Здесь вместо создания динамической функции с super
мы отбрасываем объект функции, созданный конструктором (объект this
), и заменяем его закрытием, возвращая его вместо this
из constructor
.
Замыкание также является функциональным объектом и может ссылаться на себя в своем теле через переменную closed over closure
. Мы используем ссылку closure
для перенаправления вызовов его методу _call
.
Но мы разорвали цепочку прототипов, заменив this
на closure
, поэтому мы повторно присоединяем прототип конструктора к closure
, используя Object.setPrototypeOf и new.target (который является ссылкой на конструктор), чтобы получить прототип.
Вы можете использовать this.constructor.prototype
вместо new.target.prototype
, но тогда вы должны сначала вызвать super
, чтобы создать объект this
, что расточительно.
Плюсы
- Не требует обертывания возвращаемого объекта
Proxy
илиbind
.
Минусы
- Требуется доработка прототипов.
- Изменение прототипов происходит медленно и имеет другие побочные эффекты, см. MDN.
Прокси-путь
Попробуйте на repl.it
Используя Прокси, мы можем перехватывать вызовы функции, используя ловушку apply
, и перенаправлять ее на другую функцию. Ловушка apply
дает нашему вызываемому объекту ссылку на самого себя в качестве аргумента target
.
Итак, мы создаем класс, который наследуется от Function, Callable
, обертывая вызываемые объекты, созданные в Proxy
, перехватывая любые вызовы, сделанные к этим объектам, и перенаправляя их методу _call
на самом объекте, используя ссылку target
.
Плюсы
- Простой и понятный способ перехвата вызовов и их перенаправления.
- Нет необходимости изменять прототипы.
Минусы
- Требует обертывания объектов, созданных
Callable
, вProxy
. - Небольшое снижение производительности при использовании обработчиков
Proxy
.
Что мы узнали
JavaScript - чрезвычайно гибкий язык, и вы можете придать ему множество интересных форм, например вызываемые объекты. Для вызываемых объектов вполне могут быть варианты использования, но в целом я бы не рекомендовал их использовать, это не идиоматический JavaScript, и это может сбивать с толку или непонятно для кого-либо, использующего ваш код. В основном это было просто интересное упражнение.
Ссылки: