Typescript类成员初始化器被内联到类的构造函数中。

huangapple go评论152阅读模式
英文:

Typescript class member initialisers are inlined into the constructor of the class

问题

这种行为似乎已经有很好的文档记录(请参见 https://stackoverflow.com/questions/51494083/initializing-variables-inline-during-declaration-vs-in-the-constructor-in-angula 这里的示例),但它可能导致一些非常难以跟踪的内存问题。

请看下面的示例:

class Bar {
    private num: number;
    private closure: any;
    constructor(num: number, closure: any) {
        this.num = num;
        this.closure = closure;
    }
}

class Foo {
    private bar = new Bar(5, (a: number, b: number) => { return a < b; });
    private baz: number;
    constructor(very_expensive_thing: any) {
        this.baz = very_expensive_thing.small_thing;
    }
}

如果这是普通的 JavaScript,就不会出现问题,因为 Bar 的初始化程序无法访问 very_expensive_thing 对象。

然而,TypeScript 将 Bar 的初始化程序内联到了 constructor 中,因此它现在保留了 very_expensive_thing,请查看从 TypeScript Playground v5.1.3 生成的 JavaScript 代码:

"use strict";
class Bar {
    constructor(num, closure) {
        this.num = num;
        this.closure = closure;
    }
}
class Foo {
    constructor(very_expensive_thing) {
        this.bar = new Bar(5, (a, b) => { return a < b; });
        this.baz = very_expensive_thing.small_thing;
    }
}

在闭包内保留构造函数参数并不直观,也不符合正常的作用域规则。

尽管修复方法很简单(将闭包移到类外部),但很希望听到更有经验的 TypeScript 用户的意见,是否已知这是一个常见问题,或者是否应将其视为一个问题。是否有办法关闭内联行为以防止这种情况发生?

英文:

This behaviour seems fairly well documented (See https://stackoverflow.com/questions/51494083/initializing-variables-inline-during-declaration-vs-in-the-constructor-in-angula here on SO for example), but it can cause some really hard to track down memory issues.

See the following example

class Bar {
    private num: number;
    private closure: any;
  constructor(num: number, closure : any) {
    this.num = num;
    this.closure = closure;
}
}

class Foo {
  private bar = new Bar(5, (a: number, b: number) =&gt; {return a &lt; b;});
  private baz: number;
  constructor(very_expensive_thing: any) {
    this.baz = very_expensive_thing.small_thing;
  }
}

If this would have been plain old javascript, there would be no issue as the Bar initialiser has no access to the very_expensive_thing object.

However, typescript inlines the initialiser of Bar into the constructor and as such it now retains the very_expensive_thing, see generated javascript from the typescript playground v5.1.3:

&quot;use strict&quot;;
class Bar {
    constructor(num, closure) {
        this.num = num;
        this.closure = closure;
    }
}
class Foo {
    constructor(very_expensive_thing) {
        this.bar = new Bar(5, (a, b) =&gt; { return a &lt; b; });
        this.baz = very_expensive_thing.small_thing;
    }
}

The retaining of a constructor parameter inside the closure is not intuitive and does not follow normal scoping rules.

While the fix is trivial (move the closure out of the class), it would be great to hear from more experienced typescript users whether this is a known pitfall or should be treated as an issue. Is there anyway to turn off the inlining behaviour to prevent this?

答案1

得分: 1

类字段ES2022 中正式引入了 JavaScript。如果你将 TypeScript 配置为 --target 到 ES2022 之前的 JS 运行时版本,那么代码将会被降级,通过将字段内联到构造函数中以使其在这种版本中工作。请注意,TypeScript 并不一定会承诺以这种方式降级每个语言特性,以至于保留特性的每一个细微差别。有时候,收益并不值得付出努力或增加复杂性;请参考类似于 microsoft/TypeScript#32743 的问题,以及其他类似的问题。

如果你不希望发生这种情况,并且想要语义尽量保持与 JavaScript 类字段一致,你应该将目标设置为 ES2022 或更高版本(并确保启用 --useDefineForClassFields 编译器选项),因为类字段在 JavaScript 中的语义与 TypeScript 团队最初实现时所想的略有不同。如果你这样做,你将得到几乎与 TypeScript 代码完全相同、没有注释或类型修饰符的输出 JavaScript 代码:

class Foo {
    bar = new Bar(5, (a, b) => { return a < b; });
    baz;
    constructor(very_expensive_thing) {
        this.baz = very_expensive_thing.small_thing;
    }
}

代码播放链接

英文:

Class fields were officially introduced to JavaScript in ES2022. If you configure TypeScript to --target a version of the JS runtime before ES2022, then the code will be downleveled to work in such a version, by inlining the fields into the constructor. Note that TypeScript doesn't necessarily commit to downlevel every language feature in such a way that every nuance of the feature is preserved. Sometimes the benefit isn't seen to be worth the effort or added complexity; see issues like microsoft/TypeScript#32743, among others.

If you don't want to see this happen and want the semantics to stick as close to JavaScript class fields as possible, you should target ES2022 or later (and make sure to enable the --useDefineForClassFields compiler option since class fields have slightly different semantics in JavaScript than the TypeScript team had thought when they were first implemented.) If you do that, you will get your output JavaScript to look almost exactly like the TypeScript code with no annotations or type modifiers:

class Foo {
    bar = new Bar(5, (a, b) =&gt; { return a &lt; b; });
    baz;
    constructor(very_expensive_thing) {
        this.baz = very_expensive_thing.small_thing;
    }
}

Playground link to code

huangapple
  • 本文由 发表于 2023年7月6日 11:02:55
  • 转载请务必保留本文链接:https://go.coder-hub.com/76625224.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定