JavaScript 的新超能力:显式资源管理
显式资源管理提案引入了一种确定性方法,用于显式管理文件句柄、网络连接等资源的生命周期。该提案为语言增加了以下内容:using
和 await using
声明,当资源退出作用域时会自动调用处置方法;[Symbol.dispose]()
和 [Symbol.asyncDispose]()
符号,用于清理操作。asyncDispose]()
符号;两个新的全局对象 DisposableStack
和 AsyncDisposableStack
,作为聚合一次性资源的容器;SuppressedError
,作为一种新的错误类型(既包含最近抛出的错误,也包含被抑制的错误),用于处理在资源处置过程中发生错误的情况,以及可能掩盖从主体或其他资源的处置中抛出的现有错误的情况。这些新增功能通过提供对资源处置的细粒度控制,使开发人员能够编写出更健壮、更高效、更易维护的代码。
using
和await using
声明
显性资源管理提案的核心在于 using
和 await using
声明。using
声明是为同步资源设计的,可确保在声明资源的作用域退出时调用可处置资源的 [Symbol.dispose]()
方法。对于异步资源,await using
声明的作用与此类似,但它能确保调用 [Symbol.asyncDispose]()
方法,并确保调用的结果与 [Symbol.asyncDispose]()
方法的结果一致。
let responsePromise = null;
async function readFile(url) {
if (!responsePromise) {
// Only fetch if we don't have a promise yet
responsePromise = fetch(url);
}
const response = await responsePromise;
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const processedData = await processData(response);
// Do something with processedData
...
}
async function processData(response) {
const reader = response.body.getReader();
let done = false;
let value;
let processedData;
while (!done) {
({ done, value } = await reader.read());
if (value) {
// Process data and save the result in processedData
...
// An error is thrown here!
}
}
// Because the error is thrown before this line, the stream remains locked.
reader.releaseLock();
return processedData;
}
readFile('https://example.com/largefile.dat');
因此,在使用流时,开发人员必须使用 try...finally
块,并将 reader.releaseLock()
放在 finally
中。这种模式可以确保 reader.releaseLock()
始终被调用。
async function processData(response) {
const reader = response.body.getReader();
let done = false;
let value;
let processedData;
try {
while (!done) {
({ done, value } = await reader.read());
if (value) {
// Process data and save the result in processedData
...
// An error is thrown here!
}
}
} finally {
// The reader's lock on the stream will be always released.
reader.releaseLock();
}
return processedData;
}
readFile('https://example.com/largefile.dat');
编写此代码的另一种方法是创建一个一次性对象 readerResource
,其中包含阅读器(response.body.getReader()
)和调用 this.reader.releaseLock()
的 [Symbol.dispose]()
方法。using
声明可确保在代码块退出时调用 readerResource[Symbol.dispose]()
,而且不再需要记住调用 releaseLock
,因为 using
声明会处理它。将来可能会在流等网络 API 中集成 [Symbol.dispose]
和 [Symbol.asyncDispose]
,这样开发人员就不必手动编写封装对象了。
async function processData(response) {
const reader = response.body.getReader();
let done = false;
let value;
// Wrap the reader in a disposable resource
using readerResource = {
reader: response.body.getReader(),
[Symbol.dispose]() {
this.reader.releaseLock();
},
};
const { reader } = readerResource;
let done = false;
let value;
let processedData;
while (!done) {
({ done, value } = await reader.read());
if (value) {
// Process data and save the result in processedData
...
// An error is thrown here!
}
}
return processedData;
}
// readerResource[Symbol.dispose]() is called automatically.
readFile('https://example.com/largefile.dat');
DisposableStack
和 AsyncDisposableStack
为了进一步方便管理多个一次性资源,该提案引入了 DisposableStack
和 AsyncDisposableStack
。这些基于堆栈的结构允许开发人员以协调的方式分组和处置多个资源。资源被添加到堆栈中,当同步或异步处置堆栈时,资源将按照添加时的相反顺序进行处置,以确保正确处理它们之间的任何依赖关系。这简化了处理涉及多个相关资源的复杂情况时的清理过程。这两种结构都提供了用于添加资源或处置操作的 use()
、adopt()
和 defer()
等方法,以及用于触发清理的 dispose()
或 asyncDispose()
方法。DisposableStack
和 AsyncDisposableStack
分别具有 [Symbol.dispose]()
和 [Symbol.asyncDispose]()
,因此它们可以与 using
和 await using
关键字一起使用。它们为在定义的作用域内管理多个资源的处置提供了一种健壮的方法。
让我们看看每种方法并举例说明:
use(value)
将一个资源添加到栈顶。
{
const readerResource = {
reader: response.body.getReader(),
[Symbol.dispose]() {
this.reader.releaseLock();
console.log('Reader lock released.');
},
};
using stack = new DisposableStack();
stack.use(readerResource);
}
// Reader lock released.
adopt(value,onDispose)
会将一个不可丢弃的资源和一个丢弃回调添加到堆栈顶部。
{
using stack = new DisposableStack();
stack.adopt(
response.body.getReader(), reader = > {
reader.releaseLock();
console.log('Reader lock released.');
});
}
// Reader lock released.
会在堆栈顶部添加一个处置回调。这对于添加没有关联资源的清理操作非常有用。defer(onDispose)
{
using stack = new DisposableStack();
stack.defer(() => console.log("done."));
}
// done.
会将当前堆栈中的所有资源移动到一个新的 move()
DisposableStack
中。如果需要将资源的所有权转移到代码的另一部分,这将非常有用。
{
using stack = new DisposableStack();
stack.adopt(
response.body.getReader(), reader = > {
reader.releaseLock();
console.log('Reader lock released.');
});
using newStack = stack.move();
}
// Here just the newStack exists and the resource inside it will be disposed.
// Reader lock released.
中的 DisposableStack
dispose()
和 AsyncDisposableStack
中的 asyncDispose()
用来处置该对象中的资源。
{
const readerResource = {
reader: response.body.getReader(),
[Symbol.dispose]() {
this.reader.releaseLock();
console.log('Reader lock released.');
},
};
let stack = new DisposableStack();
stack.use(readerResource);
stack.dispose();
}
// Reader lock released.
可用性
明确资源管理已在 Chromium 134 和 V8 v13.8 中发布。
你也许感兴趣的:
- 为 V8 提个醒: 通过明确的编译提示加快 JavaScript 启动速度
- 【程序员搞笑图片】纯 JavaScript vs. 框架
- JavaScript 框架选择困难症仍在增加
- 【程序员搞笑图片】盒子里有什么?javascript
- Node.js之父ry“摇人”——要求Oracle放弃JavaScript商标
- JavaScript 之父联手近万名开发者集体讨伐 Oracle:给 JavaScript 一条活路吧!
- 立即让JavaScript获得自由!JS之父等超8000人喊话Oracle:你们也不用,放手吧!
- ECMAScript 2024新特性
- 【外评】JavaScript 变得很好
- 一长串(高级)JavaScript 问题及其解释
这个提案充满了 “你的功能是什么颜色的?”的味道 https://journal.stuffwithstuff.com/2015/02/01/what-color-is-… . 同步函数和异步函数之间的区别不断侵入每个功能。正如我们在这里看到的,有 Symbol.dispose 和 Symbol.asyncDispose,还有 DisposableStack 和 AsyncDisposableStack。
我很高兴 Java 决定走虚拟线程的道路(JEP 444,JDK 21,2023 年 9 月)。他们决定在 JVM 中加入一些复杂性,以便让应用程序开发人员、库编写人员和人工调试人员免受更多复杂性的困扰。
我不同意。隐藏 async 会让代码推理变得更加困难,而不是更加容易。我想知道处置是否是异步的,是否可能受到网络中断等因素的影响。
研究一下 React Suspense 是如何隐藏异步的(通过使用纤维)。它与 nextjs 非常相似,但 React Suspense 为什么不使用 promises 的原始想法(sebmarkbage 曾在 github 上发表过相关文章)非常引人注目
引人注目?这简直糟透了!当一个承诺被解决时,他们不是暂停执行以恢复执行,而是抛出执行,当承诺被解决时,整个执行过程会再次运行,如果遇到另一个承诺,可能会再次抛出执行,如此循环。由于使用了全局来跟踪与钩子调用相关联的呈现上下文,所以这是一个黑客解决方案,所有这些都需要同步进行。如果他们将上下文值和道具一起传递给函数组件,那么他们就可以拥有 async/await 和生成器组件。
这是对 “async ”的批评,而不是对 “using ”的批评,对吗?就我的理解而言,这似乎并没有让函数变得比原来更有色彩。
明确一点……
这并没有引入函数着色。
你只是指出了已有函数着色的效果,因为有两个相关的符号 Symbol.dispose 和 Symobl.asyncDispose。
就像有 Symbol.iterator 和 Symbol.asyncIterator 一样。
恼人的是,现在大多数语言的技术水平基本上都是 “把所有代码都写成 async 就可以了,因为即使是同步调用者,在最糟糕的情况下也只能启动一个一次性的事件循环”。
据我所知,唯一能很好解决这个问题的语言是 Purescript,因为你可以编写针对 Eff(同步特效)或 Aff(异步特效)的代码,并在调用时做出决定。
结构化并发是件好事,但我的印象是,我们做这些语法工作并不是为了获得结构化并发,而主要是为了在服务器中拥有多个顶级请求处理程序。令人尴尬的并行工作!
非常高兴 Java 也做出了这样的决定。
的确如此。虚拟线程、结构化并发和作用域值都是很棒的特性。
这是因为正常执行和异步函数形成了不同的封闭笛卡尔类别,其中正常执行类别可直接嵌入异步类别。
所有函数都有颜色(即它们可以表达的特定类别),但只有某些语言将其明确化。这是语言设计上的一种选择,但类别的功能非常强大,而且不仅仅适用于线程。另外,Java 和基于线程的方法必须处理同步问题,而同步是……困难的。困难。
(JavaScript 将自己限制在单元类别中,更具体地说,限制在那些本质上可以通过调用与延续来表达的类别中。)
在普通的 JavaScript 中,这不是问题。类型被回避了,所以如果你收到一个结果或一个承诺,这并不重要。你可以利用这种动态性在功能上解决 “颜色问题”。
只有当你做了一些古怪的事情,比如试图在完全鸭式类型化的语言中添加整个类型系统时,才会遇到这样的问题。或者,如果你错误地复制了这种 async/await 机制,然后粗暴地将其塞进编译语言中。
Typescript 在键入你的场景时没有问题(或者至少没有 “纯 ”JavaScript 中没有的问题……如果你的值是一个承诺呢?)
编译语言在这方面的问题并不比 JavaScript 多。或者说,JavaScript 在这方面的问题并不少。颜色问题是语法层面的问题!
除了你可以在非 Promise 对象上等待,这只会返回原始对象。大多数其他类型化语言似乎都没有这种便利性。C#(咆哮色彩的明显来源)就没有。这让 JavaScript 与众不同。
同样,Promise.resolve() 也只是返回原始的 Promise 对象。你可以用更少的精力或对实际类型的了解来给事物上色或取消上色。
不过,等待非承诺并不能使其同步–它仍会在微任务中执行任何后续行。
试着运行这段代码。它会先打印 “bar”,然后再打印 “foo”,尽管函数只等待一个字符串字面,而调用者根本没有等待任何东西。
888/4/neonsunset.com
在 C# 中,没有什么能阻止你执行 `var t = Task.Run(() => ExpensiveButSynchronous());` 然后稍后再`await`它。让其他线程池线程处理已知的长操作并不罕见。
除非你的意思是等待非等待类型,这在任何静态类型语言中都是说不通的。
我不清楚 JVM 是如何实现的,但总的来说,多线程是出了名的难以推理。有整本书都在讨论多线程的缺陷(竞赛条件、死锁、活锁、饥饿、内存可见性问题等等)。与之相比,单线程异步编程简直是小菜一碟。我宁愿处理 “函数颜色 ”问题,也不愿尝试调试多线程应用程序中的heisenbug。
通过类似 Erlang 的消息传递实现无共享(Share-nothing)让多线程变得更加容易。我敢说这是一种享受。
对于简单的任务来说,Async 是一种不错的语法,但在组成较大的结构和处理错误等问题时,这种简单性就会分崩离析。与显式线程相比,我觉得它更难理解发生了什么。
> 对于简单的任务来说,Async 是一种不错的语法,但在组成较大的结构和处理错误等问题时,这种简单性就不复存在了。
你有具体的例子吗?自从使用 async/await(不过回调地狱还是存在的)以来,这对我来说从来都不是问题。
你不知道它是如何工作的,还对它发表评论?巅峰HN。
在大多数编程语言中,多线程的工作原理都是类似的,这也是我的假设。你的评论让我有点怀疑自己,所以我回去查了一下规范。结果发现,它确实和我最初想的一样,只是标准的多线程。
我的天
我也是刚刚才想到的。对于发现这一点显而易见的其他人来说,“干得好”,但似乎还是值得一提。
请注意,根据使用情况,使用 `DisposableStack` 和 `AsyncDisposableStack` 可能更可取,因为它们是 `using` 提议的一部分,并且内置了对回调注册的支持。
由于 `using` 是块作用域的,这对于作用域桥接和有条件的注册是非常必要的。
但由于 `using` 是 `const` 的一个变体,需要立即注册初始化值,因此会失败:
这样也会出错:
取而代之的是
类似地,如果您想在代码块中获取资源(有条件或无条件),但又希望在函数级别进行清理,那么您可以在函数顶层创建一个栈,然后在执行过程中向其中添加可处置或回调。
DisposableStack.move() 的作用是什么?它能否用于将收集到的 .defer() 回调完全转移出当前作用域,例如转移到调用栈的上层?将 DisposableStack 作为参数传递给调用者上下文中的所有 .defer() 回调,可能会更简单一些?
是的,或者把它们转移到其他地方。其中一个用例是在构造函数中分配资源的类:
在此示例中,您希望确保如果构造函数中途出错,则已分配的任何资源都会被清理,但如果构造函数成功完成,则只有在实例本身被清理后,资源才会被清理。
> 将 DisposableStack 作为参数传递给调用者上下文中的所有 .defer() 回调,也许会更容易?
这种情况下的问题是,如果当前函数可以获取一次性堆栈,那么就会出错:
但是,如果我们不在退出时释放文件,那么只有当父对象决定处理其堆栈时,文件才会被释放。
因此,我们要做的是使用本地堆栈,在成功返回控制之前,将可处置文件从本地堆栈 “移动 ”到父代堆栈,这样就避免了时间漏洞:
不过在这种情况下,你可能会把栈 “移 ”到 “someObject ”本身,因为它拥有可处置对象的所有权,然后让调用者 “使用 ”它:
从本质上讲,“DisposableStack#move ”是模拟 RAII 基于生命周期的资源管理的一种方法,或者说是某些语言的只允许出错的延迟。
(const local 在片段中应使用 local)。
我去年写过这篇文章,这是我最喜欢的规范之一:https://jonathan-frere.com/posts/disposables-in-javascript/#…
TL;DR:如果只传递正在使用的 DisposableStack,问题在于它要么是一个 “使用中 ”变量(在这种情况下,它会在函数结束时被自动处理掉,即使你实际上还没有使用完堆栈),要么就不是(在这种情况下,如果在设置堆栈时出错,资源就不会被正确处理掉)。
通过 `.move()`,你可以创建一个 DisposableStack,它是一种牺牲品:如果出了问题,它会自动处理掉所有内容;但如果没有出问题,你可以清空它,并将内容传递到其他地方作为安全操作,然后让它随时被处理掉。
就像 golang 一样。不错。
这是个好主意,但是
> 将 [Symbol.dispose] 和 [Symbol.asyncDispose]集成到网络 API(如流)中可能会在将来发生,因此开发人员不必手动编写封装对象。
因此,在可预见的未来,您会遇到这样一种情况:某些 API 和库支持该功能,但其他 API 和库–大多数–不支持该功能。
因此,您要么将代码写成 “using ”指令和 try/catch 块的复杂混合体,要么就忽略该功能,对所有内容都使用 try/catch,这样写出的代码会更容易理解。
我担心这项功能很有可能会被冠以 “不实用 ”的名声(因为现在它就是这样),即使这项功能最终得到了足够的支持而可以使用,也很难挽回这种名声。
这将是一个真正的遗憾,因为它确实解决了一个实际问题,而且设计本身看起来也经过了深思熟虑。
这就是 JavaScript 在过去 15 年中的情况:新的语言特性首先出现在 Babel 等编译器中,然后出现在语言规范中,最后在保守的 NPM 包和浏览器中被采用为稳定的 API。从 “以编译器插件的形式出现 ”到 “被某些浏览器 API 采用”,这个过程通常需要 3-4 年时间;即使在 “常青 ”浏览器中可用之后,你仍然需要进行多填充,或者再等上几年,才能保证在较旧的终端设备上可用。
由于网络 API 的改进速度非常缓慢,开发人员已经习惯于编写小型的网络 API 封装程序,而小型封装程序往往比 polyfills 更无害;或者说,浏览器 API 在典型的使用路径中非常恼人,所以你当然想要一些与众不同的东西。
至少,我个人从未见过一个看似有用的新语言功能,并且心想 “哇,这一定很难使用”。
实际上,很多东西已经使用向前兼容的 polyfills 实现了这一点。例如,NodeJS 生态系统的大部分后端产品都已支持大量此类功能,而且一段时间以来,您已经能够相当有效地使用这一功能(使用转换器来处理语法)。事实上,去年我曾就这一功能发表过几次演讲,在研究过程中,我惊讶地发现 NodeJS 本身或常用库中有很多 API 已经支持 Symbol.dispose,即使 “using ”语法并未在任何地方实现。
我猜想它在前端代码中会比较少见,因为前端代码通常都有自己的生命周期/清理管理系统,但我可以想象它在一些地方还是很有用的。我还希望看到更多的测试库使用这些符号。但我认为,由于后端代码中支持的普遍性,这一切都将随着时间的推移而到来。
对于不支持此功能的 API,你仍然可以通过使用 DisposableStack 来使用 `using`:
这仍然比 try/catch 更简单,尤其是在有多个资源的情况下,因此只要运行时支持新语法,就可以立即采用,而无需等待现有资源更新。
在 JavaScript 世界中,这通常不是用 polyfills 来解决的吗?
我经常在我使用的 JavaScript 库中添加基于符号的功能(当然,命名方法的风险更大)。
我还没有用这种方法把脚炸掉,但不做任何明示或暗示的保证。
不过到目前为止,它对我来说还是非常有效的。
比在原始类中添加符号方法要好得多。
这就是 TC39 需要在协议等基本语言特性上下功夫的原因。在 Rust 中,你可以定义一个新 Trait 并将其植入现有类型。这仍然有缺陷(孤儿规则可以防止问题,但会导致臃肿),但在具有独特符号能力的动态语言中,这无疑会更容易想出一些东西。
动态语言不需要协议。如果你想让一个现有对象 “符合 AsyncDisposable”,你可以这样做:
或者,如果您想确保所有 ImageBitmap 都符合 Disposable:
但这确实在全局范围内泄露了 “Trait 一致性”;这是不安全的,因为我们不知道其他代码是否希望将他们对 dispose 的实现注入到这个类中,也不知道我们是否在打架,也不知道某些关键迭代是否会混淆,等等……
协议在这里如何发挥作用?比如说 “哦,在这个文件或作用域中,`ImageBitmap.prototype[Symbol.dispos]`的值应该是`x`–但在这个作用域之外,它应该是通常的`undefined`”?
您可以使用模块系统将协议实现纳入作用域。这可以最终解决猴子打补丁的问题。但这是一个相当新颖的想法,TC39 会规避风险,浏览器会规避功能,而且语言的复杂性会给大多数更有趣的想法带来问题。
断开调整大小观察者的连接不就是这一功能的一个糟糕例子吗?
我想不出一个合理的例子,但这只是为了说明问题–请换一个你心目中更好的网络协议吧
(编辑:改为 ImageBitmap)
> 因此,在可预见的未来,某些应用程序接口和程序库支持该功能,但其他应用程序接口和程序库(大多数)不支持。
欢迎来到网络。自从 JavaScript 1.1 诞生以来,这种情况就一直存在:现有代码会使用垫片来实现我们想要的功能,而新代码则不会,因为它已经成为语言的一部分。
这让我想起了 C#。C# 中的 IDisposible 和 IAsyncDisposible 对编写良好的机制有很大帮助,这些机制实际上应该以一种良好的方式进行抽象(如锁处理、队列机制、用于冒充的临时作用域等)。
这是因为该提案的作者来自微软,他曾多次驳回那些使语法看起来与 C# 不同的反建议。
https://github.com/tc39/proposal-explicit-resource-managemen…
https://github.com/tc39/proposal-explicit-resource-managemen…
https://github.com/tc39/proposal-explicit-resource-managemen…
https://github.com/tc39/proposal-explicit-resource-managemen…
在我看来,这些答复都非常合理。
他刚刚被解雇。https://news.ycombinator.com/item?id=43978589
这基本上是从 C# 中搬来的,最初的提案对此毫不掩饰,并引用了 Python 的所有上下文管理器、Java 的资源尝试、C# 的使用语句和 C# 的使用声明。using “是关键字,而 ”dispose “是钩子方法,这是一个相当大的暗示。
我理解 JavaScript 需要保持向后兼容性,但语法
[Symbol.dispose]()
在我看来非常奇怪。这看起来像一个数组,而这个数组又像函数一样被调用,并且数组中包含一个方法句柄。
这种语法叫什么?我想进一步了解一下。
如果没记错的话,动态键(对象文字左侧的方括号)已经存在近 10 年了。
https://www.samanthaming.com/tidbits/37-dynamic-property-nam…
示例中还有方法速记:
https://www.samanthaming.com/tidbits/5-concise-method-syntax…
由于符号不能被字符串引用,因此可以将两者结合起来。
基本上,这里没有任何新语法。
是的,对于创建用于表示可迭代集合的对象或类的人来说,这并不陌生。您可以使用与类声明或对象字面意义相同的动态键语法,但使用 `Symbol.iterator` 作为方法的著名符号。
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe…
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe…
更有见识的人很快就会加入进来,但我很确定它是从以下内容派生出来的:
这样就很有意义了。
其他发帖人正确地描述了这是_什么_,但我没有看到有人回答_为什么_。
使用 “符号 ”作为方法名,可以使该方法与之前定义的任何方法区分开来。
换句话说,使用符号作为方法名(而不是字符串),就不可能在这个新的 API 中发生 “名称碰撞”,从而意外地将一个类标记为一次性方法。
这是最重要的原因!
也许是动态属性访问?
前提是您可以使用索引语法和普通的点语法访问对象的属性。因此,`object.foo`相当于`object[“foo”]`或`object[“f” + “o” + “o”]`(因为方括号内的值可以是任何表达式)。如果 `object.foo` 是一个方法,也可以使用 `object.foo()` 或 `object [“foo”]()` 或其他任何表达式。
通常,键表达式总是被强制为字符串,因此,如果您使用 `object[2]` ,这将等同于 object[“2”]。但符号是个例外,它是一种总是通过引用进行比较的唯一对象。符号可以作为键来使用,所以如果你使用类似于
你应该会在控制台中看到,这个对象有一个特殊的符号键,以及正常的 “foo ”属性。
最后一个难题是,有一些 “众所周知的符号 ”主要用于扩展对象的行为,有点像 Python 中的 __dunder__ 方法。Symbol.dispose 就是其中之一–它是一个全局可访问的符号,意思始终如一,可以用来定义一些新功能,而不会破坏向后兼容性。
希望对你有所帮助,欢迎提出更多问题。
不是这样的,这是一个对象字面中的动态键。
888/3/MrJohz.
这也是可能的,而且在使用这种模式时很常见,但原始问题中的特定语法我认为是属性访问,而不是属性字面的一部分。我之所以没有提出来,是因为我觉得我的评论已经够长了,我想解释一下这种特定的语法。没错,在对象字面中也有这种设置属性的语法,在类中也有类似的语法。
这种语法已经使用了很长时间。JavaScript 的迭代器也使用了相同的语法,而且它们成为 JavaScript 的一部分已经快十年了。
888/1/Kalabasa
我猜是对象属性访问。比如
myObj[“myProperty”]
如果它是一个函数,那么就可以调用它、
myObj[“myProperty”]()
如果键是一个符号
myObj[theSymbol]()
我很确定他们问的是动态属性名称,{ [thing]: … }
这是一个功能性的符号参考。
如果代码是
他们就把它记为 `function()`。
如果代码是
则记为 `[Symbol.dispose]()`。
Symbol.dispose 是一个符号键。
> 如果代码是
> obj[Symbol.dispose]()
> 他们就会把它记为 `[Symbol.dispose]()`。
所以
obj[Symbol.dispose]()`与 `[Symbol.dispose]()` 相同?这似乎不对,因为我们可能还有 `obj2` 或 `obj3`。JavaScript 如何知道 `[Symbol.dispose]()`指的是一个特定对象?
资源管理,尤其是当词法范围成为一种特性时,这就是为什么我们中的一些人一直致力于将结构化并发引入 JS:https://bower.sh/why-structured-concurrency
利用结构化并发的库: https://frontside.com/effection
我不明白为什么有人能这样编写代码,并对程序的执行进行推理/控制:)
async (() => (e) { try { await doSomething(); while (!done) { ({ done, value }) = await reader.read()); } promise .then(goodA, badA) .then(goodB, badB) .catch((err) => { console.error(err); } catch { } finally { using stack = new DisposableStack(); stack.defer(() => console.log(“done.”)); } }. });
这才是精妙之处,你不会。90% 的网络开发工作都是在 “升级”,升级的方式没有人要求,也没有人欣赏,因为人们想当然地认为,如果不经常搅拌,你的代码库就会长霉什么的。当然,没有任何概率是真正的 1. 0,所以在极少数情况下,你需要了解 ChatGP–呃,对不起,我错了,我是说 “你 ”在一年多以前写的东西,你建议你的老板把这个 bug 保留到下次有新员工时再处理,因为这将是一个很好的 “跳板”,在此之前,用户仍然可以通过使用推荐的变通方法完成工作、 在此之前,用户仍可通过建议的变通办法完成工作,即在虚拟机上安装 Windows XP 海盗版,并使用 IE6 进入传统门户网站,而该门户网站在公司合并 20 年后仍莫名其妙地存在。
你说到一半我就没听下去了,但我同意你的主要观点,即网络开发领域存在太多不必要的流失,90% 差不多是正确的。忙碌的工作、不断变化的应用程序接口、将未经测试的新范例强加给库用户、期望每个人都重写代码的重大版本升级……有意无意地,这些都是网络开发人员的工作。
不管是有意还是无意,大部分工作都是为了确保有更多的工作需求。否则,随着时间的推移,自然会有分崩离析的风险。为什么要这么做呢?
你的这段话和我们长期以来创建的代码一样复杂。这是你的观点吗?那我明白了。
你大声写出了我一直在默默思考的问题。
从没有标点符号来看,我觉得你也可以大声说出来。
哦,我的上帝,你是对的
https://soundcloud.com/snickerbockers/corporate-it-webdevs-f…
真是天才!有生以来第一次,我的迂腐得到了回报。
哦,我们必须升级,因为有漏洞。90%的代码都存在漏洞。
好吧,我明白了。
首先,您的代码中存在大量严重的语法错误,有些地方甚至不像是有效的 JavaScript。这是我对重构的最佳猜测:
但更重要的是,这根本不是一个合理的 JS 开发人员会写的东西。
1. 混合使用 await 和 while(!done) 并不常见,我无法想象哪个库真的需要这样做。你通常会使用其中一个,而且几乎总是只使用 await:
2. 如果你已经在 Async IIFE 中,就不需要承诺链了。只需根据需要等待即可,除非许诺链能让代码更简洁,例如
3. 设计良好的 JS 库通常不会像你暗示的{good,bad}{A,B}函数那样堆叠承诺处理程序。你通常只需编写代码,并在顶层设置一个异常处理程序:
4. 我们通常不再需要 AIIFEs,所以外层可以去掉。
去掉分号后,情况就更糟了。
这是一个见仁见智的问题。JavaScript 允许你根据自己的喜好使用任何一种约定。就我个人而言,我觉得没有分号的代码看起来要整洁得多。我还会大量使用空白。
在很长一段时间里,我使用分号只是为了防止分号会导致 bug。但 TypeScript 完全考虑到了这一点,并检查了所有这些情况。
在基础 JavaScript 中,只有两种情况需要使用它。
关于 await 块的注意事项: “await “将等待整个返回,所以如果 ”promise “返回另一个promise(”goodA“),而 ”goodA “又返回一个promise(”goodB“),而 ”goodB “又返回另一个promise,最后解析为非promise值 ”goodC“,那么 ”await promise “就……直接得到 ”goodC”。
这段 “示例代码”(如果我们可以这么称呼它的话)之所以使用 goodA 和 goodB,是因为它试图通过编写完全无稽的代码来让事情看起来很疯狂:这些都没有必要,我们只需使用一个等待返回即可:
完成。“await “会一直等待,直到它正在处理的内容不再是一个承诺,然后自动解决整个链,或者在链抛出时,将我们转移到代码的异常捕获部分。
以编程为生,熟悉该语言关键字的语义–就像其他人理解自己喜欢的语言一样?
毕竟,人们是以编写 Haskell 为生的。
还有 Lisp 和 Forth… 😀
Lisp 在某种程度上我可以理解……但以写 Forth 为生?我大概会二三十种语言,但对我来说那是希腊语。
基于堆栈或串联的语言可能很难理解,但就像任何东西一样,你可能会习惯它。)
不过,相比 Forth,我更喜欢 Factor[1]。也许你会喜欢它!
[1] https://factorcode.org/
不是它们难懂,而是它们更密集。摘自 Factor 的示例页面:
> 2 3 + 4 * .
比起:
> (2 + 3) * 4
这就像罗伯-派克(Rob Pike)抨击语法高亮一样。不,它对我很有用。有了它,我可以更快地阅读。
这和我们使用启发式阅读法的原理是一样的,我们只需查看每个单词的首尾,就能更快地阅读单词,而且大多数时候甚至不会注意到错别字。
嗯,我想这可以归结为一个人是如何 “思考 ”的?
有些人喜欢
另一些人喜欢
还有一些人喜欢
我个人觉得最后一种更容易阅读或理解,但我也学过不少 Common Lisp 和 Factor。
语法高亮对很多人都有用,包括我。有了它,我的阅读速度也快多了。我知道有些人在编写 Common Lisp 时不使用语法高亮。)
Forth 可以写得魔鬼般,你可以这样写
其中 + 的操作数是 2,结果由数百个单词产生!
也可以是
这样读起来就容易多了!
如果你正在编写 Forth,那么你应该尽量采用后一种链式结构,即把迄今为止计算出的所有内容,用一个简单的操作数进行一个小的运算。不知道是否总是可行:
现在找出所有这些词中分子和分母的除法。
> … 复分子 … 复分母 … /
是的,这就是为什么你应该用短词。你应该把复杂的部分分解成简短、自足、具有描述性的单词,这将使你的代码更容易阅读、测试和维护。
例如
而不是
你可能应该有
大多数(如果不是全部)Forth 书籍也提到了这一点。
在计算过程中,不是有一些操作要将名称输入全局字典吗?我至少会给它们取类似 tmp-numerator 这样的名字,把它们放到本地/临时函数的命名空间中,然后在引用它们的计算结束后立即 “遗忘 ”它们。
compute-numerator a b + c d + * ; 的编译版本是什么样的?我想至少需要调用一些运行时支持例程,才能在字典中的某个名称下插入编译后的 thunk。
是的,定义诸如 “compute-numerator ”之类的单词确实会在字典中添加条目,但这完全发生在编译时。Forth 不会在运行时插入一个 “编译 Thunk”,而是将单词编译为一个与代码字段地址(CFA)序列绑定的名称。在调用时,它只是通过通常的内部解释器跳转。定义单词本身不需要运行时成本。当你在运行时调用 “compute-numerator ”时,内部解释器只需线程通过这些 CFA。不涉及间接、JIT 或动态 thunk 创建。唯一的运行时影响是调用时执行的单词。所有链接都在编译时解决。
如果你担心会污染全局字典,一个常用的成语是(你已经知道了):
[Define and forget immediately if temporary
: tmp-numerator a b + c d + * ;
tmp-numerator
FORGET tmp-numerator
或者,你也可以在一个单独的词汇表中分离出临时定义:
简而言之:定义中间词会为词典添加词条,但这是在编译时发生的,而不是在运行时。没有额外的运行时开销。命名约定、FORGET 或词汇表可以减轻字典污染/混乱,但因式分解仍然是 Forth 的标准习语。
注意:在某些本地代码编译或基于 JIT 的 Forth 实现中,定义可能会生成机器代码或运行时对象,而不是我提到的简单 CFA 链,但即使在这些情况下,编译也是在运行时执行之前进行的,在单词调用期间不会发生动态插入 Thunk 的情况。
希望我对您的评论理解正确。请告诉我!
要在 HN 上嵌入代码,请在每行开头添加 2 个或更多空格:
(保留 OP 发布时的缩进–我也不明白怎么会有人写出这样的代码 🙂
缩进有帮助。
此外,坚持使用一种风格,而不是将所有风格迥异的方法混合起来做同一件事。
与 HTML 一样,JS 也有一个特殊的特性,那就是您实际上永远无法做出向后不兼容的更改,因为上世纪 90 年代最后一次更新的简陋网店或路由器用户界面仍需正常工作。
但这意味着这种语言更像是一个考古遗址,上面有不同层次的废墟和现代城市。不要因为有了这些功能,就使用它们。
以前有一本关于这方面的好书《JavaScript 精彩部分》。2025 年是否有一本备受推崇的 JavaScript 书籍?
同样是实践,编程是很难的,但一个人不理解一件事,并不意味着它是不可能的,或者是一个坏主意。
但在打开开发工具浏览网页时,几乎所有网站上的错误信息都在向我暗示,不理解某些东西的人不止一个。
如果只有极少数人能在上面工作,这对工作保障也有好处。
你可以用任何编程语言故意写出可怕的代码
这种情况似乎更多发生在 JavaScript 中,但我也见过绝对可怕和令人困惑的 Python。
JavaScript 的语法一开始就不是很好,随着语言功能的增加,它不得不在可能的范围内使用。此外,JavaScript 也正在成为一种相当庞大的语言,它没有标准库,因此所有东西都被挂在全局命名空间中。老实说,这与 PHP 并无不同,后者的功能越来越多。
正如其他人指出的,它与 C# 也有一些相似之处。问题是,除非你是经验丰富的 C# 开发人员,否则部分更现代的 C# 也会让人一头雾水。新的语法特性并不坏,开发人员显然会用它们来实现各种各样的东西,但如果你是语言新手,就会觉得它们像魔法咒语。它们更难阅读、更难遵循,而且看起来与你从其他语言中了解到的任何东西都不一样。它们也不够简单,以至于你无法接受它们,只能输入神奇数量的括号和愚蠢的字符,并接受它们以某种方式起作用。你经常不知道自己刚刚做了什么,也不知道为什么会成功。
我觉得,Javascript 已经到了成为一种有生命力的语言的地步,但由于其最初的实现和继承限制,所有这些伟大的功能都让人感觉是错位的、被附加的,并为新手或经验不足的开发人员提供了障碍。Javascript 已成为一种企业语言,并带来了各种负面影响和包袱。我们不再被半门语言所束缚,可以做更多现代的事情,这固然很好,但这也意味着我们不能再指望人们能轻松掌握这门语言了。
> 部分更现代的 C# 也是一团乱麻
你有什么例子吗?
就我个人而言,大量使用 => 操作符(这恰好与我对大量 JavaScript 代码和匿名函数的主要抱怨不谋而合)。你可以避免它,但它是非常标准的。
更具体地说,我还研究了 ASP.NET Core 中的 JWT 身份验证,发现整个过程非常棘手。这更像是一个库,但我认为许多使用示例最终都变成了一堆意大利面条代码。
等等,什么?=> 操作符只是用于 lambdas、简短返回和切换表达式武器,这已经很常见了!
难道你从来没有用过其他语言来做这些事情吗?
或
或
在几乎所有(优秀的)现代语言中,你都能看到它的形式。它究竟是如何混淆的?
一般来说,身份验证是一个很难的问题,它是 ASP.NET Core(在其他方面都是顶级的)的一个较弱(-ish)方面。但它与 C# 完全没有关系。
老实说,并不比 C++ 差。
大语言模型(LLMs)只会做这个,你会爱上它的。
也许不爱,但你真的别无选择。
我的意思是,在我们谈论的语言社区中,有人创建了一个包来判断变量是否为数字……而且这个包还被大量使用。
JavaScript 在某些方面已经取得了如此大的进步,但仍然缺少参数类型这样的基本功能,这在我看来简直太疯狂了。
JS 中绝大多数严肃的工作都是用 TypeScript 编写的。
这是一个 “没有真正的苏格兰人 ”的论点。
怎么说?GP 抱怨 JS 缺乏类型。我指出,大多数 JS 实际上都受益于类型,因为它通常是用 TS 编写的。没有移动门柱,没有 “真正的苏格兰人 ”的论点。
转述:
>> JS 已经取得了进步,但它仍然缺乏类型,用一种不具备这些基本功能的语言来进行严肃的编程工作似乎太疯狂了。
> 在 JS 中进行严肃的编程工作是在 TypeScript 中完成的
我的意思是同意 GP 的基本观点,即类型很重要。是的,在没有类型的情况下用 JS 进行严肃的编程工作简直是疯了–这就是为什么 ~ 没有人会这么做。这也是 TypeScript 存在的原因,~每个在 JS 项目中认真工作的人都会使用它。他们的论点几乎就是一个稻草人,它指的是一种假设的情况,但并不适用。严肃的 JS 程序员都使用 TS。
那 vanilla 0-dep 0-build 项目只是噱头和玩具吗?
这听起来更像是为语言添加类型的有力论据。
是啊。
困难的是,类型是非常困难/复杂的,需要进行很多权衡。
一个向后兼容性强、解释一致的标准是很难的(参见 Python)。
可以是
需要有人开始用 Rust 创建 leftPad 和 isOdd 类型的巨魔包,这样我们就可以嘲笑他们的狂妄了。
完成,完成:
https://docs.rs/isodd/latest/isodd/
https://docs.rs/leftpad/latest/leftpad/
我打赌你可以在所有现代软件包管理器中找到类似的东西。
> 但仍然缺少一些基本的东西,比如参数类型
就像 Bash、Python 和 Ruby 一样?
Python 正在采取措施添加参数类型。
Ruby 没有类型对我来说也很奇怪,但 Ruby 也把猴子修补代码视为一件好事,所以我已经放弃去理解它的吸引力了。
这一切都要从良好的格式和适当的代码编辑器开始,而不仅仅是网页上的文本区,这样你就会收到许多代码的错误提示(因为它肯定不是有效的 JS =)
当然,因为你的工作需要,所以你也需要真正了解你每时每刻都在使用的语言,这样你就知道如何将那些乱七八糟的代码重写成正常的代码。因为把 async/await 和 .then.catch 混在一起是很荒谬的,而且 while 循环永远都不应该出现在真正的代码库中,除非你想因为在不寻常的情况下故意写成进入自旋循环的代码而被骂。
如果你想玩这个,Bun 1.0.23+ 似乎已经支持:https://github.com/oven-sh/bun/discussions/4325
有人能解释一下为什么不使用(匿名)类析构函数吗?或者用符号(Symbol)作为特殊对象键以外的其他方式。尤其是当有两个符号(异步用不同的符号)时,这将使抽象变得不可靠,不是吗?
析构函数需要确定性清理,而高级 GC 做不到这一点(从效率角度来看,也确实不想这样做)。使用高级 GC 的语言在收集过程中会调用 “finalizers”,因此非常不可靠(而且充满了微妙的陷阱),通常只能作为本地资源(FFI 封装器)的最后解决方案。
因此,许多资源清理工具要么已经存在,要么最终被淘汰、
– 基于 HoF(smalltalk、haskell、ruby)
– 专用的作用域/值钩子(python[1]、C#、Java)
– 回调注册(go、swift)
[1]: Python 最初使用的析构函数得益于重新计数的 GC,但替代的非重新计数实现、重新计数周期以及锁等资源不具备保护(而且不想添加没有明确用途的保护)等因素的结合导致了上下文管理器的引入。
HoF “代表什么?
高阶函数,接收其他函数(/块)的函数。
例如,在 Ruby 中,您可以锁定/解锁一个互斥体,但通常的做法是向 `Mutex#synchronize` 传递一个块,而 `Mutex#synchronize` 本质上只是
并调用为
在其他语言中,析构函数通常用于对象被垃圾回收时。这有一大堆相关的问题,这也是为什么现在人们经常避免使用这种模式的原因。
而处置方法则是在变量离开作用域时调用的,这更容易预测。例如,你可以在方法返回前依赖于文件的关闭或锁的释放。
JavaScript 对同步和异步的定义已经非常明确,这一点也不例外。你的方法需要等待处置完成,因此如果处置是异步的,你的方法也必须是异步的。不过,如果你不习惯这种语法,使用双 await(如 `await using a = await b()`)确实有点恼人。
至于使用符号–这与随着时间推移添加的其他功能(如迭代器)是一样的。它为以向后兼容的方式添加支持提供了一种很好的方法。而且大多数情况下,只有库作者才会使用符号,一般的应用程序开发人员根本不需要直接接触符号。
对于垃圾回收语言,析构函数在大多数情况下不能同步调用,因为虚拟机必须首先确保对象不可访问。因此,这样做的确定性不高,而且还会暴露 JS 虚拟机的内部结构。为此,JS 已经有了 WeakRef 和 FinalizationRegistry。
https://waspdev.com/articles/2025-04-09/features-that-every-… https://waspdev.com/articles/2025-04-09/features-that-every-…
但即使 Mozilla 也不建议使用它们,因为它们很不稳定,在不同的引擎中可能会有不同的效果。
因为这种方法也适用于不是类实例的东西。
JavaScript 中不存在匿名属性。你的问题没有道理。这还能是什么?
因为 JavaScript 不文明。
他们的第一个例子是关于在一个函数中必须有一个 try/finally 块,就像这样:
这样,即使 reader.read() 引发错误,读取锁也会被解除。
这是否只适用于长时间运行的进程?在浏览器环境中,或者在出错时终止的 cli 脚本中,进程退出时是否会解除锁定?
规范只是说,当一个程序块 “完成 ”其执行时,无论执行过程如何(正常完成、异常、中断/继续语句等),处置都必须运行。这一点对于 “using ”和 “try/finally ”都是一样的。
当进程被强制终止时,其行为本质上超出了 ECMAScript 规范的范围,因为此时解释器无法采取任何进一步的操作。
因此,会发生什么取决于你所说的是哪种对象。文章中的例子说的是网络平台流规范中的 “流”。从这个意义上说,流是一个只存在于 JS 解释器中的 JS 对象。如果 JS 解释器消失了,那么询问锁是锁定还是解锁就毫无意义了,因为锁已经不存在了。
如果你说的是某种操作系统分配的资源(例如分配的内存或文件描述符),那么当进程终止时,无论终止是如何发生的,即使进程本身没有采取任何行动,操作系统通常也会提供某种清理。当然,具体细节要视平台而定。
浏览器网页是典型的长时间运行程序!至少对 Notion 来说,浏览器标签页的运行时间(天到周)通常比服务器进程的运行时间(小时到下一次部署)要长得多。它们像服务器一样是一个事件循环,通常有多个子进程,而不是一个运行到完成的 CLI 工具。错误不会终止网页。
未处理错误的执行顺序是明确定义的。错误会在调用堆栈中运行 catch 和 finally 块,如果回到事件循环,系统通常会将其分派给 “未捕获异常”(同步上下文)或 “未处理的拒绝”(异步上下文)处理函数。在 NodeJS 中,默认的错误处理程序会退出进程,但你也可以用自己的行为来替代,这在长期运行的服务器中很常见。
总之,这是可行的,因为终止处理程序是在栈顶调用的,在栈通过 finally 块解开之后。
这对于可能有不同内存支持的 WASM 类型的资源管理非常有用。
是的,非常适合这种使用情况–内存管理;DisposeStack 允许 “移出 ”当前作用域,这一点也很好,非常方便。
我在 quickjs-emscripten(我的 quickjs in wasm thingy,用于浏览器中不受信任的代码)中采用了它,但发现 TypeScript 编译器和 Babel 之间的不同实现导致它无法可靠地为我的用户所用。最后,我写了这段代码,试图解决多填充问题;我的编译器将使用 Symbol.for(‘Symbol.dispose’),但其他编译器可能会选择不同的符号……
https://github.com/justjake/quickjs-emscripten/blob/aa48b619…
为什么他们不采用更符合人体工程学和更成熟的 `defer` 模式呢?
defer “也不需要改变成千上万个对象的 API 就可以使用,相反,现在你必须为任何类似对象的资源添加一个方法,或者对于不是对象的东西,你甚至不能使用这个功能。
这和 Java 中的资源尝试一样吗?
它与 C# 的 “using 声明 ”类似,但更多地受到 C# 的 “using 声明 ”的启发,using 块是 C# 版本的 try-with-resource 的演化:“using ”声明不引入自己的块/作用域。
最初的提议引用了 Python 的上下文管理器、Java 的 try-with-resource 以及 C# 的 using 语句和声明:https://github.com/tc39/proposal-explicit-resource-managemen…
只有 C# using 和 Python 上下文管理器的经验,但与 C++/Rust 风格的 RAII 相比,它们似乎总是一种不令人满意的解决方案。我的意思是,我知道这些都是 GC 语言,但为什么不能在常规作用域的末尾发生这种情况呢?为什么一定要有这些人为的新作用域呢?
这就是更简单的 C# 版本(以及本提议)的好处:它与它所在的任何块绑定,不需要自己的作用域/缩进。
using var stream = GetStream(); 并没有引入新的(词法)作用域,避免使用意味着类型系统要么必须理解移动语义(以防流被返回或移动到别处,但那样的话,共享怎么办?
是的,正如有人指出的,它受 C# 启发,这是一个 C# 示例:
如果你需要更精细的控制,你仍然可以进行包装,或者在最后执行其他操作。
你甚至可以像这样嵌套它们:
编辑:没有读完整篇文章,javascript 版本相当不错!
这似乎容易出错,至少有两个原因:
* 如果不小心使用了 `let` 或 `const` 而不是 `using`,一切都会正常工作,但会无声地泄漏资源。
* 包含资源的对象需要手动定义 `dispose` 并在其子对象上调用。忘记这样做会导致资源泄漏。
这看起来就像把 defer 打扮得像 RAII。
这里有一些关于脚枪的相关讨论:
https://github.com/typescript-eslint/typescript-eslint/issue…
https://github.com/tc39/proposal-explicit-resource-managemen…
我认为最终会在某个地方制定相关的 lint 规则,而且许多使用这种现代功能的人可能会通过 eslint 使用静态分析来帮助降低风险,但在它得到更多的认可和理解,以及 lint 规则得到充实和广泛采用之前,这里肯定存在风险。
https://github.com/typescript-eslint/typescript-eslint/issue…
在我看来,流行的 lint 库只要继续并添加该规则,就会带来很大的不同。
您所描述的已经是目前的现状。这个提议仍然是一个很大的改进,因为当你意识到要使用它时,它使资源管理不那么容易出错,并通过符号_使机制标准化。这使得工具可以根据类型信息对您描述的情况进行校验。
在 .NET 领域,这种设计已经有了很好的先例–如果它很糟糕或者明显不如 `defer`,我相信 Chrome 工程团队一定会注意到的。
C# 的优势在于它是一种类型化语言,这使得编译器和集成开发环境可以在我提到的情况下发出警告。JavaScript 不是类型化语言,这就限制了此类警告的可能性。
总之,我并没有说它 “不如 defer”,我只是说在 Rust 和 C++ 等语言中,它似乎比 RAII 更容易出错。
编辑:如果我错得离谱,请原谅(我不使用 C#),但相关的代码分析规则看起来像 CA2000 和 CA2213。
> 不管怎么说,我并没有说它 “不如延迟”,我只是说在 Rust 和 C++ 等语言中,它似乎比 RAII 更容易出错。
确实如此,但如果有高级 GC,RAII 确实不是一种选择,因为它是基于生命周期的,需要确定性地销毁单个对象,而高级 GC 的大部分性能都来自于不这样做。
大多数 GC 语言都有某种终结器(javascript 也是如此:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe……),但这些终结器并不可靠,而且在用于清理时往往会产生微妙的脚注。
实际上,在 C# 中很容易忘记,这取决于你是否记得。这两个分析器默认是禁用的,容易出现误报和漏报。他们对一堆 .NET 类的已知行为进行了硬编码,使其根本无法使用。
在将一次性元素作为成员的情况下,仍然很难正确处理。如果传递进来的可处置成员也会被处置,这一点并不明显,而且正确与否取决于具体情况(例如,一个基于字符串的 TextWriter 被传递到一个基于字节的 Stream 中),你将需要处理双重处置。
此外,C# 的析构函数在处理本地资源(如文件描述符)时也是不得已而为之。
> 此外,C# 的析构函数在处理本地资源(如文件描述符)时是不得已而为之。
没错,我本来也想提到这一点,但我看到 JS 也有 “终结寄存器”,它似乎在 JS 中提供了终结器支持,所以我觉得这并不是本质区别。
正是如此。不知道你为什么会被降权。
他们试图解决的问题是,程序员可能会忘记用 try 封装对象创建。但他们的解决方案只是在路上踢了一脚,因为现在程序员可能会忘记写 “using”!
我在想,更好的解决方案应该是简单地添加一个无操作的 dispose() 默认实现,并在任何对象到达 refcount=1 时调用它,然后完全去掉 “using ”关键字,因为这样程序员就不会忘记写 “using ”了。但我又想起来,JavaScript 没有 refcounts,我们不能假定对象被传递的函数调用没有保留对它的引用,而期望它以后仍以未处置的状态存在。
反过来说,如果真的没有 “好 ”的解决方案来检测这种 “逃脱”,那就意味着,在新系统下,写 “using ”一定是危险的–它可能导致在某个函数调用将对象的引用保存在某个地方时,dispose()被调用,从而期望它以后仍以未处置状态存在。
我觉得将资源管理与垃圾回收混为一谈是没有意义的。这里的清理操作更像是释放锁、删除临时文件或关闭连接。这并不会导致缺乏安全性。这些资源已经需要处理这些未初始化的状态。例如,考虑一下锁管理对象。不能因为拥有管理器资源的引用,就认为自己拥有锁。拥有需要某种初始化的对象是完全正常的。
你的论点似乎是,新的 “使用 ”不会产生任何新的、危险的行为。但我已经同意这一点了–我是说,它并没有使任何东西比以前更好(忘记写 “try ”的程序员同样也会忘记写 “using”,所以程序员所需的纪律水平并没有改变),另外,如果你错误地说服自己,认为一直在所有地方都写 “using ”是安全的,那么你就会引入错误,比如过早释放锁。
为什么程序员会这样说服自己呢?因为如果不这样做,“using ”就没有任何好处了,除了用 “try … catch (x) { for (o of objs) o.dispose(); }”来包装函数体的语法稍微甜美一些。
> 我认为将资源管理与垃圾回收混为一谈是没有意义的。
内存只是一种资源,当它的生命周期结束时,并不需要紧急处理(比如说,与锁不同)。GC 是一个管理这种资源的系统,或者说是任何具有相同非紧急属性的资源。例如,你可以为数据库连接池设想一个基于 GC 的资源管理系统。
> 你不应该因为拥有管理器资源的引用就认为自己拥有锁。
有些语言必须以这种方式编写代码,即在对某样东西进行操作之前,需要始终检查它是否处于有效状态,但这是很不幸的,因为有些语言(比如 C++,它在很多其他方面都很糟糕)可以维护这样一个不变式:如果你有一个对锁的引用,那么相关的资源就是被锁定的。让无效状态变得不可代表(Make Invalid States Unrepresentable)这一总体思路是提高代码质量的绝佳方法,因此我们应该一直在寻找将其融入尚不具备此功能的语言的方法。解析不验证(Parse Don’t Validate)也是同样的基本思想。
还有一点是,JS 一直在尽力避免以任何方式暴露 GC。例如,你不能枚举 WeakSet,因为这会导致行为依赖于 GC。在对象被收集时调用 dispose 会非常明确地导致 GC 产生语义影响,我认为这与 JS 的理念背道而驰。
FinalizationRegistry 是 5 年前添加的。
是的,它和 WeakRef 是例外情况,但它们是唯一的例外情况,其设计目的是可以否认的–如果你删除 globalThis.WeakRef;和删除 globalThis.FinalizationRegistry;,你就回到了完全不公开 GC 的状态。在规范中,WeakRef 甚至有一个特殊的例外,那就是 .constructor 属性是可选的,特别是这样一来,将弱引用交给某些代码并不一定能使其创建更多的弱引用,因此你也可以限制观察哪些对象的 GC。
但另一个问题是,规范并没有明确规定何时可以收集对象,也不允许程序员以任何方式控制 GC,这意味着依赖 FinalizationRegistry 可能会导致泄漏/无法最终使用未使用的资源(很糟糕,但有时还可以忍受),或者更糟糕的是,出现免用后错误(完全致命)–参见 https://github.com/tc39/ecma262/issues/2650
终结器不是析构函数。首先,终结器无法访问被 GC 的对象。但更重要的是,规范允许引擎在对象被 GC 后很长时间内调用终结器,或者永远不调用终结器。
对于非关键资源的清理,它们基本上是一种不错的便利。你不能依赖它们。
是吗?恭喜你,你知道 finalizer 是什么吗?
我是在回答这个问题:
> 这将非常明确地导致 GC 产生语义影响,而我认为这与 JS 的理念背道而驰。
你是否不同意,finalizer 正是提供了这样的功能,因此不会 “强烈违背 JS 理念”?
我的意思是,正如提案中指出的那样,这明确违反了该理念:
> 因此,W3C TAG 设计原则建议不要创建暴露垃圾回收的 API。最好是将 WeakRef 对象和 FinalizationRegistry 对象用作避免过多内存使用的一种方法,或用作防止某些 bug 的后盾,而不是用作清理外部资源或观察已分配资源的正常方法。
很公平,我并不知道这一点。但即便如此,为特殊情况设计的古怪功能和 “这是处理资源的新方法 ”之间还是有很大区别的。
而且,这种做法有违 JS 的理念,这一点已经非常明确了:
https://w3ctag.github.io/design-principles/#js-gc
我写过一篇文章,其中包含更多示例 https://waspdev.com/articles/2025-05-17/js-destructors-or-ex… 。实际上,这是 https://github.com/tc39/proposal-explicit-resource-managemen… 的简化版。
不确定这是否受到 C++ RAII 的启发。RAII 看起来非常优雅。
[Symbol.dispose]() “把我搞糊涂了
> 不确定这是否受到 C++ RAII 的启发。
并非如此。两者都是执行确定性资源管理的方法,但 RAII 是确定性资源管理的一个分支,大多数 GC 语言都无法使用,因为它们没有确定性的对象生命周期。
这是受 Java、C# 和 Python 中类似构造的启发(实际上是从 C# 中移用的,并根据 JS 的能力做了一些调整),只要这些构造与 RAII 相关,它们就离 RAII 很远,至少在 Python 中是这样: CPython 历史上使用析构函数进行资源管理,这些析构函数大多会在 refcount 降为零时被可靠、确定地调用。
但是
1. 对于 Python 的非 refcounted 替代实现来说,这是一个问题; 2.
2. 这对 CPython 最终(即使不太可能)摆脱 refcounting 的可能性是个问题; 3.
析构函数与引用循环的交互方式令人尴尬
4. 即使在引用计数语言中,析构函数也与对象复活等常见的终结器问题有关。
因此,Python 最终引入了上下文管理器作为确定性资源管理的手段,并发布了避免依赖引用计数和 RAII 风格管理的指南。
感谢您的解释
我刚刚写了一篇关于此功能的博文(https://morsecodist.io/blog/typescript-resource-management)。我很喜欢它,我觉得它在生态系统中还没有流行起来。
几年前,我曾尝试为 JS 编写一个 “使用 ”工具:https://gist.github.com/davidmurdoch/dc37781b0200a2892577363…
它不太符合人体工程学,所以我从未尝试在任何地方使用它。
错误可能是试图编写通用的 `using`。根据我的经验,使用高阶函数或宏来清理作用域的语言倾向于直接在最底层功能上构建高级实用程序,这可能会有点重复,但通常不会太糟糕。
因此在本例中,与其在更通用的 `try/except` 上构建通用的 `using` ,不如构建一个 `withFile` 回调。虽然重复性会更高一些,但因为你清楚地知道你在处理什么,所以出错的几率会小很多,而且你也不需要寄希望于有一个现成的协议。
例如,由于 `withFile` 将专门用于文件交互,因此它可以将所有文件操作封装为基于承诺的方法,而不必混合使用承诺和传统回调。
当然,with* 模式很好。我只是想在 JS 中使用 C# 的一次性模式。
我希望 “资源 ”指的是内存。
如果能有一个低级借用检查的 JS 子集,作为 JS 的一部分,这样你就可以用它重写你的热循环了。
当然,你也可以直接从’./low-level.wat’(或 .c,并自动编译为 WASM)中导入 *。
我还需要进一步研究,但我创建了 OneJS [1](有点像 React Native,但适用于 Unity),乍一看,这对我们来说非常完美(?) 对于 Unity 来说,网格、渲染纹理、计算缓冲区和 NativeContainers 分配都需要在 JS 之外进行适当处理,这似乎非常方便。通过在词法作用域强制处理,我们可以在长时间的编辑器会话或大量热加载时保持内存更稳定。
[1] https://github.com/Singtaa/OneJS
这是一个即将推出的很棒的功能,几个月前我在这里写了一些关于它的实用建议(一个现实的例子,如何与 TypeScript/vite/eslint/neovim/etc… 一起使用):https://abstract.properties/explicit-resource-management-is-…
使用 “提议太糟糕了。[1]
完全没有理由为显式资源管理引入新的变量绑定。
现在它又不支持重构等。
它应该是
类似于 for-of。
[1] https://github.com/tc39/proposal-explicit-resource-managemen…
[2] https://github.com/tc39/proposal-explicit-resource-managemen…
这就引入了一个新的变量绑定,而且在所有情况下都会变得更加冗长,但却没有特别的好处。我们已经多次讨论过析构;天真地使用析构的问题在于,第一次遇到析构时并不清楚析构对象或析构出来的字段是否会被丢弃。提案中通过 DisposableStack 支持去结构化。这个问题已经争论得面红耳赤,你可以在你的链接中看到作者的回应。
> 析构已经被讨论过很多次了;天真地使用它的问题在于,第一次遇到它时并不清楚析构对象或析构出来的字段是否会被丢弃。
是的,而这个方法很容易解决这个问题!
没有歧义。非常清楚。
> 这是在引入新的变量绑定
const、let、var 是变量绑定,有作用域和可变性规则。
using 则是新增的变量绑定。我已经记不起关于可变性的规定了。
using-of将保留这一组。
> 在所有情况下,“using-of ”都会更冗长,但没有任何特别的好处。
见上文。
额外的好处是,对象的生命周期更清晰,也更有利于快速清理。而不是埋藏在前后各 50 行的代码块中。
> 这个问题已经被诉讼得体无完肤,你可以在你的链接中看到作者的回应。
完全正确。不幸的是,所有者决定继续使用它。
尽管很别扭,不合格。
顺便说一下,我使用 `using` 大概有一年多了,我发现只有一种情况下我想为资源创建一个新的显式作用域。在所有其他情况下,让资源与包含它的函数/循环/其他东西一样存在是最明确的选择,而强制创建一个新的作用域会让我的代码更混乱、更冗长、更缩进。
尤其是在需要创建显式作用域的情况下,我可以使用常规块来创建,例如
在多次使用 Python 的 `with` 块后,我发现我更喜欢 Javascript 的方法,即不创建单独的作用域,而是使用现有的作用域机制。
我更喜欢 “defer”,但 “using ”总比什么都没有好。
using 更灵活,因为它不需要调用函数,而只需赋值一个实现[[dispose]]的变量即可。
只有当你的对象实现了 [[Symbol.dispose]]。如果没有,你就需要创建一个(就像文章示例中的包装器),或者编写一些模板来显式地创建和使用 DisposableStack()。
因此,在使用时需要学习和使用一些语言特性,而且(可能更重要的是),要么应用程序开发人员和库开发人员必须同时在这一点上达成一致,要么应用程序开发人员必须在每次调用包装器或 DisposableStacks 时添加一些模板。
顺便提一下,大多数具有此类功能的后端框架和库都已经使用了多填充的 Symbol.dispos,如果你在支持它的环境中开始使用它,它就会透明地与真正的东西一起工作。我已经通过转置语法使用该功能一段时间了,效果相当不错。
在前端,我认为这需要更长的时间才能普及,但我相信很快就会实现。
它们基本上是对立的。
与 `defer` 不同,`using` 更方便,因为它无需额外调用就能注册清理。
当然,你也可以通过将函数封装为可处置字面量或使用 DisposableStack/AsyncDisposableStack 实用程序类型(该提案还添加了这些类型)来桥接回调。
这是否经过了 TC39 的讨论,还是这只是 V8 的一个功能?
https://github.com/tc39/proposal-explicit-resource-managemen…
谢谢!
Bun 似乎有这个功能,而且没有使用 V8。
也许只是我的错觉,但 [Symbol.dispose]() 似乎是为对象添加该功能的一种非常笨拙的方法。下面是他们的示例:
首先,我必须温习一下新的对象定义速记: 简而言之,你可以使用一个变量或表达式,通过括号定义一个键名,比如:let key = “foo”; { [key]: “bar“},其次,你不必写 { ”baz” : function(p) { … } } ,你可以改写 { baz(p) {…} 。}. 好的,明白了。
因此,如果我没有看错上面的示例,他们正在实现的基本上是基于接口的新 “资源 ”对象定义。(如果它走起路来像鸭子,还会嘎嘎叫……)。
要创建一个 “资源”,就需要在 POJO 中添加一个新的神奇方法,这个方法不是用标准名称(如 Object.constructor() 或 Object.__proto_)来标识的,而是用 “Symbol.dispose ”求值的结果来标识的。因此,上面定义的 { [Symbol.dispose]() {…} } ,显然,“using ”关键字会在对象退出作用域时调用它。
我的理解正确吗?
我认为,JavaScript 的正确做法应该是创建一个新的对象专用修改器关键字,就像 getters 和 setters 的工作方式一样,或者创建一个名为 “Resource ”的新全局对象,其中包含可以覆盖的所需方法原型。
使用 “符号 ”实在是太奇怪了。处置资源与 Symbol 创建唯一标识符的核心目的毫无关系。另外,它看起来很不雅观,肯定会引起混淆。
还有其他任意方法名被关键字调用的例子吗?这不是一个函数参数,就像 async/await 用来返回 Promise 一样,这只是一个使用 Symbol 定义名称的对象的随机方法。真奇怪!
也许我遗漏了什么。
符号是在语言标准或应用代码中引入新 “协议 ”的一种非常安全的方式。这是因为符号永远不会与现有的类定义发生冲突。如果我们为方法使用字符串名称,那么现有代码的语义就会发生变化。
下面是我的 NodeJS 22 在使用 `Symbol.<tab>` 时提供的众所周知的符号:
JS 使用 “知名符号”[1] 来扩展/重写对象的功能已有约 10 年的历史。例如,如果一个对象具有 `[Symbol.iterator]` 属性,那么它就是一个可迭代对象。符号是有效的对象键;它们不仅仅是字符串别名。
[1] https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe…
是的,你漏掉了一些东西。你不应该调用这些方法,它们是为运行时准备的。
更具体地说,JavaScript 在检测到您退出 “using ”声明的范围时,会调用 [Symbol.dispose]。
> 在 “using ”声明中,[Symbol.dispose]不是以标准名称(如 Object.constructor() 或 Object.__proto__)来标识的。
__proto__ 是一个可怕的错误。谷歌 “prototype pollution”(原型污染)有太多的例子可以链接。在以 JSON.parse() 为主要数据反序列化机制的鸭子类型语言中,你不能相信任何纯字符串键的值。
> 创建一个名为 “Resource ”的新全局对象,其中包含可覆盖的所需方法原型。
如果你想让现有的类成为 Resource 的子类,这些方法可能会与其他方式已经使用的现有方法相冲突。
> 要创建一个 “资源”,你需要在 POJO 中添加一个新的神奇方法,而不是用一个标准名称来标识[……]这与 Symbol 创建唯一标识符的核心目的毫无关系。
在 JS 中引入 Symbol 的核心目的和最初原因是创建不冲突但众所周知的标准名称的能力,因为该语言最初没有为此类名称保留命名空间,因此无法知道任何名称是否可用(而且不会被猴子修补到现有类型上,包括本地类型)。
> 是否还有其他例子说明任意方法名被关键字调用?这不是一个函数参数,就像 async/await 用来返回 Promise 一样,这只是一个使用符号定义方法名的对象的随机方法。真奇怪
符号.迭代器 “被 ”for…of “调用,这简直就是符号的原始用例。
> 我认为,JavaScript 的正确做法应该是创建一个新的对象专用修改器关键字,就像 getters 和 setters 的工作方式一样,或者创建一个名为 “资源 ”的新全局对象,其中包含可以覆盖的所需方法原型。
真的:你在说什么?
> 你在说什么?
他们在普通 JS 对象中添加了 get 和 set 关键字,以识别 getter 和 setter。所以只需添加一个 dispose 关键字即可。就像这样
干净多了。
没错,所以你不需要通过众所周知的唯一名称(也就是符号的全部意义)来获取属性,而是需要首先解析属性描述符,检查一个新的完全不必要的标志,然后才能调用方法。当然,如果有人忘记了该属性,或者在调用父方法时没有检查该属性,就会导致调用失败。
现在,你在一个有固定名称的属性上浪费了整个关键字,而已经使用该名称但语义不同的代码库又无法添加 “using ”兼容性。
在 Python 中,上下文管理器有 __enter__ / __exit__ 和更通用的 Protocol(协议)概念,可以用于任何事情,但我并没有考虑过我是多么幸运,因为那个定义看起来丑陋无比。就连 Perl 也惊恐地看着 JS 所犯下的罪行。
> 即使是 Perl 也对 JS 所犯的罪孽瞠目结舌。
大笑。多好的一句话。
我同意,这越来越荒谬了。我不明白为什么需要这么多速记符号。它们所做的只是为了省几个按键而使代码变得难以辨认。这样使用符号对象实在是太难看了。
我请求 JSLand 放弃 “使用 ”一词及其所有衍生词。不过这个功能很酷,我很期待使用它。
他们只是采用了 C# 沿用已久的语法而已
好吧,但这并没有减少它的无意义性。
async、await、let、var、const、try、catch、yield 都是有意义且精确的关键字。
而 “使用”“using ”则完全不是一个精确的词。对于任何非 C# 人士来说,它都可以用来替代上述任何一个词!
JavaScript 新特性:分段故障、内存泄漏、内存损坏和核心转储。
不,它仍然不允许你手动分配或释放内存。
所以……放弃
这些东西介绍得越多,我就越想在所有地方都使用 Rust。我并不是说这就是 Rust 的方式–这其实让人想起 Python/C#。但是,Rust 做得更好。
如果我们继续沿着这些道路走下去,Rust 实际上会成为更简单的语言,因为它在设计之初就考虑到了所有这些目标,而不是把它们塞回去。
先是 “为什么动物会不断进化成螃蟹?”,现在又变成了 “为什么编程语言会不断进化成螃蟹?”
我不太同意,或许我们都同意。
我认为 JavaScript 应该保持简单。如果我们真的需要这个功能,我们可以引入 defer。但要与 golang 中的功能保持 1:1。这种介于 python 和 golang 之间的功能对于 JavaScript 的本意来说实在是太多了。
我绝对认为网络需要第二语言,它需要类型、资源管理和各种结构性防护栏。但继续对 JavaScript 进行黑客攻击并非如此。
你这话是什么意思?drop “是另一种语言中的语言结构吗?
是的,它在 Rust 中叫做 Drop:https://doc.rust-lang.org/std/ops/trait.Drop.html
这条评论中也有这个词,读起来像是 Gen Zed 的俚语。
https://news.ycombinator.com/item?id=44012969
Drop?
这是一个令人讨厌的用法,因为你永远不知道它的意思是 “出现了新东西 ”还是 “旧东西停止供应了”。
新下降
下降和双周
还是用超级恶棍语言c++实现的?
这取决于 Javascript 引擎是用哪种语言实现的。对于 v8 来说就是 c++。我同意谷歌是当今的超级反派,但其他人也使用 c++,所以我认为称其为超级反派语言并不公平……
c++ 是一种令人憎恶的语言,来吧,让我们保持诚实,我们现在都知道这一点。它甚至不应该再被教授。
它是所有超级恶棍计算机语言之母。
我们现在必须停止伪善了。
上下文管理器:存在。
联署材料:放弃,但我们不能占用一个可能被占用的名字,“符号 ”就是胜利!
太尴尬了
> JS: drop but we couldn’t occupy a possibly taken name, Symbol for the win!
你迟到了十年?
这正是符号和 “知名符号 ”的意义所在,也是 ES6 引入它们的原因。
我没有使用它是因为没有必要。
资源范围是一项重要特性。上下文管理器(在 python 中)是日常任务的面包和黄油。
它之所以笨拙,不是因为 Symbol,而是因为它引入了与现有隐式作用域绑定的新语法。根据 Go 的经验,这有点脆弱。显式作用域的可预测性更高。
不,Symbol Traits 已经在 javascript 中应用了很长时间,例如 Symbol.iterator
决定新名称的是 “处置 ”部分。
Traits 就像旱冰鞋是汽车一样。
当我发现这个特性时,我在代码库中到处寻找可以使用它的地方。结果发现,无论是 Web 还是 Node.js,大多数 JS API 都不需要它,因为它们会自动关闭资源。我调用 .close() 的几次都使用了回调,如果重写为作用域,就不那么简洁/直观/正确了。我还没能利用这项功能清理哪怕一行代码 🙁
我只是个业余爱好者,为了 “改进 ”不同网站上的东西,我编写了一些小程序。作为其中的一部分,我需要一个卸载/取消功能–因此,我用我的 “草根 ”代码编写了 “window.mything = {something}`”,根据以前对 python/c#/go 等软件的使用经验,我推测我可以编写 “delete window.mything`”,它会自动调用我编写的函数来完成这项工作。因此,新的 `[Symbol.dispose]()`特性/函数就能实现我想要的功能–但实际上,这也没什么大不了的,因为我所要做的就是编写一个接口规范的 `{}.remove()` 方法,并在需要时调用它。
(这一段已经跑题了,但还是……)下面是我在 .d.ts 文件中的具体接口。创建该文件的原因是我喜欢类型化语言(即 TypeScript),但我不想为这么简单的事情安装像 node-js 这样的东西。所以我意识到 vscode 可以/将检查 js 文件为 ts,所以在一些地方(比如这里)我需要 “键入 ”一些东西–然后我发现了一些关于 svelte 源代码使用 js-docs 代替 typescript 来键入代码库的帖子。所以我基本上就是这么做的…
因此,在你可以使用这一功能的地方,你很可能已经有了一个 “接口”,用于在完成后关闭事物(即使你没有在类型系统中定义该接口)。
如果你使用的是 withResource() 模式,那么你已经有效地做到了这一点,所以是的。如果你使用的是 try/finally,也许值得再看一看。
它似乎还没有被广泛使用。我用它来为一个下载临时文件的加载程序清理临时文件。
它看起来与 golang 的 defer 最相似。它在离开当前作用域时运行清理代码。
它与 try/finally、c# 的 “using ”和 Java 的 try-with-resources 不同的是,它不要求在作用域的起始位置声明待处理对象(尽管这样做可以说使代码更容易理解)。
它与某种析构函数的不同之处在于,处置调用与作用域而非对象生命周期相关联。如果存在其他引用,对象的生命周期可能会超过作用域,因此这两者是不同的。
如果你喜欢 golang 的 defer,那么你可能会喜欢这个。
> 这看起来与 golang 的 defer 最相似。它在离开当前作用域时运行清理代码。
它和 go 的 defer 完全不同: Go 的 defer 是函数作用域并注册一个回调,而 using 是块作用域并注册一个具有明确定义协议的对象。
> 它不同于[…] c# 的 “using”。
它几乎是 C# 的 “using ”声明(相对于 using 语句)的直接复制:https://learn.microsoft.com/en-us/dotnet/csharp/language-ref….
这一点也可以从提案本身(https://github.com/tc39/proposal-explicit-resource-managemen……)中看出,提案引用了 C# 的 using 语句和声明、Java 的 try-with-resource 以及 Python 的上下文管理器作为先有技术,但只提到 Go 的 defer 可以通过 DisposableStack 和 AsyncDisposableStack(这些类型特别受到 Python ExitStack 的启发)来模仿、