本文深入解析JavaScript中装饰器的实现原理,涵盖类属性与类的装饰机制。通过ES6语法糖与Object.defineProperty的关系,阐明装饰器的工作流程,并结合TypeScript中的实际案例,展示如何为类及其方法添加功能扩展,如属性只读控制、类增强及方法叠加等应用场景。
近期在采用TS结合Vue的开发架构时,观察到项目中广泛使用了装饰器特性,这引发了我对底层实现机制的探究兴趣。为了透彻理解这一知识点,特此梳理其核心原理。
装饰器的常见应用场景
- 对类属性进行装饰处理
- 对类本身进行装饰处理
需要明确的是,装饰器仅能作用于类及其属性,无法直接应用于普通函数,这主要归因于JavaScript中存在的函数提升机制。
接下来,我们将通过具体示例分别阐述这两种装饰情况。
类属性装饰详解
function readonly(target, name, descriptor) {
discriptor.writable = false;
return discriptor;
}
class Cat {
@readonly
say() {
console.log("meow ~");
}
}
var kitty = new Cat();
kitty.say = function() {
console.log("woof !");
}
kitty.say() // meow ~
在ES6规范中,类的定义本质上是一种语法糖,其底层实现仍依赖于构造函数。类属性的定义过程实际调用了 Object.defineProperty() 方法。上述类的代码结构可以等价转换为以下形式:
class Cat {
say() {
console.log("meow ~");
}
}
// 上面等价于下面代码
function Cat() {}
Object.defineProperty(Cat.prototype, "say", {
value: function() { console.log("meow ~"); },
enumerable: false,
configurable: true,
writable: true
});当在 say 方法前添加 @readonly 装饰器后,整个代码逻辑等价于执行以下操作:
function readonly(target, name, descriptor) {
discriptor.writable = false;
return discriptor;
}
function Cat() {}
let descriptor = {
value: function() { console.log("meow ~"); },
enumerable: false,
configurable: true,
writable: true
};
descriptor = readonly(Cat.prototype, 'say', descriptor) || descriptor;
Object.defineProperty(Cat.prototype, "say", descriptor);
类属性装饰器的底层实现原理,正是基于上述语法糖的转换过程。
此处需特别注意装饰器的书写规范:应在属性前使用 @readonly 形式,而非 @readonly() 形式。若采用后者写法,装饰器函数必须返回一个函数以表示装饰行为。这种传参写法的主要目的是通过参数扩展装饰器功能,从而提升装饰器函数的复用性。
例如,可以通过传参来控制是否设置属性为只读,具体代码如下:
function readonly(flag) {
return function (target, name, descriptor) {
if (flag) {
discriptor.writable = false;
} else {
discriptor.writable = true;
}
return discriptor;
}
}
class Cat {
@readonly(true)
say() {
console.log("meow ~");
}
}
var kitty = new Cat();
kitty.say = function() {
console.log("woof !");
}
kitty.say()
类装饰详解
在装饰一个类时,类本身在本质上是一个函数,并不具备描述符(descriptor)。此时,装饰器接收的 target 参数即为该函数本身。
function isAnimal(target) {
target.isAnimal = true;
return target;
}
@isAnimal
class Cat {
...
}
console.log(Cat.isAnimal); // true
换言之,上述 @isAnimal 装饰器的作用等同于执行以下代码:
Cat = isAnimal(function Cat() { ... });
关键注意点
作用于具体方法上的 decorator 接收的第一个参数(target)指向类的 prototype;而若将一个 decorator 直接作用于类本身,则其第一个参数 target 指向的是类构造函数本身。
装饰器的执行时机发生在属性定义阶段,这意味着被装饰的属性在定义完成后,即已变为经过装饰器处理后的新属性。
以下示例展示了装饰器的具体执行时序:
function log(message) {
return function() {
console.log(message);
}
}
console.log('before class');
@log('class Bar')
class Bar {
@log('class method bar');
bar() {}
@log('class getter alice');
get alice() {}
@log('class property bob');
bob = 1;
}
console.log('after class');
let bar = {
@log('object method bar')
bar() {}
};控制台输出结果如下:
before class class method bar class getter alice class property bob class Bar after class object method bar
从输出结果可以看出,装饰器在类及其属性定义阶段便已介入并完成了“装饰”操作。
实际应用场景
本示例设定的场景如下:
- 首先,创建一个基础的 Man 类,设定其防御值为 2,攻击力为 3,血量为 3;
- 接着,为其装备钢铁侠盔甲,使防御力增加 100,总计变为 102;
- 随后,为其配备光束手套,使攻击力增加 50,总计变为 53;
- 最后,为其增加“飞行”能力。
1. 构建 Man 类:
class Man{
constructor(def = 2,atk = 3,hp = 3){
this.init(def,atk,hp);
}
init(def,atk,hp){
this.def = def; // 防御值
this.atk = atk; // 攻击力
this.hp = hp; // 血量
}
toString(){
return `防御力:${this.def},攻击力:${this.atk},血量:${this.hp}`;
}
}
var tony = new Man();
console.log(`当前状态 ===> ${tony}`);
// 输出:当前状态 ===> 防御力:2,攻击力:3,血量:3
2. 利用装饰器为钢铁侠装配盔甲:
function decorateArmour(target, key, descriptor) {
const method = descriptor.value;
let moreDef = 100;
let ret;
descriptor.value = (...args)=>{
args[0] += moreDef;
ret = method.apply(target, args);
return ret;
}
return descriptor;
}
class Man{
constructor(def = 2,atk = 3,hp = 3){
this.init(def,atk,hp);
}
@decorateArmour
init(def,atk,hp){
this.def = def; // 防御值
this.atk = atk; // 攻击力
this.hp = hp; // 血量
}
toString(){
return `防御力:${this.def},攻击力:${this.atk},血量:${this.hp}`;
}
}
var tony = new Man();
console.log(`当前状态 ===> ${tony}`);
// 输出:当前状态 ===> 防御力:102,攻击力:3,血量:3
3. 利用装饰器增加光束手套功能:
function decorateLight(target, key, descriptor) {
const method = descriptor.value;
let moreAtk = 50;
let ret;
descriptor.value = (...args)=>{
args[1] += moreAtk;
ret = method.apply(target, args);
return ret;
}
return descriptor;
}
class Man{
constructor(def = 2,atk = 3,hp = 3){
this.init(def,atk,hp);
}
@decorateArmour
@decorateLight
init(def,atk,hp){
this.def = def; // 防御值
this.atk = atk; // 攻击力
this.hp = hp; // 血量
}
...
}
var tony = new Man();
console.log(`当前状态 ===> ${tony}`);
//输出:当前状态 ===> 防御力:102,攻击力:53,血量:3
在此处可以清晰看到装饰模式的优势:它支持对特定方法进行叠加使用,对原类的侵入性极小,仅需增加一行 @decorateLight 即可实现,便于灵活增删功能;同时具备良好的代码复用性。
4. 增加飞行能力:
function addFly(canFly){
return function(target){
target.prototype.canFly = canFly;
let extra = canFly ? '(技能加成:飞行能力)' : '';
let method = target.prototype.toString;
target.prototype.toString = (...args)=>{
return method.apply(target.prototype,args) + extra;
}
return target;
}
}
@addFly(true)
class Man{
constructor(def = 2,atk = 3,hp = 3){
this.init(def,atk,hp);
}
@decorateArmour
@decorateLight
init(def,atk,hp){
this.def = def; // 防御值
this.atk = atk; // 攻击力
this.hp = hp; // 血量
}
...
}
...
console.log(`当前状态 ===> ${tony}`);
// 输出:当前状态 ===> 防御力:102,攻击力:53,血量:3(技能加成:飞行能力)至此,关于JavaScript装饰器实现原理的详细介绍已告一段落。如需了解更多JavaScript装饰器相关内容,欢迎查阅本站过往文章或继续浏览后续相关文章,感谢大家的支持!