For AI agents: the complete documentation index is available at /tc39-atlas/llms.txt, the full documentation bundle is available at /tc39-atlas/llms-full.txt, and this page is available as Markdown at /tc39-atlas/proposals/proposal-new-initialize.md.
  • 简体中文
  • new.initialize S1

    提案概览
    提案速览

    该提案引入了 new.initialize(),允许类在不调用父构造函数的情况下用字段和私有方法初始化对象。它解决了动态父类查找导致错误初始化的问题,并简化了使用 Reflect.construct 或 Proxy 的模式。这是一个早期草案,尚未提交 TC39。

    Note

    以下 README 来自上游仓库,其中的阶段或状态标注可能滞后;当前信息以提案概览为准。

    new.initialize()

    早期草案提案,用于支持使用字段和私有方法初始化给定对象。

    状态:尚未提交至 TC39;没有阶段

    背景

    创建 JavaScript 子类的实例是一个自底向上的过程:子类构造函数被调用,它可以返回任何它想要的对象。它“正常”的做法是调用父类构造函数,然后执行与子类相关的任何初始化。这里有一个抽象的例子:

    class A {
      constructor() { this.x = 1; }
    }
    class B extends A {
      constructor() { super(); this.y = 2; }
    }
    
    const instance = new B();
    console.log(instance.x);  // 1
    console.log(instance.y);  // 2
    console.log(instance.z);  // undefined
    

    “super 调用” super() 会根据 GetSuperConstructor 算法动态地找到父类。这个算法是一种花哨的说法,super() 调用的是 B.__proto__,无论执行 super() 时它是什么,而不是总是调用原始的 A

    因此,如果您修改原型,将调用不同的父构造函数,它不会执行原始父构造函数的操作!

    B.__proto__ = class { constructor() { this.z = 3; } };
    const brokenInstance = new B();
    console.log(brokenInstance.x);  // undefined!
    console.log(brokenInstance.y);  // 2
    console.log(brokenInstance.z);  // 3!

    您可以通过 Object.preventExtensionsObject.freeze 使原型链不可变来防止这种情况发生:

    Object.freeze(B)
    B.__proto__ = class { constructor() { this.z = 3; } };  // TypeError

    然而,内置子类倾向于调用原始父类,而不冻结构造函数,无论是来自 JavaScript 还是 Web 平台,正如这个问题指出的。幸运的是,JavaScript 提供了一个强大的工具来解决这个问题:Reflect.construct。使用 Reflect.construct,您可以专门调用特定的父类构造函数,并将 new.target(最低级别的子类)作为参数传递。使用这种技术,我们可以重新定义 B 如下:

    class B extends A {
      constructor() {
        const instance = Reflect.construct(A, [], new.target);
        instance.y = 2;
        return instance;
      }
    }
    
    B.__proto__ = class { constructor() { this.z = 3; } };
    const instance = new B();
    console.log(instance.x);  // 1!
    console.log(instance.y);  // 2
    console.log(instance.z);  // undefined!

    这效果很好,您可以将其视为与内置类的工作方式类似。

    问题

    字段声明和私有方法由语言运行时隐式添加,要么在构造函数开头(对于基类),要么作为 super() 的最后一步(对于子类)。如果我们使用 Reflect.construct 确保总是调用原始父类构造函数,同时保持构造函数本身不被冻结,我们如何允许子类添加字段或私有方法?

    class C {
      #v = 1;
      get v() { return this.#v; }
    }
    class D extends C {
      #w = 2;
      get w() { return this.#w; }
    }
    const instance = new D();
    console.log(instance.v);  // 1
    console.log(instance.w);  // 2
    
    D.__proto__ = class {};
    const broken = new D();
    console.log(broken.v);  // TypeError!
    console.log(broken.w);  // 2

    我们如何防止 TypeError 并确保调用 C 的构造函数,同时仍然允许 D 的构造函数添加私有字段 #w

    提出的解决方案

    new.initialize() 语法可用于向任何对象添加字段和私有方法,当类想要避免调用父构造函数时。类 D 可以写成如下形式:

    class D {
      #w = 2;
      get w() { return this.#w; }
      constructor() {
        const instance = Reflect.construct(C, [], new.target);
        new.initialize(instance);
        return instance;
      }
    }
    
    D.__proto__ = class {};
    const broken = new D();
    console.log(broken.v);  // 1!
    console.log(broken.w);  // 2

    另一个用例:带有私有的简单观察类

    Proxy 可以用来观察对象。对于普通属性,可以使用 =Object.defineProperty 在 Proxy 上创建属性,但私有字段和方法仅由构造函数安装。这意味着可能需要一个笨拙的“super 返回技巧”来观察带有私有字段的对象的访问。

    new.initialize 可以简化这种模式,允许一个构造函数既返回 Proxy 又能添加字段。

    class E {
      #x;
      get x() { return this.#x; }
      constructor(x) {
        const proxy = new Proxy(Object.create(E.prototype), { /* ... */ });
        new.initialize(proxy);
        proxy.#x = 1;
        return proxy;
      }
    }
    const e = new E;
    e.x;  // 1,但也会调用 get 陷阱

    详细语法和语义

    new.initialize 仅在类的构造函数内或构造函数中的直接 eval 中在词法上被允许。它求值为一个长度为 1 的内置函数,该函数接受其单个参数并对其调用 InitializeInstanceElements 抽象操作。该参数用作返回值。

    注意,只会添加包含当前构造函数的类中的字段和私有方法;继承类中的字段和私有方法不会由 new.initialize 添加(但如果您想,可以通过 Reflect.construct 生成一个包含它们的对象)。

    该语法使用元属性模式,遵循 new.targetfunction.sent 的示例。

    实现说明

    当前和正在进行的 JS 引擎中公共和私有字段及方法的一些实现内部创建一个隐藏函数,该函数相当于 new.initialize,但将正在构造的实例作为接收者,而不是参数。new.initialize 的实现将是该函数的简单包装器。仅当构造函数包含 new.initialize 或词法包含的直接 eval 时才需要创建它,因此实践中不应有显著的内存开销。

    临时解决方案

    如果您想在不等待此提案通过标准化的情况下获得其好处,现在可以使用以下策略之一:

    • 在基类中定义私有字段或方法(可以说这就是内置类所做的),并将子类中的私有字段替换为对属性的写入(例如,通过转译去掉它们)
    • 使用 WeakMap 和 WeakSet 代替私有字段和方法,以便您可以在 Reflect.construct 模式中将它们放置到位。字段和私有方法以及此提案的转译器实现可能会这样做。
    • 通过 Object.preventExtensionsObject.freeze 在子类构造函数上防止原型修改。

    因为这些策略一开始似乎足够,尽管它们各自都有局限性,此提案不需要阻塞类字段提案。

    致谢

    感谢 Allen Wirfs-Brock 几年前提出这个问题,以及 Domenic Denicola 提供了用于 polyfilled 标准库功能的应用程序上下文。