C# 构造函数重载到 JS 单一 constructor:分派机制详解
C# 多个构造函数 → JS 只有一个 constructor:签名分派机制详解
把构造函数签名的哈希值作为 JS 调用的第一个参数——这是一种编译期确定的精确分派,不依赖参数个数或运行时类型。
一、问题:C# 里你可以写 5 个构造函数,JS 里只能有 1 个
C# 的构造函数重载是编译期决定的:
class Person
{
public Person() { }
public Person(string name) { }
public Person(string name, int age) { }
}
每个 new Person(...) 调哪个重载在编译期就已经确定了。但 JavaScript 的 class 只有一个 constructor。
Jazor 的策略:把构造函数签名的稳定标识作为 JS 调用的第一个参数,在分派器里精确匹配。
二、调用约定:签名哈希作为第一个参数
Jazor 为每个构造函数重载生成一个 helper 名——基于签名的稳定哈希,比如 $ctor_3a2f。在调用 new Person("Alice") 时,编译端知道该调哪个重载,于是生成的 JS 调用变成:
new Person("$ctor_7b1e", "Alice")
"$ctor_7b1e" 就是 Person(string name) 这个签名的 helper 名。它是第一个参数。
三、分派器内部:读取 arguments[0] 做精确匹配
生成的 constructor 长这样:
class Person {
constructor() {
let $args = arguments;
let $ctor = $args[0]; // 取出签名标识
// 按签名精确匹配
if ($ctor === "$ctor_3a2f") {
// Person() — 无参
return this.$ctor_3a2f();
}
if ($ctor === "$ctor_7b1e") {
let name = $args[1]; // 从 arguments 中解出真实参数
super(name); // 如果有基类构造器
return this.$ctor_7b1e(name);
}
if ($ctor === "$ctor_9c4d") {
let name = $args[1], age = $args[2];
return this.$ctor_9c4d(name, age);
}
throw new Error("No matching constructor overload for Person.");
}
$ctor_3a2f() { /* Person() 的逻辑 */ }
$ctor_7b1e(name) { /* Person(string name) 的逻辑 */ }
$ctor_9c4d(name, age) { /* Person(string name, int age) 的逻辑 */ }
}
关键几步:
arguments被缓存到$args,再从中取出$args[0]作为$ctor。$ctor跟每个 helper 名做严格相等比较(===)。因为 helper 名是编译期确定的稳定字符串,匹配无歧义。- 匹配命中后,
BuildConstructorDispatcherParameterBindings把$args[1]、$args[2]……依次解出并绑定到参数名——带默认值补齐。 - 调用
this.$ctor_<hash>(...)执行真正的构造逻辑,然后return。
四、参数绑定:从 arguments 解包到具名变量
分派器不只是 if/else 链。每条分支还需要把 arguments 里的匿名参数解包到 helper 方法的具名参数。这是 BuildConstructorDispatcherParameterBindings 做的事:
private IEnumerable<Statement> BuildConstructorDispatcherParameterBindings(
MemberConstructorLowering lowering,
Identifier argsIdentifier)
{
// 对于构造函数的每个参数:
// let paramName = $args[index] ?? defaultValue;
// 声明成 VariableDeclaration
}
如果某个参数有默认值,在这里补齐:
let name = $args[1]; // 必选参数
let timeout = $args[2] !== undefined ? $args[2] : 30; // 带默认值
这样分派器逻辑保持简洁——不做类型检查,只做身份匹配和参数解包。
五、super(...) 的签名传递
派生类中 : base(name) 对应的 super() 调用也需要传入基类构造器的签名 key:
class Student : Person
{
public Student(string name, int grade) : base(name) { }
}
如果基类 Person 也有多个构造函数,Jazor 生成的 super() 调用会在参数列表前面插入基类对应 helper 的签名:
// 生成在 $ctor_d5a8 helper 里:
$ctor_d5a8(name, grade) {
super("$ctor_7b1e", name); // "base(name)" 变成 super("$ctor_7b1e", name)
// Student 自己的逻辑
}
// 对应的逻辑在 CreateConstructorInitializerArguments 里:
if (baseConstructorSymbol is not null &&
HasMultipleExplicitConstructors(baseConstructorSymbol.ContainingType))
arguments.Insert(0, new StringLiteral(
GetMemberConstructorHelperName(baseConstructorSymbol), ...));
只有当基类有多个显式构造函数时,才插入签名 key。 如果基类只有一个构造函数(或隐式声明的),不需要签名分派,super() 直接传参即可。
六、与旧版参数个数分派的对比
早期版本用 arguments.length 分派:
// 旧版:按参数个数分派
if (arguments.length === 0) return this.$ctor_3a2f();
if (arguments.length === 1) return this.$ctor_7b1e(arguments[0]);
if (arguments.length === 2) return this.$ctor_9c4d(arguments[0], arguments[1]);
新版签名分派解决了旧版的三个问题:
| 场景 | 旧版(arguments.length) |
新版(签名 key) |
|---|---|---|
| 同参数个数的不同重载 | 无法区分,直接报错 | $ctor key 不同,精确分派 |
| 带默认参数 | 传 0 个 vs 传 1 个参数分到不同分支 | 签名 key 唯一,默认值在 helper 内补齐 |
| 基类多构造函数 | super() 传参个数可能模糊 |
super("$ctor_key", ...) 精确分派 |
七、仍然拒绝的:ref/out 参数和 this(...) 链
两条旧约束在新版里同样存在:
-
ref/out/in/params参数:BuildConstructorDispatcherParameterBindings依赖"每个参数对应arguments[n+1]的一个位置"。ref/out需要包装数组,params会吞掉可变数量的参数——这些都会破坏分派器的参数解包逻辑。 -
this(...)构造函数链:this("default")等价于"在当前类的分派器里再走一次分派",这在语义上意味着constructor需要递归调用自己。Jazor 选择直接拒绝。
八、小结
Jazor 当前版本的构造函数分派策略:
编译期:为每个重载生成稳定哈希 → helper 名 $ctor_<hash>
调用点:new Foo("$ctor_<hash>", arg1, arg2, ...)
分派器:读 arguments[0] → === 匹配辅助名 → 解包 arguments[1..n] → 调用 this.$ctor_<hash>
基类:super("$ctor_baseHash", arg1, arg2, ...)
这个策略的核心优点是:分派完全由编译期信息驱动,不依赖运行时类型检查。 签名 key 是编译期算出来的稳定字符串,=== 比较无歧义。同一个重载在不同编译运行中产出的 JS 代码完全一致——因为哈希是签名内容的确定性函数。

浙公网安备 33010602011771号