typescript中的对象默认值问题

38 阅读3分钟

这是某个教程中的写法

/**
 * 定义一个属性装饰器
 * 设置默认值
 */
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