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/year/2025/proposal-redeclarable-global-eval-vars.md.
  • 简体中文
  • Redeclarable global eval-introduced vars S4

    中文标题:可重新声明的全局 eval 引入的变量

    提案概览
    提案速览

    该提案通过消除 eval 引入的全局 var 绑定与其他全局属性之间的区别,简化了 ECMAScript,允许在全局顶层对 eval 引入的 var 进行词法重新声明。它移除了全局环境记录中的 [[VarNames]] 跟踪,将重新声明错误的情况从三种减少为两种。

    Note

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

    eval 引入的全局 var 可重新声明

    阶段:3

    作者:Shu-yu Guo

    提案发起人:Shu-yu Guo

    动机

    目前,在全局顶层,以下情况会报错:

    1. 使用同名的 letconst 重新声明一个 varfunction 绑定
    2. 使用同名的 letconst 重新声明一个不可配置的全局属性
    3. 使用同名的 letconst 重新声明一个 letconst

    虽然所有的全局 varfunction 绑定同时也是全局属性,但(1)与(2)不同,因为由宽松的直接 eval 在全局顶层引入的 varfunction 绑定是可配置的(而通过脚本引入的 var 绑定是不可配置的)。

    这种区别是通过在全局环境记录中的 [[VarNames]] 字段中跟踪所有 var 绑定名称来规定的。

    本提案声称这种区别的实用性可疑,并且使语言的思维模型和实现都变得复杂。

    首先,防止重新声明的目标并未实现。由 eval 引入的 var 绑定已经是可删除的,因为它们是可配置的。所以并不是你不能重新声明它们。你可以,只是必须先删除 var

    虽然确实在函数作用域中由 eval 引入的 var 绑定也是可删除的,但关键区别在于函数作用域是 封闭的,而全局顶层作用域是 开放的。也就是说,对比以下示例。

    <script>
    eval("var x = 42");
    </script>
    <script>
    delete x;  // 删除 eval 引入的 `var`
    </script>
    <script>
    let x;     // 将 x 重新声明为 let
    </script>
    function f() {
      // 这是重新声明错误,因为当作用域中已有 let x 时,
      // eval 被执行。
      //
      // 也就是说,无法在单个函数作用域中通过先删除再重新声明的方式,
      // 将 eval 引入的 var 重新声明为词法绑定。
      eval("var x = 42");
      delete x;
      let x;
    }

    其次,[[VarNames]] 列表必须被特殊跟踪,并且基本上只是全局对象上所有属性的属性描述符上的一个额外比特。这增加了额外的实现复杂性。

    提案

    规范更改请参见 https://github.com/tc39/ecma262/pull/3226([渲染](https://arai-a.github.io/ecma262-compare/?pr=3226))。该 PR 移除了 [[VarNames]],实际上将上述 3 种情况减少为 2 种:

    1. 使用同名的 letconst 重新声明一个不可配置的全局属性
    2. 使用同名的 letconst 重新声明一个 letconst

    使宽松的直接 eval 引入的变量变为不可配置的替代方案未被考虑,因为其历史更长,且不太可能成为 Web 兼容的更改。

    后果

    主要变化是以下代码现在被允许。在全局顶层作用域中由 eval 引入的 var 可以被词法绑定重新声明,实际上遮蔽了 globalThis 上的可配置属性。

    <script>
    eval("var x = 'var'");
    </script>
    <script>
    let x = 'let';
    console.log(x);            // 'let'
    console.log(globalThis.x); // 'var'
    </script>

    此更改是 Web 兼容的,因为我们正在将抛出行为更改为不抛出行为。

    常见问题

    这是否破坏了与函数作用域 var 和直接 eval 的对称性?

    并非如此,因为函数作用域和全局顶层作用域的行为已经大不相同。

    如上所述,全局顶层是 开放的 作用域,而函数作用域是 封闭的。本提案改变的可观察行为仅通过重新进入全局作用域(例如在 HTML 嵌入中通过新的 <script> 标签)才能观察到。换句话说,如果将全局顶层作用域当作封闭作用域使用且不重新进入它,则没有可观察的差异。