Как устроены декораторы в 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
};
}Создаётся новый ключ, так как мы не можем использовать оригинальный ключ свойства, так как это приведет к бесконечному циклу. Теперь каждый экземпляр может иметь разное значение для этого свойства.
Это вольный перевод материала