理解并应用 tsconfig 中的 useDefineForClassFields

173 阅读3分钟

useDefineForClassFields 与类(class)有关,当 useDefineForClassFields 为 true 时,TypeScript 编译器会生成符合 ECMAScript 标准的类字段。useDefineForClassFields 有利于我们平滑地升级 TypeScript 。

es6 借鉴了传统的面向对象语言(比如 C++ 和 Java),引入了类的概念,可以通过 class 关键字定义类。class 其实是 function 的语法糖。例如下面的代码:

class Point {
  x: number
  y: number
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }

  toString() {
    return '(' + this.x + ', ' + this.y + ')';
  }
}

与下面 ES5 的构造函数代码本质是一样的:

function Point(x, y) {
  this.x = x;
  this.y = y;
}
Point.prototype.toString = function () {
  return '(' + this.x + ', ' + this.y + ')';
};

TypeScript 在 TC39 批准之前很多年就引入了类字段(class),TypeScript 中的类与标准中的类虽然语法相同,但是运行时行为却不一样,TypeScript 为了向标准看起,同时帮助开发者平滑地升级,就定义了 useDefineForClassFields 字段。

当 tsconfig 中 targetES2022 及以上版本时(包括 ESNext)时,useDefineForClassFields 默认为 true 。否则,默认为 false 。

下面看看一段 TypeScript 代码:

class C {
  foo = 100;
  bar: string;
}

const c = new C()

当 tsconfig 中 target 配置为 es6 ,没有启用 useDefineForClassFields 时,编译结果如下:

class C {
  constructor() {
    this.foo = 100;
  }
}
const c = new C();

当 tsconfig 中 target 配置为 es6 ,启用 useDefineForClassFields 时,编译结果如下:

class C {
  constructor() {
    Object.defineProperty(this, "foo", {
      enumerable: true,
      configurable: true,
      writable: true,
      value: 100
    });
    Object.defineProperty(this, "bar", {
      enumerable: true,
      configurable: true,
      writable: true,
      value: void 0
    });
  }
}
const c = new C();

可以看到变化主要由如下两点:

  1. 字段声明的方式从 = 赋值的方式变更成了 Object.defineProperty

  2. 所有的字段声明都会生效,即使它没有指定默认值

默认 = 赋值的方式是 [[Set]] 语义,因为 this.foo = 100 这个操作会隐式地调用上下文中 foosetter。相应地 Object.defineProperty 的方式是 [[Define]] 语义。

当启用 useDefineForClassFields 时,类内部属性的定义由 [[Set]] 语义变为 [[Define]]

[[Set]] 语义是一种相对直接、简单的属性值设定方式。只是提供一种简单直接的对象属性赋值方式,用于快速更新或创建对象属性的值,而不涉及对属性其他复杂特性(如可枚举性、可配置性等)的精细设置。

[[Define]] 语义会比 [[Set]] 语义更加严格、精准地把控属性各方面行为特性。比如规定属性的可枚举性(enumerable)、可配置性(configuarable)、可写性(writable)等。能用于属性访问控制、数据封装等复杂场景。

以 Vue.js 为例,在定义响应式数据时,虽然 Vue.js 内部对数据属性进行了很多复杂的处理,但底层原理是借助 [[Define]] 语义,通过精确地定义属性的特性来实现数据的响应式更新。

class C {
  constructor() {
    // 定义 foo 属性的可枚举性、可配置性、可写性
    Object.defineProperty(this, "foo", {
      enumerable: true,
      configurable: true,
      writable: true,
      value: 100
    });
    Object.defineProperty(this, "bar", {
      enumerable: true,
      configurable: true,
      writable: true,
      value: void 0
    });
  }
}
const c = new C();

总结

代码要向标准看起,如无特殊情况,尽量将 useDefineForClassFields 设置为 true 。

当启用 useDefineForClassFields 时,类内部属性的定义由 [[Set]] 语义变为 [[Define]][[Define]] 语义会比 [[Set]] 语义更加严格、精准地把控属性各方面行为特性。