Skip to content

Commit bbde468

Browse files
committedJul 1, 2024·
docs(class): modify useDefineForClassFields, fixed #113
1 parent 8d73b03 commit bbde468

File tree

2 files changed

+140
-59
lines changed

2 files changed

+140
-59
lines changed
 

‎docs/class.md

+134-59
Original file line numberDiff line numberDiff line change
@@ -954,65 +954,6 @@ class Test extends getGreeterBase() {
954954

955955
上面示例中,例一和例二的`extends`关键字后面都是构造函数,例三的`extends`关键字后面是一个表达式,执行后得到的也是一个构造函数。
956956

957-
对于那些只设置了类型、没有初值的顶层属性,有一个细节需要注意。
958-
959-
```typescript
960-
interface Animal {
961-
animalStuff: any;
962-
}
963-
964-
interface Dog extends Animal {
965-
dogStuff: any;
966-
}
967-
968-
class AnimalHouse {
969-
resident: Animal;
970-
971-
constructor(animal:Animal) {
972-
this.resident = animal;
973-
}
974-
}
975-
976-
class DogHouse extends AnimalHouse {
977-
resident: Dog;
978-
979-
constructor(dog:Dog) {
980-
super(dog);
981-
}
982-
}
983-
```
984-
985-
上面示例中,类`DogHouse`的顶层成员`resident`只设置了类型(`Dog`),没有设置初值。这段代码在不同的编译设置下,编译结果不一样。
986-
987-
如果编译设置的`target`设成大于等于`ES2022`,或者`useDefineForClassFields`设成`true`,那么下面代码的执行结果是不一样的。
988-
989-
```typescript
990-
const dog = {
991-
animalStuff: 'animal',
992-
dogStuff: 'dog'
993-
};
994-
995-
const dogHouse = new DogHouse(dog);
996-
997-
console.log(dogHouse.resident) // undefined
998-
```
999-
1000-
上面示例中,`DogHouse`实例的属性`resident`输出的是`undefined`,而不是预料的`dog`。原因在于 ES2022 标准的 Class Fields 部分,与早期的 TypeScript 实现不一致,导致子类的那些只设置类型、没有设置初值的顶层成员在基类中被赋值后,会在子类被重置为`undefined`,详细的解释参见《tsconfig.json》一章,以及官方 3.7 版本的[发布说明](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-7.html#the-usedefineforclassfields-flag-and-the-declare-property-modifier)
1001-
1002-
解决方法就是使用`declare`命令,去声明顶层成员的类型,告诉 TypeScript 这些成员的赋值由基类实现。
1003-
1004-
```typescript
1005-
class DogHouse extends AnimalHouse {
1006-
declare resident: Dog;
1007-
1008-
constructor(dog:Dog) {
1009-
super(dog);
1010-
}
1011-
}
1012-
```
1013-
1014-
上面示例中,`resident`属性的类型声明前面用了`declare`命令,这样就能确保在编译目标大于等于`ES2022`时(或者打开`useDefineForClassFields`时),代码行为正确。
1015-
1016957
## 可访问性修饰符
1017958

1018959
类的内部成员的外部可访问性,由三个可访问性修饰符(access modifiers)控制:`public``private``protected`
@@ -1278,6 +1219,140 @@ class A {
12781219
}
12791220
```
12801221

1222+
## 顶层属性的处理方法
1223+
1224+
对于类的顶层属性,TypeScript 早期的处理方法,与后来的 ES2022 标准不一致。这会导致某些代码的运行结果不一样。
1225+
1226+
类的顶层属性在 TypeScript 里面,有两种写法。
1227+
1228+
```typescript
1229+
class User {
1230+
// 写法一
1231+
age = 25;
1232+
1233+
// 写法二
1234+
constructor(private currentYear: number) {}
1235+
}
1236+
```
1237+
1238+
上面示例中,写法一是直接声明一个实例属性`age`,并初始化;写法二是顶层属性的简写形式,直接将构造方法的参数`currentYear`声明为实例属性。
1239+
1240+
TypeScript 早期的处理方法是,先在顶层声明属性,但不进行初始化,等到运行构造方法时,再完成所有初始化。
1241+
1242+
```typescript
1243+
class User {
1244+
age = 25;
1245+
}
1246+
1247+
// TypeScript 的早期处理方法
1248+
class User {
1249+
age: number;
1250+
1251+
constructor() {
1252+
this.age = 25;
1253+
}
1254+
}
1255+
```
1256+
1257+
上面示例中,TypeScript 早期会先声明顶层属性`age`,然后等到运行构造函数时,再将其初始化为`25`
1258+
1259+
ES2022 标准里面的处理方法是,先进行顶层属性的初始化,再运行构造方法。这在某些情况下,会使得同一段代码在 TypeScript 和 JavaScript 下运行结果不一致。
1260+
1261+
这种不一致一般发生在两种情况。第一种情况是,顶层属性的初始化依赖于其他实例属性。
1262+
1263+
```typescript
1264+
class User {
1265+
age = this.currentYear - 1998;
1266+
1267+
constructor(private currentYear: number) {
1268+
// 输出结果将不一致
1269+
console.log('Current age:', this.age);
1270+
}
1271+
}
1272+
1273+
const user = new User(2023);
1274+
```
1275+
1276+
上面示例中,顶层属性`age`的初始化值依赖于实例属性`this.currentYear`。按照 TypeScript 的处理方法,初始化是在构造方法里面完成的,会输出结果为`25`。但是,按照 ES2022 标准的处理方法,初始化在声明顶层属性时就会完成,这时`this.currentYear`还等于`undefined`,所以`age`的初始化结果为`NaN`,因此最后输出的也是`NaN`
1277+
1278+
第二种情况与类的继承有关,子类声明的顶层属性在父类完成初始化。
1279+
1280+
```typescript
1281+
interface Animal {
1282+
animalStuff: any;
1283+
}
1284+
1285+
interface Dog extends Animal {
1286+
dogStuff: any;
1287+
}
1288+
1289+
class AnimalHouse {
1290+
resident: Animal;
1291+
1292+
constructor(animal:Animal) {
1293+
this.resident = animal;
1294+
}
1295+
}
1296+
1297+
class DogHouse extends AnimalHouse {
1298+
resident: Dog;
1299+
1300+
constructor(dog:Dog) {
1301+
super(dog);
1302+
}
1303+
}
1304+
```
1305+
1306+
上面示例中,类`DogHouse`继承自`AnimalHouse`。它声明了顶层属性`resident`,但是该属性的初始化是在父类`AnimalHouse`完成的。不同的设置运行下面的代码,结果将不一致。
1307+
1308+
```typescript
1309+
const dog = {
1310+
animalStuff: 'animal',
1311+
dogStuff: 'dog'
1312+
};
1313+
1314+
const dogHouse = new DogHouse(dog);
1315+
1316+
console.log(dogHouse.resident) // 输出结果将不一致
1317+
```
1318+
1319+
上面示例中,TypeScript 的处理方法,会使得`resident`属性能够初始化,所以输出参数对象的值。但是,ES2022 标准的处理方法是,顶层属性的初始化先于构造方法的运行。这使得`resident`属性不会得到赋值,因此输出为`undefined`
1320+
1321+
为了解决这个问题,同时保证以前代码的行为一致,TypeScript 从3.7版开始,引入了编译设置`useDefineForClassFields`。这个设置设为`true`,则采用 ES2022 标准的处理方法,否则采用 TypeScript 早期的处理方法。
1322+
1323+
它的默认值与`target`属性有关,如果输出目标设为`ES2022`或者更高,那么`useDefineForClassFields`的默认值为`true`,否则为`false`。关于这个设置的详细说明,参见官方 3.7 版本的[发布说明](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-7.html#the-usedefineforclassfields-flag-and-the-declare-property-modifier)
1324+
1325+
如果希望避免这种不一致,让代码在不同设置下的行为都一样,那么可以将所有顶层属性的初始化,都放到构造方法里面。
1326+
1327+
```typescript
1328+
class User {
1329+
age: number;
1330+
1331+
constructor(private currentYear: number) {
1332+
this.age = this.currentYear - 1998;
1333+
console.log('Current age:', this.age);
1334+
}
1335+
}
1336+
1337+
const user = new User(2023);
1338+
```
1339+
1340+
上面示例中,顶层属性`age`的初始化就放在构造方法里面,那么任何情况下,代码行为都是一致的。
1341+
1342+
对于类的继承,还有另一种解决方法,就是使用`declare`命令,去声明子类顶层属性的类型,告诉 TypeScript 这些属性的初始化由父类实现。
1343+
1344+
```typescript
1345+
class DogHouse extends AnimalHouse {
1346+
declare resident: Dog;
1347+
1348+
constructor(dog:Dog) {
1349+
super(dog);
1350+
}
1351+
}
1352+
```
1353+
1354+
上面示例中,`resident`属性的类型声明前面用了`declare`命令。这种情况下,这一行代码在编译成 JavaScript 后就不存在,那么也就不会有行为不一致,无论是否设置`useDefineForClassFields`,输出结果都是一样的。
1355+
12811356
## 静态成员
12821357

12831358
类的内部可以使用`static`关键字,定义静态成员。

‎docs/tsconfig.json.md

+6
Original file line numberDiff line numberDiff line change
@@ -819,6 +819,12 @@ class User {
819819

820820
如果`"types": []`,就表示不会自动将所有`@types`模块加入编译。
821821

822+
### useDefineForClassFields
823+
824+
`useDefineForClassFields`这个设置针对的是,在类(class)的顶部声明的属性。TypeScript 早先对这一类属性的处理方法,与写入 ES2022 标准的处理方法不一致。这个设置设为`true`,就用来开启 ES2022 的处理方法,设为`false`就是 TypeScript 原有的处理方法。
825+
826+
它的默认值跟`target`属性有关,如果编译目标是`ES2022`或更高,那么`useDefineForClassFields`默认值为`true`,否则为`false`
827+
822828
### useUnknownInCatchVariables
823829

824830
`useUnknownInCatchVariables`设置`catch`语句捕获的`try`抛出的返回值类型,从`any`变成`unknown`

0 commit comments

Comments
 (0)
Please sign in to comment.