Как устроены декораторы в Typescript?
Декораторы - это механизм наблюдения, модификации или замены классов, методов или свойств в декларативном стиле. Декораторы были предложены в стандарте ECMAScript2016
и в настоящее время находятся на второй стадии (проект). В TypeScript их можно включить, установив флаг experimentalDecorators
компилятора. Рассмотрим, как компилятор TypeScript
преобразует декораторы в нативный JS-код
.
Сосредоточимся на трех самых распространенных видах декораторов — декораторах класса, декораторах методов и декораторах свойств. Начнем.
Декоратор класса
Декоратор класса - это функция, которая принимает конструктор класса в качестве единственного параметра. Если декоратор класса возвращает значение, оно заменит объявление класса предоставленной функцией-конструктором, т.е. переопределит конструктор; В противном случае объявление класса будет использовать оригинальный конструктор.
Рассмотрим пример из официальной документации:
@sealed class Greeter { } function sealed(constructor: Function) { Object.seal(constructor); Object.seal(constructor.prototype); }
Метод Object.seal()
запечатывает объект, предотвращая добавление новых свойств к нему и помечая все существующие свойства как неконфигурируемые.
Теперь рассмотрим транспилированный код, сгенерированный TypeScript
:
var Greeter = /** @class */ (function() { function Greeter(message) { this.greeting = message; } Greeter = __decorate([ sealed ], Greeter); return Greeter; }()); function sealed(constructor) { Object.seal(constructor); Object.seal(constructor.prototype); }
Во-первых, мы видим, что когда мы выбираем ES5
как целевую версию, TypeScript
изменяет декоратор класса на простую функцию, чтобы код работал во всех браузерах.
Затем мы видим новую функцию с именем __decorate
, которую создал TypeScript
. Первый параметр, который она принимает, - это массив декораторов. Дополнительные параметры могут варьироваться в зависимости от типа декоратора. В нашем случае функция вызывается с одним дополнительным параметром, который является конструктором класса. Рассмотрим __decorate
детальнее:
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; };
Классно и понятно, верно? Не волнуйтесь, это не так сложно, как кажется. Разберемся: Я упростил эту функцию немного, чтобы мы могли понять, что происходит.
Сначала она проверяет, поддерживается ли средой Reflect.decorate
. Если это так, то использует его; в противном случае переходит к своей собственной реализации - полифиллу, о котором мы сейчас расскажем:
var __decorate = function(decorators, target) { var argumentsLength = arguments.length, descriptorOrTarget, decorator; if (argumentsLength < 3) { // class decorator descriptorOrTarget = target; } for (var i = decorators.length - 1; i >= 0; i--) { if ((decorator = decorators[i])) { if (argumentsLength < 3) { // if the decorator function returns a value use it; otherwise use the original. descriptorOrTarget = decorator(descriptorOrTarget) || descriptorOrTarget; } } } return descriptorOrTarget; };
Из примера видно, что в этом случае, функция __decorate
принимает два параметра: массив декораторов и цель, которая может быть конструктором или прототипом класса.
Затем TypeScript
проверяет, передано ли менее трех аргументов; Если это так, то предполагается, что это декоратор класса, и переменной descriptorOrTarget
присваивается конструктор класса (в нашем случае Greeter
).
Массив декораторов перебирается в обратном порядке. Это потому, что при применении нескольких декораторов в одном объявлении их оценка аналогична композиции функций в математике. Например, в следующем коде:
@decoratorTwo @decoratorOne class Greeter { }
будет эквивалентно decoratorTwo(decoratorOne(Greeter))
Наконец, в случае декоратора класса функция декоратора вызывается с конструктором класса в качестве параметра. Если возвращается значение, оно заменяет объявление класса; в противном случае класс остается неизменным.
В нашем случае мы не заменяем исходный конструктор, но если мы захотим это сделать, мы легко можем добиться этого, расширив исходный конструктор с помощью выражения класса:
function classDecorator(constructor) { return class extends constructor { newProperty = "new property"; hello = "override"; } } @classDecorator class Greeter { hello: string; constructor(m: string) { this.hello = m; } }
var Greeter = /** @class */ (function() { function Greeter(message) { this.greeting = message; } Greeter = __decorate([ sealed ], Greeter); return Greeter; }()); function sealed(constructor) { Object.seal(constructor); Object.seal(constructor.prototype); }
Также стоит упомянуть, что из этого примера мы можем сделать вывод, что функция-декоратор вызывается ровно один раз во время выполнения программы и не зависит от количества экземпляров класса.
Декораторы методов класса
Метод-декоратор - это функция, которая принимает три параметра: цель - это либо функция-конструктор класса для статического члена, либо прототип класса для члена экземпляра; ключ - это имя метода; и дескриптор - это дескриптор свойства для метода.
Если декоратор возвращает значение, оно будет использоваться в качестве нового дескриптора свойства для метода; в противном случае останется исходный дескриптор.
Рассмотрим простой пример из официальной документации:
class Greeter { @enumerable greet() { return '...'; } } function enumerable(target, methodName, descriptor) { descriptor.enumerable = false; }
Мы изменяем свойство enumerable
и устанавливаем его в значение false
. Вот как выглядит транспилированная версия приведенного выше кода:
var Greeter = /** @class */ (function() { function Greeter() {} Greeter.prototype.greet = function() { return '...'; }; __decorate([ enumerable ], Greeter.prototype, "greet", null); return Greeter; }()); function enumerable(target, methodName, descriptor) { descriptor.enumerable = false; }
Функция декоратора метода принимает следующие параметры: прототип класса, имя метода и дескриптор метода, который в нашем примере равен null
.
Рассмотрим расширенную реализацию функции __decorate
и посмотрим, как она поддерживает декораторы методов:
var __decorate = function(decorators, target, key, desc) { var argumentsLength = arguments.length, descriptorOrTarget, decorator; if (argumentsLength < 3) { // class decorator descriptorOrTarget = target; } else { if (desc === null) { // method decorator descriptorOrTarget = Object.getOwnPropertyDescriptor(target, key); } } for (var i = decorators.length - 1; i >= 0; i--) { if ((decorator = decorators[i])) { if (argumentsLength < 3) { // if the decorator function returns a value use it; // otherwise use the original. descriptorOrTarget = decorator(descriptorOrTarget) || descriptorOrTarget; } else { // if the decorator function returns a descriptor use it; // otherwise use the original. descriptorOrTarget = decorator(target, key, descriptorOrTarget) || descriptorOrTarget; } } } if (argumentsLength > 3 && descriptorOrTarget) { Object.defineProperty(target, key, descriptorOrTarget); } return descriptorOrTarget; };
Если мы не имеем дело с декоратором класса (что указывается наличием 3 или более аргументов), мы проверяем, равен ли переданный дескриптор null
. Если это так, мы получаем ссылку на дескриптор свойства с помощью метода getOwnPropertyDescriptor
, передавая ему прототип класса и имя метода.
Затем мы вызываем функцию декоратора, передавая ей прототип класса, имя метода и дескриптор.
Поскольку у нас есть возможность вернуть либо новый дескриптор, либо изменить существующий, нам нужно проверить, возвращает ли функция декоратора значение; Если да, это значение будет использоваться в качестве нового дескриптора; В противном случае мы используем оригинальный.
Вот пример декоратора метода, который возвращает новый дескриптор:
function methodDecorator(target, propertyKey, descriptor) { return { ...descriptor, value: function() { console.log('...override the original function'); } }; }
Наконец, мы вызываем метод Object.defineProperty()
и устанавливаем полученный дескриптор в качестве нового дескриптора метода. Этот статический метод определяет новое свойство непосредственно в объекте или модифицирует существующее свойство в объекте, а затем возвращает этот объект.
Декораторы свойств
Декоратор свойства объявляется прямо перед объявлением поля класса. Это функция, которая принимает два параметра: цель — это либо функция-конструктор класса для статического члена, либо прототип класса. Второй параметр — это ключ, который представляет собой имя свойства.
Аналогично декораторам методов, если декоратор свойства возвращает значение, оно будет использоваться в качестве нового дескриптора свойства; в противном случае свойство остается неизменным.
Пример декорирования свойства:
class Greeter { @logProperty greeting = 'Hello'; } function logProperty(target, key) { let value; const getter = function() { console.log(`Get => ${key}`); return value; }; const setter = function(newVal) { console.log(`Set: ${key} => ${newVal}`); value = newVal; }; Object.defineProperty(target, key, { get: getter, set: setter, enumerable: true, configurable: true }); }
Функция logProperty
переопределяет декорированное свойство объекта. Мы можем определить новое свойство в прототипе конструктора, используя метод Object.defineProperty()
.Для перехвата значений в консоль используются геттер и сеттер.
Транслированная версия приведенного выше кода:
var Greeter = /** @class */ (function() { function Greeter() { this.greeting = 'Hello'; } __decorate([ logProperty ], Greeter.prototype, "greeting", void 0); return Greeter; }()); function logProperty(target, key) { ... }
Вызов __decorate()
аналогичен вызову, выполненному для декоратора метода, за исключением последнего параметра, который в этом случае установлен в void
0 (undefined)
, а не null
.
Таким образом, в этом случае, если мы повторно рассмотрим метод __decorate
:
var __decorate = function(decorators, target, key, desc) { var argumentsLength = arguments.length, descriptorOrTarget, decorator; if (argumentsLength < 3) { // class decorator descriptorOrTarget = target; } else { if (desc === null) { // method decorator descriptorOrTarget = Object.getOwnPropertyDescriptor(target, key); } } for (var i = decorators.length - 1; i >= 0; i--) { if ((decorator = decorators[i])) { if (argumentsLength < 3) { // if the decorator function returns a value use it; // otherwise use the original. descriptorOrTarget = decorator(descriptorOrTarget) || descriptorOrTarget; } else { // if the decorator function returns a descriptor use it; // otherwise use the original. descriptorOrTarget = decorator(target, key, descriptorOrTarget) || descriptorOrTarget; } } } if (argumentsLength > 3 && descriptorOrTarget) { Object.defineProperty(target, key, descriptorOrTarget); } return descriptorOrTarget; };
Теперь descriptorOrTarget
не присваивается значение до того, как мы перебираем функции-декораторы, и остаётся неопределенным в нашем примере, поскольку декоратор свойства logProperty
не возвращает новый дескриптор свойства.
В результате декоратор свойства logProperty
изменяет прототип класса, но дополнительных модификаций не происходит, поскольку условие if
в конце не выполняется.
В качестве альтернативы мы можем вернуть новый дескриптор свойства из нашей функции-декоратора:
function logProperty(target, key) { let value; const getter = function() { console.log(`Get => ${key}`); return value; }; const setter = function(newVal) { console.log(`Set: ${key} => ${newVal}`); value = newVal; }; return { get: getter, set: setter, enumerable: true, configurable: true } }
В этом случае дескриптор свойства, который мы возвращаем, будет использоваться в качестве третьего параметра при вызове метода defineProperty
. Результат использования обеих версий декоратора свойства logProperty
будет идентичным.
TypeScript
всегда определяет свойство в прототипе класса вместо экземпляра. Это поведение приводит к тому, что каждый экземпляр разделяет одно и то же значение:
Эту проблему можно обойти, создав уникальный ключ и добавив его к экземпляру, используя функции getter
и setter
. Например:
function logProperty(target, key) { const _key = Symbol(); const getter = function() { console.log(`Get => ${key}`); return this[_key]; }; const setter = function(newVal) { console.log(`Set: ${key} => ${newVal}`); this[_key] = newVal; }; return { get: getter, set: setter, enumerable: true, configurable: true }; }
Создаётся новый ключ, так как мы не можем использовать оригинальный ключ свойства, так как это приведет к бесконечному циклу. Теперь каждый экземпляр может иметь разное значение для этого свойства.
Это вольный перевод материала