这是某个教程中的写法
/**
* 定义一个属性装饰器
* 设置默认值
*/
function defaultValue(value: any) {
return function (target: any, propertyKey: string) {
console.log(`Decorating ${propertyKey} with default value: ${value}`);
let val = value;
const getter = function () {
console.log(`Getting value: ${val}`);
if (typeof val == "undefined") return value;
return val;
};
const setter = function (newValue: any) {
console.log(`Setting value: ${newValue}`);
val = newValue;
};
Object.defineProperty(target, propertyKey, {
get: getter,
set: setter,
enumerable: true,
configurable: true,
});
// console.log(target[propertyKey]);
};
}
class User {
@defaultValue("Jane") // 默认值
username: string;
constructor() {}
}
const user = new User();
console.log(user.username); // 这里打印 undefined
- 注意最后一句,实际上这里打印出来的就是undefined,无法设置默认值。
- 为什么会这样?
- 因为ts中,创建一个实例的时候,如果没有声明构造函数,所有的属性都会默认定义为undefined。
那么当前的代码,怎么修改才能完成默认值的设置呢?
- 使用类装饰器重新写一个构造函数
- 在这个类装饰器中创建一个实例
- 在这个实例上检查所有的属性,如果这个属性有“defaultValue”这个元数据,并且这个属性的值是 undefined,那么就将元数据的值赋值给这个属性。
- 下面是修改过后的代码:
import "reflect-metadata";
type Constructor<T = {}> = new (...args: any[]) => T;
// 元数据 key
const DEFAULT_VALUE_KEY = Symbol("defaultValue");
// 属性装饰器:记录默认值
function defaultValue(value: any) {
return function (target: any, propertyKey: string) {
Reflect.defineMetadata(DEFAULT_VALUE_KEY, value, target, propertyKey);
};
}
// 类装饰器:自动为实例注入默认值
function WithDefaults<T extends Constructor>(constructor: T) {
// 保存原始构造函数
const original = constructor;
// 创建一个新的构造函数
const f: any = function (...args: any[]) {
// 先调用原构造函数
const instance = new original(...args);
// 获取该实例原型上所有已定义的属性名(非 constructor)
Object.getOwnPropertyNames(instance).forEach((key) => {
// 检查是否有 defaultValue 元数据
const defaultValue = Reflect.getMetadata(
DEFAULT_VALUE_KEY,
constructor.prototype,
key
);
if (defaultValue !== undefined) {
// 如果这个实例上有username这个属性,并且这个属性的值是 undefined
if (
Object.hasOwn(instance, key) &&
typeof instance[key] === "undefined"
) {
// 那么设置这个属性的默认值
instance[key] = defaultValue;
}
}
});
return instance;
};
// 继承原型链和静态属性
f.prototype = constructor.prototype; // 继承构造函数中的prototype,这个相当于继承函数
f.prototype.constructor = constructor; // 继承构造函数
// 返回新构造函数
return f as T;
}
@WithDefaults
class User {
@defaultValue("Jane")
username: string;
}
const user = new User();
// console.log(Object.getOwnPropertyNames(User.prototype['constructor'].prototype))
console.log(user.username); // Jane
疑问
- 为什么这里
Object.getOwnPropertyNames(instance)不能写成Object.getOwnPropertyNames(constructor)呢? - 大家可以使用
Object.getOwnPropertyNames(constructor)看看打印出来的信息。这里面根本就没有username, - 为什么会这样?
- 因为ts实例化的时候,如果没有构造函数,那么属性是根本不会挂载在原型链上。
- 大家可以运行下面的代码测一下
class Foo {
username: string;
}
const foo = new Foo();
console.log(Object.getOwnPropertyNames(Foo.prototype)); // 打印:['constructor']
- 最后整理一次思路
- 目的:设置 某个类上面的属性 的 默认值
- 步骤一:找到这个属性
- ts中,这个类的属性只有在类实例化之后才能被找到
- 通过类的原型链去找根本找不到这个属性。
- 那么就只能创建这个类的实例,才能找到这个属性咯。
- 步骤二:在类的装饰器中,重写构造器(constructor)
- 在构造器返回的实例中找到该属性
- 赋予该属性对应的元数据中的默认值
补充
- 上面的所有代码,都是在ES2023(或者更高,例如ESNEXT)这个前提下。其中tsconfig.json如下:
{
"compilerOptions": {
"types": ["node"],
"module": "nodenext",
"moduleResolution": "nodenext",
"resolvePackageJsonExports": true,
"esModuleInterop": true,
"isolatedModules": true,
"declaration": true,
"removeComments": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"target": "ES2023",
"sourceMap": true,
"outDir": "./dist",
"baseUrl": "./",
"incremental": true,
"skipLibCheck": true,
"strictNullChecks": true,
"forceConsistentCasingInFileNames": true,
"noImplicitAny": false,
"strictBindCallApply": false,
"noFallthroughCasesInSwitch": false
}
}
- 注意这里的 target 是 ES2023
- 如果要使用getter和setter是可以跑通过的,但是要转成 ES2021
END