本文深入解析JavaScript中的装饰者模式,阐述其通过包装链动态扩展对象职责的结构型特性。重点介绍TypeScript中装饰器的语法规范、配置方法及类、方法、属性等类型的装饰器用法。详细分析装饰器工厂的执行顺序,并通过TypeScript编译器生成的源码,揭示__decorate函数的内部实现机制与参数传递逻辑。
装饰者模式介绍
装饰者模式(Decorator Pattern),亦称装饰器模式,是一种结构型设计模式。它允许在不修改原有对象代码的前提下,动态地为其添加新的职责或行为。
该模式的主要优势在于将对象的核心功能与附加的装饰功能解耦。这种非侵入式的修改方式,使得功能扩展更加灵活且不影响原有逻辑。
以图像处理为例,一个普通女孩可以通过美颜滤镜获得惊艳效果。通过叠加瘦脸、大眼、磨皮等装饰功能,最终呈现出完美的照片。
经过多重装饰后,人物的本质并未改变,但视觉效果得到了显著提升。若需不同风格,只需更换或组合不同的装饰功能即可,如同“百变星君”般灵活多变。
在代码层面,可以将每个装饰功能抽象为一个独立的类来实现。
// 女孩子
class Girl {
faceValue() {
console.log('我原本的脸')
}
}
class ThinFace {
constructor(girl) {
this.girl = girl;
}
faceValue() {
this.girl.faceValue();
console.log('开启瘦脸')
}
}
class IncreasingEyes {
constructor(girl) {
this.girl = girl;
}
faceValue() {
this.girl.faceValue();
console.log('增大眼睛')
}
}
let girl = new Girl();
girl = new ThinFace(girl);
girl = new IncreasingEyes(girl);
// 闪瞎你的眼
girl.faceValue(); //
从代码结构看,装饰者模式通过将一个对象嵌入另一个对象中,形成一条包装链。调用时,请求沿着链条传递,每个包装对象都有机会处理或增强该请求。
这种机制在增加或移除装饰功能时具有极高的灵活性。例如,若想展示真实面貌,只需移除“瘦脸”包装,其他功能不受影响;若需增加“磨皮”,只需添加对应功能类并继续装饰,各功能可并存运行。
在JavaScript中,使用类来实现装饰者模式可能显得较为笨重。鉴于JavaScript的灵活性,通常直接使用对象来实现装饰功能。
let girl = {
faceValue() {
console.log('我原本的脸')
}
}
function thinFace() {
console.log('开启瘦脸')
}
function IncreasingEyes() {
console.log('增大眼睛')
}
girl.faceValue = function(){
const originalFaveValue = girl.faceValue; // 原来的功能
return function() {
originalFaveValue.call(girl);
thinFace.call(girl);
}
}()
girl.faceValue = function(){
const originalFaveValue = girl.faceValue; // 原来的功能
return function() {
originalFaveValue.call(girl);
IncreasingEyes.call(girl);
}
}()
girl.faceValue();
具体做法是在不修改原代码的基础上,先保存原始函数,然后重新定义该函数。在重写的函数内部调用之前保存的原始函数,从而实现功能的增强。
通过图示可以更直观地理解装饰者模式的原理。

从图中可见,通过层层包装,原有的对象功能被逐步增强,实现了功能的动态扩展。
TypeScript中的装饰器
TypeScript中的装饰器采用 @expression 语法,其中 expression 求值后必须是一个函数。该函数在运行时被调用,被装饰声明的元数据将作为参数传入。
目前,JavaScript规范中的装饰器提案尚处于第二阶段(Stage 2),这意味着原生浏览器环境暂不支持直接使用,需借助编译工具。
开发者可以使用Babel或TypeScript编译器,在编译阶段将装饰器语法转换为浏览器可执行的常规代码(后续将分析编译后的源码)。
本节主要讨论TypeScript中装饰器的具体使用方式。
TypeScript装饰器可应用于类声明、方法、访问器(getter/setter)、属性以及参数上。
启用装饰器支持,需在命令行编译时添加标志:
tsc --target ES5 --experimentalDecorators test.ts
或者在配置文件 tsconfig.json 中配置:
{
"compilerOptions": {
"target": "ES5",
"experimentalDecorators": true
}
}
装饰器的使用
装饰器本质上是一个函数,使用时需在其前添加 @ 符号,并置于被装饰声明之前。多个装饰器可同时作用于一个声明,可写在一行或换行书写:
// 换行写
@test1
@test2
declaration
//写一行
@test1 @test2 ...
declaration
定义 face.ts 文件:
function thinFace() {
console.log('开启瘦脸')
}
@thinFace
class Girl {
}
编译生成的JS代码在运行时,会直接调用 thinFace 函数。由于该装饰器作用于类,因此被称为类装饰器。
若需附加多个功能,可组合使用多个装饰器:
function thinFace() {
console.log('开启瘦脸')
}
function IncreasingEyes() {
console.log('增大眼睛')
}
@thinFace
@IncreasingEyes
class Girl {
}
当多个装饰器组合使用时,需注意其调用顺序是从下至上依次执行,这与代码书写顺序相反。示例中的运行结果如下:
'增大眼睛'
'开启瘦脸'
若需在一个装饰器中为类添加属性,并在其他装饰器中使用,该属性定义应放在最后一个装饰器中,因为最后一个装饰器最先执行。
装饰器工厂
有时需要为装饰器传递参数,此时需借助装饰器工厂函数。工厂函数是一个高阶函数,调用后返回一个函数,返回的函数作为实际的装饰器函数。
function thinFace(value: string){
console.log('1-瘦脸工厂方法')
return function(){
console.log(`4-我是瘦脸的装饰器,要瘦脸${value}`)
}
}
function IncreasingEyes(value: string) {
console.log('2-增大眼睛工厂方法')
return function(){
console.log(`3-我是增大眼睛的装饰器,要${value}`)
}
}
@thinFace('50%')
@IncreasingEyes('增大一倍')
class Girl {
}
@ 符号后为工厂函数的调用,执行顺序从上到下,目的是获取最终的装饰器函数。装饰器函数的实际执行顺序依然遵循从下到上的规则。
运行结果如下:
1-瘦脸工厂方法
2-增大眼睛工厂方法
3-我是增大眼睛的装饰器,要增大一倍
4-我是瘦脸的装饰器,要瘦脸50%
总结执行流程:
- 工厂函数从上到下依次执行,用于获取装饰器函数。
- 装饰器函数的实际执行顺序是从下到上依次执行。
类装饰器
作用于类声明的装饰器,提供了修改类的机会。执行装饰器函数时,类的构造函数将作为参数传递给装饰器。
function classDecorator(value: string){
return function(constructor){
console.log('接收一个构造函数')
}
}
function thinFace(constructor){
constructor.prototype.thinFaceFeature = function() {
console.log('瘦脸功能')
}
}
@thinFace
@classDecorator('类装饰器')
class Girl {}
let g = new Girl();
g.thinFaceFeature(); // '瘦脸功能'
在上述示例中,获取构造函数后,可向原型添加新方法,甚至实现继承其他类的功能。
方法装饰器
作用于类的方法,包括静态方法和原型方法。作用于静态方法时,装饰器接收类构造函数;作用于原型方法时,装饰器接收原型对象。以下以原型方法为例。
function methodDecorator(value: string, Girl){
return function(prototype, key, descriptor){
console.log('接收原型对象,装饰的属性名,属性描述符', Girl.prototype === prototype)
}
}
function thinFace(prototype, key, descriptor){
// 保留原来的方法逻辑
let originalMethod = descriptor.value;
// 改写,增加逻辑,并执行原有逻辑
descriptor.value = function(){
originalMethod.call(this); // 注意修改this的指向
console.log('开启瘦脸模式')
}
}
class Girl {
@thinFace
@methodDecorator('方式装饰器', Girl)
faceValue(){
console.log('我是原本的面目')
}
}
let g = new Girl();
g.faceValue();
从代码可见,装饰器函数接收三个参数:原型对象、方法名以及属性描述对象。若对描述对象不熟悉,可参考相关文档。
要增强功能,可先保留原函数,并将描述对象的 value 属性改写为新的函数。
当通过 g.faceValue() 访问方法时,实际访问的是描述对象 value 对应的值。
在改写的函数中增加逻辑,并执行之前保留的原函数。注意,原函数需使用 call 或 apply 将 this 指向原型对象。
属性装饰器
作用于类中定义的属性,这些属性并非原型上的属性,而是通过类实例化得到的实例对象上的属性。
装饰器同样接收两个参数:原型对象和属性名。为何没有属性描述对象?这与TypeScript初始化属性装饰器的机制有关。目前无法在定义原型成员时描述实例属性。
function propertyDecorator(value: string, Girl){
return function(prototype, key){
console.log('接收原型对象,装饰的属性名,属性描述符', Girl.prototype === prototype)
}
}
function thinFace(prototype, key){
console.log(prototype, key)
}
class Girl {
@thinFace
@propertyDecorator('属性装饰器', Girl)
public age: number = 18;
}
let g = new Girl();
console.log(g.age); // 18
其他装饰器的写法
以下组合多个装饰器书写,除上述三种外,还包括访问器装饰器和参数装饰器。这些装饰器同时存在时,有特定的执行顺序。
function classDecorator(value: string){
console.log(value)
return function(){}
}
function propertyDecorator(value: string) {
console.log(value)
return function(){
console.log('propertyDecorator')
}
}
function methodDecorator(value: string) {
console.log(value)
return function(){
console.log('methodDecorator')
}
}
function paramDecorator(value: string) {
console.log(value)
return function(){
console.log('paramDecorator')
}
}
function AccessDecorator(value: string) {
console.log(value)
return function(){
console.log('AccessDecorator')
}
}
function thinFace(){
console.log('瘦脸')
}
function IncreasingEyes() {
console.log('增大眼睛')
}
@thinFace
@classDecorator('类装饰器')
class Girl {
@propertyDecorator('属性装饰器')
age: number = 18;
@AccessDecorator('访问符装饰器')
get city(){}
@methodDecorator('方法装饰器')
@IncreasingEyes
faceValue(){
console.log('原本的脸')
}
getAge(@paramDecorator('参数装饰器') name: string){}
}
运行编译后的代码会发现,执行顺序为:属性装饰器 -> 访问器装饰器 -> 方法装饰器 -> 参数装饰器 -> 类装饰器。
更详细的用法可参考官方文档:https://www.tslang.cn/docs/handbook/decorators.html#decorator-factories
装饰器运行时代码分析
由于浏览器不支持原生装饰器,必须通过工具编译为浏览器可执行的代码。
下面分析编译后的源码结构。
生成 face.js 文件:
tsc --target ES5 --experimentalDecorators face.ts
打开 face.js 文件,会看到一段被压缩的代码,可格式化以便阅读。
先看这段代码:
__decorate([
propertyDecorator('属性装饰器')
], Girl.prototype, "age", void 0);
__decorate([
AccessDecorator('访问符装饰器')
], Girl.prototype, "city", null);
__decorate([
methodDecorator('方法装饰器'),
IncreasingEyes
], Girl.prototype, "faceValue", null);
__decorate([
__param(0, paramDecorator('参数装饰器'))
], Girl.prototype, "getAge", null);
Girl = __decorate([
thinFace,
classDecorator('类装饰器')
], Girl);
__decorate 函数的作用是执行装饰器函数。从这段代码中可看出许多信息,印证了前述结论。
通过 __decorate 的调用顺序,可看出多种装饰器一起使用时,顺序为:属性装饰器 -> 访问器装饰器 -> 方法装饰器 -> 参数装饰器 -> 类装饰器。
调用 __decorate 函数时,根据使用的装饰器类型不同,传入的参数也有所不同。
第一个参数始终为数组,确保与书写顺序一致,每一项是求值后的装饰器函数。若写的是 @propertyDecorator(),则先执行该工厂函数得到装饰器函数,这与前述分析一致。
类装饰器将类作为第二个参数传入;其他装饰器将原型对象作为第二个参数,属性名作为第三个参数,第四个参数为 null 或 void 0。void 0 的值为 undefined,即未传参数。
需记住传给 __decorate 函数的参数个数和值,以便深入 __decorate 源码时,根据这些值决定执行装饰器函数时传入参数的多少。
接下来看 __decorate 函数的实现:
// 已存在此函数,直接使用,否则自己定义
var __decorate = (this && this.__decorate) ||
// 接收四个参数:
//decorators存放装饰器函数的数组、target原型对象|类,
//key属性名、desc描述(undefined或null)
function(decorators, target, key, desc) {
var c = arguments.length,
// 拿到参数的个数
r = c < 3 // 参数小于三个,说明是类装饰器,直接拿到类
? target
: desc === null // 第四个参数为 null,则需要描述对象;属性装饰器传入是 void 0,没有描述对象。
? desc = Object.getOwnPropertyDescriptor(target, key)
: desc,
d;
// 如果提供了Reflect.decorate方法,直接调用;否则自己实现
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 // 参数小于3个,说明是类装饰器,执行装饰器函数,直接传入类
? d(r)
: c > 3 // 参数大于三个,是方法装饰器、访问符装饰器、参数装饰器,则执行传入描述对象
? d(target, key, r)
: d(target, key) // 为属性装饰器,不传入描述对象
) || r;
// 给被装饰的属性,设置得到的描述对象,主要是针对,方法、属性来说的
/***
* r 的值分两种情况,
* 一种是通过上面的 Object.getOwnPropertyDescriptor 得到的值
* 另一种,是装饰器函数执行后的返回值,作为描述对象。
* 一般不给装饰器函数返回值。
*/
return c > 3 && r && Object.defineProperty(target, key, r),r;
};
上述参数装饰器调用了一个名为 __params 的函数。
var __param = (this && this.__param) || function (paramIndex, decorator) {
return function (target, key) { decorator(target, key, paramIndex); }
};
其目的是为装饰器函数传入参数的位置 paramIndex。
分析编译后的源码,相信能对装饰器的理解更加深刻。
以上就是JS装饰者模式和TypeScript装饰器的详细内容,更多关于JS的资料请关注本站其它相关文章!