Date 已过时,Temporal 正流行

Date即将终结,彻底消失,成为Web平台中“已弃用”的组件——这意味着它将永远存在,但若能避免,你就不该再使用它。不久之后,我们终于将迎来全面替代Date的对象:Temporal。

💬 206 条评论 | /|

时间总让我们显得愚蠢,而JavaScript在这方面也毫不逊色。说实话,我向来不太介意后者——事实上,若你参加过全民JavaScript课程或订阅过新闻通讯,便知我其实相当享受JavaScript的这些小怪癖,信不信由你。

我喜欢能看到它的缝隙;我喜欢即使ES-262规范看似严谨如铁,只要知道哪里找,依然能窥见数百位开发者在语言演进中做出的所有明智错误决策。JavaScript有个性。当然,它未必总能完全按预期行事,但说真的,一旦你真正了解它,就会发现JavaScript的魅力所在!

不过有处设计让我瞬间破功。


// Numeric months are zero-indexed, but years and days are not:
console.log( new Date(2026, 1, 1) );
// Result: Date Sun Feb 01 2026 00:00:00 GMT-0500 (Eastern Standard Time)

Date构造函数。


// A numeric string between 32 and 49 is assumed to be in the 2000s:
console.log( new Date( "49" ) );
// Result: Date Fri Jan 01 2049 00:00:00 GMT-0500 (Eastern Standard Time)

// A numeric string between 33 and 99 is assumed to be in the 1900s:
console.log( new Date( "99" ) );
// Result: Date Fri Jan 01 1999 00:00:00 GMT-0500 (Eastern Standard Time)

// ...But 100 and up start from year zero:
console.log( new Date( "100" ) );
// Result: Date Fri Jan 01 0100 00:00:00 GMT-0456 (Eastern Standard Time)

我对Date怀有极度厌恶


// A string-based date works the way you might expect:
console.log( new Date( "2026/1/2" ) );
// Result: Date Fri Jan 02 2026 00:00:00 GMT-0500 (Eastern Standard Time)

// A leading zero on the month? No problem; one is one, right?
console.log( new Date( "2026/02/2" ) );
// Result: Date Mon Feb 02 2026 00:00:00 GMT-0500 (Eastern Standard Time)

// Slightly different formatting? Sure!
console.log( new Date( "2026-02-2" ) );
// Result: Date Mon Feb 02 2026 00:00:00 GMT-0500 (Eastern Standard Time)

// A leading zero on the day? Of course; why wouldn't it work?
console.log( new Date('2026/01/02') );
// Result: Date Fri Jan 02 2026 00:00:00 GMT-0500 (Eastern Standard Time)

// Unless, of course, you separate the year, month, and date with hyphens.
// Then it gets the _day_ wrong.
console.log( new Date('2026-01-02') );
// Result: Date Thu Jan 01 2026 19:00:00 GMT-0500 (Eastern Standard Time)

Date糟透了。它就像在去学校的车上仓促抄袭Java作业的产物,不仅答案全错,连页眉名称都搞砸了:Date根本不代表日期,它代表的是时间。内部存储时,日期被转换为称为 时间值 的数值:Unix时间戳,精确到千分之一秒——好吧,Unix时间确实必然包含日期信息,但即便如此:Date类本质上表示时间,你只能由此推断日期。恶心。

元素周期表

// Unix timestamp for Monday, December 4, 1995 12:00:00 AM GMT-05 (the day JavaScript was announced):
const timestamp = 818053200;

console.log( new Date( timestamp * 1000 ) );
// Result: Date Mon Dec 04 1995 00:00:00 GMT-0500 (Eastern Standard Time)

“日期”和“时间”这类词本有其明确含义,但——算了,JavaScript 随它去吧。

早在1997年,Java就已弃用他们的Date类,而此时JavaScript的Date才刚被放任到毫无防备的世界中;反观我们,却从此背负着这个烂摊子。正如你在此处所见,它在解析日期时存在严重不一致性。除了本地时区和格林威治时间外,它对其他时区毫无概念——这在“全球性”都写在网络名称里的时代实在不理想。更别提Date对象仅支持公历模型。它完全不理解夏令时概念——好吧,我明白,但开发者又不是机器。所有这些缺陷导致人们普遍依赖第三方库来规避问题,其中某些库体量极其庞大;这种性能消耗已对网络造成切实可测的损害。

但这些都不是我对Date的核心诟病。我的不满远不止于解析语法、“开发者体验”、“全网性能损耗”或“日期定义”这些层面。我对Date的批判是灵魂深处的抗拒——使用它意味着背离时间本质的根本属性

所有JavaScript的原始值都是 不可变的 ,这意味着值本身无法被改变。数字值3永远只能代表“三”的概念——你无法让true表示除“真”之外的任何含义。这些值具有具体、铁板钉钉的现实世界意义。我们清楚三的本质,它不可能是其他非三的实体。这类不可变数据类型采用 值存储 机制,即表示数字值3的变量实质上“包含”——并因此表现为——数字值3本身。

当不可变值被赋给变量时,JavaScript引擎会创建该值的副本并存储在内存中:


const theNumber = 3;

console.log( theNumber );
// Result: 3

这完全符合人们对“变量”的常见认知模型:theNumber“包含”3

当我们用theNumber绑定的值初始化theOtherNumber时,这种认知模型依然成立:系统再次创建并存储了一个3。此时theOtherNumber可视为包含其独立的3


const theNumber = 3;
const theOtherNumber = theNumber;

console.log( theOtherNumber );
// Result: 3;

当然,当我们修改theOtherNumber关联的值时,theNumber的值不会改变——因为我们始终在处理两个独立的3实例。


const theNumber = 3;
let theOtherNumber = theNumber;

theOtherNumber = 5;

console.log( theOtherNumber );
// Result: 5;

console.log( theNumber );
// Result: 3

修改theOtherNumber的绑定值时,你并非在改变3本身,而是创建了一个新的不可变数值并将其绑定至原位置。因此当尝试修改const声明的变量时会报错:


const theNumber = 3;

theNumber = 5;
// Result: Uncaught TypeError: invalid assignment to const 'theNumber'

你无法改变const的绑定关系,更绝对不能改变3的含义。

创建后被修改的数据类型称为 可变类型 ,意味着数据值本身可被修改。对象值——任何非基本类型值,如数组、映射或集合——均属于可变类型。

变量(以及对象属性、函数参数、数组/集合/映射中的元素)无法像我们理解上例中theNumber“包含”3那样“包含”对象。变量只能包含原始值或 引用值 ,后者是指向该对象在内存中存储位置的指针。当你将对象赋值给变量时,并非创建该对象的副本,而是使标识符指向内存中对象的存储位置。因此,绑定在const声明变量上的对象仍可被修改:引用值本身不可变,但对象内部的值可以改变:


const theObject = {
	theValue : 3
};

theObject.theValue++;

console.log( theObject.theValue );
// Result: 4

虽然无法改变const的绑定关系,但可以修改被引用的对象。

当引用值从一个变量赋值给另一个变量时,JavaScript引擎会创建该引用值的副本——而非对象值本身(这与原始值创建独立副本的方式不同)。两个标识符指向内存中同一个对象——通过任一引用对该对象所做的修改都会反映在其他引用上,因为它们都指向同一事物:


const theObject = {
	theValue : 3
};

const theOtherObj = theObject;

theOtherObj.theValue++;

console.log( theOtherObj.theValue );
// Result: 4

console.log( theObject.theValue );
// Result: 4

正是JavaScript日期处理令我困惑之处。尽管代表着“日历上的特定日期”,JavaScript的日期值却是可变的——Date是构造函数,通过new调用构造函数必然生成对象,而所有对象本质上都是可变的:


const theDate = new Date();

console.log( typeof theDate );
// Result: object

尽管“2026年1月1日”与“三”或“true”一样是现实世界中不可变的概念,但我们唯一能表示该日期的方式却是使用可变数据结构。

这也意味着任何用Date构造函数实例初始化的变量都包含一个引用值,指向内存中可通过任何引用方式修改的数据值:


const theDate = new Date();

console.log( theDate.toDateString() );
// Result: Tue Dec 30 2025

theDate.setMonth( 10 );

console.log( theDate.toDateString() );
// Result: Sun Nov 30 2025

再次忽略月数10代表十一月的事实。

因此尽管现实日期具有固定的含义,但与代表该现实值的Date实例交互时,我们可能无意间改变了该实例:


const today = new Date();

const addDay = theDate => {
	theDate.setDate( theDate.getDate() + 1 );
	return theDate;
};

console.log(`Today is ${ today.toLocaleDateString() }, tomorrow is ${ addDay( today ).toLocaleDateString() }.`);
// Result: Today is 12/31/2025. Tomorrow is 1/1/2026.

目前看起来没问题吧?今天是今天,明天是明天,世界井然有序。若将这段代码提交后继续工作,你完全可以被原谅。除非我们稍微调整输出顺序。


const today = new Date();
const addDay = theDate => {
	theDate.setDate( theDate.getDate() + 1 );
	return theDate;
};

console.log(`Tomorrow will be ${ addDay( today ).toLocaleDateString() }. Today is ${ today.toLocaleDateString() }.`);
// Result: Tomorrow will be 1/1/2026. Today is 1/1/2026.

看出问题了吗?变量today代表着由new Date()创建的对象引用。当我们将today作为参数传递给addDay函数时,参数theDate实际获取的是引用值的副本——并非值本身,而是对表示今日日期对象的第二个引用。当我们操作该值来确定次日日期时,实际操作的是内存中可变的对象本身,而非不可变的副本——今日变为明日,猎鹰难以听清驯鹰师的指令,中锋球员的“持球”表现开始显得有些不稳当,诸如此类。

至此你或许已察觉我并非来赞美Date,但你可能没想到我是来埋葬它的。没错:Date即将终结,彻底消失,成为Web平台中“已弃用”的组件——这意味着它将永远存在,但若能避免,你就不该再使用它。不久之后,我们终于将迎来全面替代Date的对象:Temporal

Temporal不是构造函数,它是命名空间对象

眼尖的读者或许注意到,我说是“替代Date对象”而非“构造函数”。Temporal并非构造函数,若尝试将其作为构造函数调用,浏览器开发者控制台也会给出相同提示:


const today = new Temporal();
// Uncaught TypeError: Temporal is not a constructor

恕我直言,Temporal作为与时间相关的命名要好得多

实际上,Temporal命名空间对象 ——如同Math对象那般由静态属性和方法构成的普通对象:


console.log( Temporal );
/* Result (expanded):
Temporal { … }
	Duration: function Duration()
	Instant: function Instant()
	Now: Temporal.Now { … }
	PlainDate: function PlainDate()
	PlainDateTime: function PlainDateTime()
	PlainMonthDay: function PlainMonthDay()
	PlainTime: function PlainTime()
	PlainYearMonth: function PlainYearMonth()
	ZonedDateTime: function ZonedDateTime()
	Symbol(Symbol.toStringTag): "Temporal"
*/

相较于Date,我认为这种设计瞬间就能让人理解。Temporal包含的类和命名空间对象允许你计算两个时间点之间的持续时间,表示带或不带时区特性的时间点,或通过Now属性获取当前时刻。Temporal.Now引用的是一个包含自身属性和方法的命名空间对象:


console.log( Temporal.Now );
/* Result (expanded):
Temporal.Now { … }
	instant: function instant()
	plainDateISO: function plainDateISO()
	plainDateTimeISO: function plainDateTimeISO()
	plainTimeISO: function plainTimeISO()
	timeZoneId: function timeZoneId()
	zonedDateTimeISO: function zonedDateTimeISO()
	Symbol(Symbol.toStringTag): "Temporal.Now"
	<prototype>: Object { … }
*/

Temporal 为我们提供了一种合理且通俗易懂的方式来获取今日日期,类似于破旧不堪的 Date 对象:其 Now 属性包含一个 plainDateISO() 方法。由于我们未指定任何时区(得益于 Temporal,现在我们可以这样做),该方法会返回当前时区(以我的情况为例,即 EST)下的今日日期:


console.log( Temporal.Now.plainDateISO() );
/* Result (expanded):
Temporal.PlainDate 2025-12-31
	<prototype>: Object { … }
*/

注意plainDateISO直接返回格式化后的纯日期值?稍后还会用到这个特性,敬请关注。

——等等。这看起来很眼熟:


const nowTemporal = Temporal.Now.plainDateISO();
const nowDate = new Date();

console.log( nowTemporal );
/* Result (expanded):
Temporal.PlainDate 2025-12-31
	<prototype>: Object { … }
*/

console.log( nowDate );
/* Result (expanded):
Date Tue Dec 31 2025 11:05:52 GMT-0500 (Eastern Standard Time)
	<prototype>: Date.prototype { … }
*/

难道说——…


const rightNow = Temporal.Now.instant();

console.log( typeof rightNow );
// object

没错,我们仍在操作一个表示当前日期的可变对象,我用最阴森的嗓音说道,手电筒正对着下巴下方。乍看之下,这似乎完全没解决我对Date对象的核心不满。

但我们终究受制于语言特性:日期代表复杂的现实世界值,复杂数据需要复杂数据结构,而对JavaScript而言,这意味着对象。区别在于我们如何交互这些Temporal对象——相较于Date实例——而魔力往往藏在原型链中:


const nowTemporal = Temporal.Now.plainDateISO();

console.log( nowTemporal.__proto__ );
/* Result (expanded):
Object { … }
	add: function add()
	calendarId: >>
	constructor: function PlainDate()
	day: >>
	dayOfWeek: >>
	dayOfYear: >>
	daysInMonth: >>
	daysInWeek: >>
	daysInYear: >>
	equals: function equals()
	era: >>
	eraYear: >>
	inLeapYear: >>
	month: >>
	monthCode: >>
	monthsInYear: >>
	since: function since()
	subtract: function subtract()
	toJSON: function toJSON()
	toLocaleString: function toLocaleString()
	toPlainDateTime: function toPlainDateTime()
	toPlainMonthDay: function toPlainMonthDay()
	toPlainYearMonth: function toPlainYearMonth()
	toString: function toString()
	toZonedDateTime: function toZonedDateTime()
	until: function until()
	valueOf: function valueOf()
	weekOfYear: >>
	with: function with()
	withCalendar: function withCalendar()
	year: >>
	yearOfWeek: >>
	Symbol(Symbol.toStringTag): "Temporal.PlainDate"
	<get calendarId()>: function calendarId()
	<get day()>: function day()
	<get dayOfWeek()>: function dayOfWeek()
	<get dayOfYear()>: function dayOfYear()
	<get daysInMonth()>: function daysInMonth()
	<get daysInWeek()>: function daysInWeek()
	<get daysInYear()>: function daysInYear()
	<get era()>: function era()
	<get eraYear()>: function eraYear()
	<get inLeapYear()>: function inLeapYear()
*/

你立刻会注意到,这里提供了大量方法和属性,专门用于访问、格式化及操作我们正在处理的 Temporal 对象的细节。这并不意外——确实需要些学习成本,但偶尔查阅MDN文档即可解决,且这些方法基本都符合其命名所示的功能。与Date对象的核心差异在于其底层实现机制:


const nowTemporal = Temporal.Now.plainDateISO();

// Current local date:
console.log( nowTemporal );
/* Result (expanded):
Temporal.PlainDate 2025-12-30
	<prototype>: Object { … }
*/

// Current local year:
console.log( nowTemporal.year );
// Result: 2025

// Current local date and time:
console.log( nowTemporal.toPlainDateTime() );
/* Result (expanded):
Temporal.PlainDateTime 2025-12-30T00:00:00
	<prototype>: Object { … }
*/

// Specify that this date represents the Europe/London time zone:
console.log( nowTemporal.toZonedDateTime( "Europe/London" ) );
/* Result (expanded):
Temporal.ZonedDateTime 2025-12-30T00:00:00+00:00[Europe/London]
	<prototype>: Object { … }
*/

// Add a day to this date:
console.log( nowTemporal.add({ days: 1 }) );
/*
Temporal.PlainDate 2025-12-31
	<prototype>: Object { … }
*/

// Add one month and one day to this date, and subtract two years:
console.log( nowTemporal.add({ months: 1, days: 1 }).subtract({ years: 2 }) );
/*
Temporal.PlainDate 2024-01-31
	<prototype>: Object { … }
*/

console.log( nowTemporal );
/* Result (expanded):
Temporal.PlainDate 2025-12-30
	<prototype>: Object { … }
*/

注意这些转换操作既无需手动创建新对象,nowTemporal引用的对象值也保持不变?不同于Date,我们与Temporal对象交互时使用的方法会生成新的 Temporal对象,而非要求我们在新实例上下文中使用它们或修改当前操作的实例——这正是我们能在nowTemporal.add({ months: 1, days: 1 }).subtract({ years: 2 })中串联addsubtract方法的原因。

诚然,我们仍在操作对象,这意味着我们处理的是表示现实世界值的可变数据结构:


const nowTemporal = Temporal.Now.plainDateISO();

nowTemporal.someProperty = true;

console.log( nowTemporal );

/* Result (expanded):
Temporal.PlainDate 2026-01-05
	someProperty: true
	<prototype>: Object { … }

…但 Temporal 对象所表示的值在正常交互过程中不应被改变——尽管该对象本质上仍是可变的,但我们无需拘泥于可能改变其现实日期时间含义的使用方式。这点我接受。

那么,让我们重新审视之前用Date写的那段拙劣的“今天是X,明天是Y”脚本。首先,通过确保使用两个独立的Date实例而非修改表示今日日期的实例来修复它:


const today = new Date();

const addDay = theDate => {
	const tomorrow = new Date();

	tomorrow.setDate( theDate.getDate() + 1 );
	return tomorrow;
};

console.log(`Tomorrow will be ${ addDay( today ).toLocaleDateString() }. Today is ${ today.toLocaleDateString() }.`);
// Result: Tomorrow will be 1/1/2026. Today is 12/31/2025.

谢谢,我讨厌它。

好吧,行吧。它确实能完成任务,就像Date对象最初笨拙地出现在网络上那天起就一直那样。由于我们在addDay函数内创建了新的Date实例,所以不会无意中改变today的值——虽然冗长,但几十年来一直有效。我们向日期加1,必须凭经验理解这代表增加一天。接着在模板字面量中,还得反复提示JavaScript以字符串形式输出不含时间的日期格式。虽然能用,但实在冗长。

现在改用Temporal重写:


const today = Temporal.Now.plainDateISO();

console.log(`Tomorrow will be ${ today.add({ days: 1 }) }. Today is ${ today }.`);
// Result: Tomorrow will be 2026-01-01. Today is 2025-12-31.

这才是我们想要的效果。

好得多。更精简、更高效,且大幅降低了出错概率。我们需要不含时间的当日日期,而调用plainDateISO生成的对象(及由此派生的所有Temporal对象)将自动保持该格式,无需强制转换为字符串。格式化:完成

我们需要输出代表“今天日期+1天”的值,且必须明确表达“增加一天”的意图,避免任何解析猜测:完成完成

最关键的是,我们必须避免原始today对象被意外修改的风险——因为调用add方法的结果永远是新的Temporal对象:已验证

Temporal 将成为相较于 Date 的重大改进,之所以说“将成为”,是因为它尚未完全准备好投入实际应用。拟议的Temporal对象规范草案已进入标准化流程的第三阶段,这意味着它现已被正式“推荐实施”——虽尚未成为指导JavaScript持续发展的标准组成部分,但已足够接近,浏览器可开始进行初步适配。这意味着早期实验结果可能被用于进一步完善规范,因此一切尚未尘埃落定。毕竟,网络标准本就是一个迭代过程。

这正是我们发挥作用的契机。随着Temporal登陆Chrome和Firefox最新版本——其他浏览器也将很快跟进——现在正是我们深入测试的好时机。虽然我们对Date规范无权置喙,但能在最终实现落地前率先体验Temporal

不久之后,JavaScript将拥有合理现代的日期处理机制,我们终于能把Date扔进杂物抽屉最深处,和橡皮筋、错配的罐盖、来历不明的钥匙、半耗的AA电池作伴——它依然存在,仍是Web平台不可或缺的部分,但不再是我们处理日期的唯一选择。而我们只需等待——等等,让我快速计算下:

const today = Temporal.Now.plainDateISO();
const jsShipped = Temporal.PlainDate.from( "1995-12-04" );
const sinceDate = today.since( jsShipped, { largestUnit: 'year' });

console.log( `${ sinceDate.years } years, ${ sinceDate.months } months, and ${ sinceDate.days } days.` );

诚然,取代Date的最佳时机本该在1995年,但嘿:其次最佳时机不就是Temporal.Now吗?

元素周期表抱枕

本文由 TecHug 分享,英文原文及文中图片来自 Date is out, Temporal is in

共有{206}精彩评论

本文列举了Date构造函数的若干荒谬之处,却仅略微触及了最不可原谅的问题。文中举例如下:

// 除非你用连字符分隔年、月、日
  // 否则它会把_日期_搞错
  console.log( new Date(‘2026-01-02’) );
  // 结果:Date Thu Jan 01 2026 19:00:00 GMT-0500 (Eastern Standard Time)

在此示例中,日期被“错误”处理是因为构造函数输入被解释为1月2日午夜UTC时间,而该瞬间在作者所在的东部标准时间区域恰是1月1日晚7点。

实际发生的情况堪称一连串的错误闹剧。JavaScript 将该字符串格式(“YYYY-MM-DD”)解读为 ISO 8601 纯日期格式。ISO 8601 规定:若未指定时区标识符,则默认采用当地时间。ES5 规范制定者本意是遵循 ISO 8601 行为,却意外将其改为“缺失时区偏移值时默认取值为’Z’” (即UTC)。

数年后开发者意识到错误,试图在ES2015中修正。结果可想而知:当浏览器实现正确行为时,大量依赖旧错误行为的网站报错,最终该修正被完全撤销,成为“网页兼容性”的牺牲品。

更多详情请参阅本文末尾“解析器缺陷”章节:

https://maggiepint.com/2017/04/11/fixing-javascript-date-web

-- tshaddox

>结果被彻底回滚,牺牲在“网络兼容性”的祭坛上。

正因如此,我无法理解为何缺乏指令机制。

在文件开头添加’use strict’;曾长期广泛使用且行之有效。它并未强制回滚不兼容性,而是允许用户主动选择更严格的JavaScript解析模式。

类似此类重大变更时,若能提供类似’strict datetime’;的指令,让开发者主动选择使用修正后的行为,本会更理想。

他们不可能也不该对所有变更都这么做,但对于平台的重大变革,这确实是种改进。

或者他们可以全面采用内部模块机制,就像现在可以导入node:fs那样。他们可以提供修正后的全局对象版本,例如:

import Date from ‘browser:date’;

该行为已得到修正,例如:

-- no_wizard

公平地说,这里新增的可选机制“use strict”实际上是“切换到Temporal”。这是个更严格的新命名空间对象。旧版Date代码保留原有特性,新代码则获得优化的Temporal API。

理论上内部模块能避免每次浏览器推出更严格的API时,开发者都得翻遍词典查找对应方案。内部模块甚至已被提议为TC-39推荐的JS API扩展方案。上次查阅该提案时,其受困于多重问题:

1. 特性检测:检测 Temporal 是否可用只需 if (‘Temporal’ in globalThis) {},但检测模块导入缺失则复杂得多。当前标准规定:若模块导入失败,整个加载过程将抛出错误。虽然可通过try/catch中的动态导入绕过此限制,但相比const thingINeed = ‘someApi’ in globalThis ? someApi() : someApiPolyfill()的简洁性,这种方案会增加大量冗余代码。我已看到多项针对此问题的提案,包括扩展导入映射、在导入语句中添加with { }选项等方案。

2. 过度讨论(且非常多):定义类似browser:standard:的URI方案需要深入思考其扩展机制。若采用browser:some-api形式,则面临最终污染所有简洁名称的风险——这恰恰是人们担忧globalThis过度污染的症结所在(如同在npm上难以找到可用单词名称般棘手),本质上只是将命名问题转移到其他地方。另一方面,若采用类似 `es-standard:https://tc39.es/ecma262/2025/v1/final-draft/Temporal这类命名方式,即便(尤其)假设用户大多会通过importmap将其映射为更短的名称,你依然是以新奇的方式复刻了XMLNS URI——而JS用户对XMLNS URI向来意见纷呈,许多人公开表达过强烈厌恶,但这种机制的诞生恰恰源于类似的强力向后兼容修复需求。(正如俗语所说:时间是个扁平的圆环。)

-- WorldMaker

> 公平地说,这里新增的可选机制“use strict”本质上是“切换至Temporal”。

正是如此。无需破坏旧代码,只需提供新的最佳实践方案。

更新代码检查工具(或更理想的是采用Rust语言中“版本”机制这类一流语言规则),逐步淘汰旧行为。无需经历Python 2到3长达十年的迁移过程。

Temporal设计精妙。它吸取了前人无数失败案例的教训,并借鉴了众多优秀实现方案:Joda Time、Chrono等。

-- echelon

PHP同样深受其害。过度严格的向后兼容性使PHP沦为混乱的语言。据我所知,它至今仍保留着临时参数顺序约定,且内置函数缺乏命名空间管理——所有内容都处于全局状态。

JS的情况尚可理解,毕竟开发者无法控制运行环境。但PHP明明没有这种限制,却依然无法收拾PHP这团乱麻。

-- phplovesong

公平地说,这里新增的可选“use strict”机制本质是“切换到Temporal”

没错,但添加全新API来解决大量日期相关问题,显然比修复一个明确且广为人知的漏洞困难得多。Temporal框架直到现在才开始发布,距离这个漏洞出现已逾15年,而他们决定永不修复该漏洞也已超过10年。

-- tshaddox

NuGet采用系统包规范:若目标平台原生支持功能则保持空包,否则提供独立实现。因此该包可在所有平台无条件导入。

-- GoblinSlayer

> 类似此类广泛变更时,若能提供类似’strict datetime’指令让开发者主动启用修正行为就更好了。

这会很糟糕,因为程序某些部分(如库文件)需要保留旧行为,而其他部分可能需要新行为。当多个模块对标准库的行为预期不一致时,如何实现它们的联合压缩?

我认为正确做法是采用多构造函数机制(如Obj-C、Swift、C和Rust所采用)。例如:

    let d = new Date(...) // 旧行为(新代码不推荐使用)
    
    let d = Date.fromISOString(...) // 固定行为

此方案的最大弊端在于难以追踪哪些字段和函数应在现代JavaScript中彻底弃用。若能在开发阶段为过时JS特性启用更醒目的警告机制就再好不过了。

-- josephg

我认为浏览器(或者说规范)不支持版本控制机制实在遗憾。若能声明使用特定版本的HTML、JS和CSS,就能在不破坏整个网络的前提下实现重大变更。

由于向后兼容性要求,当前存在大量(事后看来糟糕的)设计决策和实现事故永久存在;若能定期清理旧有负担,网络生态将获益匪浅。

-- abuob

这也会迫使浏览器实现多种细微差异的引擎模式,极大增加浏览器代码复杂度。

现有案例如怪异模式与标准模式之争,以及曾被视为进步必需的“use strict”模式,都已证明这种机制会给浏览器带来负担。我们不希望出现超出必要范围的模式。

-- bazoom42

Perl 采用过这种方案,尽管我深爱(曾经深爱)这门语言,但若想采用更新或更严格的语言特性,久而久之确实会变得难以掌控。

-- hnlmorg

> 若想采用更新或更严格的语言特性,久而久之确实会变得难以掌控。

具体如何失控?

顺便说一句,我只需执行use v5.32;或类似语句就能启用该版本的所有特性。

https://perldoc.perl.org/functions/use#use-VERSION

当然,若想自由选择特性,列表确实可能变得冗长。

-- AceJohnny2

或许可以借鉴Rust的版本机制,允许选择特定时间点引入的破坏性变更集合。

-- thayne

指令机制勉强能凑合使用——但仅作为权宜之计,且仅限于无法或不愿修改API的情形。

> 或者他们可以全面采用内部模块机制,类似当前导入node:fs的方式。他们可以提供修正后的全局变量版本,例如import Date from ‘browser:date’;

此处实际情况是API也同步变更了

-- agos

我清楚记得编写过一个函数:先将字符串按组成部分拆分,再重新组合以确保日期不带时区创建。

有时日期就是单纯的日期。你的生日固定在某天,不会因迁居他州而偏移x小时。

旧版Outlook将生日标记为全天事件,却以时区值存储数据——这意味着当我从比利时迁居加州后,所有存储在比利时的生日日期都发生了偏移…

-- OptionOfT

我一直觉得系统用DateTime字符串表示日期很奇怪。日期和时间应该有不同的基本类型:日期本身本质上不带时区,而DateTime则需要时区信息。

在处理用DateTime编码的日期时遇到诸多问题后,我开始将日期编码为Date基本类型,并编写了日期计算函数,确保时区因素绝不会渗入其中。若数据库日期字段存入DateTime字符串,除非开发过程中明确规范化处理过,否则根本无法还原原始日期。

后来发现许多日期选择器库即便处于“DATE”模式,仍会自动附加本地时区信息。因此我不得不编写一个净化器,在数据上传至服务器前剥离时区信息。

尽管如此,我对Temporal仍充满期待,它终将简化其他操作。

-- abustamam

Temporal确实提供了PlainDate类型,这正是你描述的日期基本类型(采用不同命名,可能是为了避免与旧版Date类型冲突)。

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe

-- cpmsmith

太棒了!感谢分享

-- abustamam

这在C#中一直是个令人沮丧的问题。

BCL提供的DateTime类实在令人困惑,尤其当你只需要Date类型时。虽然后来终于加入了DateOnly,但在那之前我就转用名为“Noda”的库(Java中对应Joda),虽然需要学习成本,但之后一切逻辑都变得清晰多了。

它提供LocalDate和LocalDateTime用于存储本地日期,Instant用于存储UTC时间。虽然也支持ZonedDateTime,但我很少用到。我在医疗领域工作,许多法规对日期要求极其严格,比如“必须在5天内完成X操作”而非“120小时内完成X操作”。因此在处理这类数据时,存储时间信息反而会增加复杂性。

-- mexicocitinluez

哇!确实,医疗行业对严谨性要求更高,连一天的误差都容不得,有时甚至一小时的偏差都不可接受。

-- abustamam

您说得完全正确。

计算本身通常相当简单(少数例外),但问题在于这类计算数量庞大。而且它们彼此存在微妙关联,还经常变动。

-- mexicocitinluez

必须区分三种概念:瞬时时间、观测地点的瞬时时间,以及“构建日期规范”(该日期可能已过、正在进行或未来发生)。

例如对话中“非休息日每天上午9点开门” 这属于简单周期性规则,仅含例外情况*。若时区变更,每日仍需重新评估预定时间。

-- mjevans

确实如此,还需考虑令人头疼的夏令时问题(例如企业营业时间在本地不变,但在协调世界时却会变化)

-- abustamam

我的意思是…这不就是现实吗?我多次差点忘记东区朋友的生日,结果发消息说“生日快乐!(我这边还过着生日呢,哈哈)”

我并非为现有方案辩护,只是指出时间本质上具有不可避免的模糊性与诅咒性。

-- eszed

若我变更时区,生日提醒理应随之调整。但生日日期本身未变,就不该移动到其他日子。

-- whiskey-one

> 但生日日期本身未变,就不该移动到其他日子。

但事实并非如此。我兄长曾移居美国数年。我们总在澳大利亚时间的生日当天向他致贺,而他收到祝福时已是当地时间的生日前夕。如今他重返澳大利亚,情况却反转了——美国朋友总在他生日次日才送上祝福。

我妻子脸书上有许多美国朋友(虽未谋面,都是昔日玩《Farmville》的战友)——他们同样会在她生日次日送上祝福。或许她也在反向操作着同样的事。

-- skissane

但采用UTC时间并不能解决这个问题,除非收件人恰好位于本初子午线附近。

-- thayne

你让我想起曾读过的谜题:如何解释某人出生晚一年却比前一年出生者年长?谜底同样涉及时区。显然,生日日期确实与时区息息相关。

谜题的解释大致如下:1月1日凌晨12点15分,纽约市诞生了一个婴儿。半小时后,洛杉矶又诞生了一个婴儿,当地时间是12月31日晚9点45分。尽管纽约的婴儿实际年长30分钟,但日历日期却显示洛杉矶的婴儿似乎先出生。

-- layman51

类似谜题中另一大趣味时区计算技巧在于国际日期变更线:若婴儿出生于某侧,按日历计算可能属于“前日”出生,尽管其实比另一侧晚出生30分钟。

-- WorldMaker

一对异卵双胞胎在横跨太平洋的西行船上出生。其中一人官方出生日期为2016年1月1日,晚出生30分钟的弟弟官方出生日期为2015年12月31日。等他们长大成人后,要说服别人相信他们是双胞胎可就难了。

-- rmunn

即使不考虑时区差异,这种情况也可能发生。若出生间隔仅一秒(尤其在午夜时分),双胞胎的出生日期就可能分属不同年份。

-- hdjrudni

确实如此。若咨询助产士、妇产科医生或其他接生专业人士,你定会听闻大量分属不同出生日的双胞胎案例。我一位从事接生工作的亲戚医生常分享这类故事,其中部分因HIPAA规定无法透露。但婴儿出生后,其出生日期即属公开信息,她便可分享相关细节。因此她常对丈夫说:“我的产妇要分娩了,得去医院了”,却不透露具体姓名。待婴儿出生后,她便会说“史密斯太太的孩子昨晚11点出生”——此时产妇身份已属公开记录,不再受HIPAA保护。下次见面时,我打算问她是否亲手接生过生日不同的双胞胎。

时区问题当然只是让年轻的双胞胎比年长的“提前一年出生”的手段——通过让两人出生在不同时区实现。这种情况唯一可能发生在船上,因为:1)飞机上出生的婴儿通常会采用起飞地或目的地时区作为出生时间,因此双胞胎不会被视为出生在不同时区; 其次,陆地交通工具如汽车或火车很可能中途停车(火车则让孕妇在最近车站下车),避免产妇在移动车辆中分娩。因此船舶是唯一可能发生这种情况的移动载具——毕竟在大洋中央根本无法中途下船。虽然东西向跨越时区时可能发生,但西向东跨越国际日期变更线更能形成有趣的思想实验。

是的,我确实对这个荒诞的玩笑场景投入了远超其价值的思考。:-)

-- rmunn

这不完全相同吗?

甚至不必是国际日期变更线,任意两个时区都适用。

-- hdjrudni

没错,本质相同。国际日期变更线只是让笑话更成立——否则婴儿必须在跨越时区线时恰好出生于午夜前后。有了IDL,出生时间只要避开午夜时段,笑话就能成立。

-- rmunn

> 牺牲在“网页兼容性”的祭坛上。

那他们该怎么做?像IE5时代那样强制所有人检测浏览器版本并分支处理?

(认真提问,或许我忽略了什么巧妙方案。)

-- teiferer

我认同“不破坏网络”的设计原则,但有时不同意TC39划定的具体界限。破坏陈旧且不变的网站显然有代价,但允许这些网站掣肘整个网络同样代价高昂。权衡这些代价是主观问题。

据我所知,TC39并未明确规定拒绝JavaScript行为变更提案时,需要多少网站或多少用户受到影响。显然有些破坏性变更微不足道,TC39本应忽略(试想某个网站的JavaScript代码遍历所有内置API,只要其中任何一个变更就会崩溃)。

-- tshaddox

浏览器应当为语言版本化。它们应该声明“若使用<html version="5.2">或更高版本,则采用此行为”。

不知何故,标准制定机构却决定移除原有的版本机制。

-- marcosdumay

他们之所以放弃版本标识,是因为不愿在引擎中永久维护4.0版本。

这正是他们再未推出类似“use strict”功能的根本原因。

依我之见,这是个糟糕的选择。允许基于版本引入新行为和功能是相当自然的,也是大多数编程语言的演进方式。始终保持永久的向前向后兼容性既难以维护,又会让修复旧错误变得极其困难。

他们选择这条路可能还有另一个原因:将兼容性级别概念集成到压缩工具中相当困难。

-- cogman10

提供可选参数来启用旧行为,同时将新正确行为设为默认(无参数时),似乎是个不错的选择。

-- mejutoco

为保持向后兼容性且避免所有旧网站更新,必须将旧行为设为默认,新行为则需主动启用。

-- stevula

这恰恰是相反的思路。同样可行。也可将无参数调用标记为弃用,强制要求每次调用都指定行为参数。弃用期可以足够长,让那些网站有时间重写多次 😉

-- mejutoco

烧录在硬件设备中的控制接口不会被重写。况且不可能通过“标志日”让所有人统一切换,因此硬件设备的寿命周期并非关键考量。

向后兼容性是网络存在的根本意义之一。

你可以按任意粒度为所有内容打版本号,但随时间推移会累积问题(例如"bug 3718938: JS gen24代码错误地将Date运算视为gen25-34处理"),更不用说那些只支持特定版本的库——它们通过创建的对象隐式传递自身预期,导致依赖解析器必须处理所有依赖项版本的交叉组合…

-- sfink

天下没有免费的午餐。持续十年才报错的弃用警告,顶多破坏某些CSS盒模型和多数浏览器的严格模式。

-- mejutoco

不,这并非“牺牲”,而是唯一理智的选择。理想情况下应明确标注构造函数不符合ISO标准,并提供符合ISO的替代方案。

根据我(不幸的)经验,日期时间/时区处理本就是最容易引入隐蔽却影响深远的漏洞的领域。引入这种(通常)不会快速报错的行为变更,往往看似能持续正常运行直至彻底失效,其难以调试/定位/修复的特性简直是通往混乱的快车道。

即便JavaScript在向后兼容性上付出额外努力,我认为其他语言也不会以这种方式引入破坏性变更。

-- dahauns

不妨试试https://jsdate.wtf/

JavaScript的Date对象究竟有多诡异,实在难以想象。

-- Kyro38

前三题猜对两题。

到第四题就放弃了:

    new Date(“not a date”)
    1) 无效日期
    2) undefined
    3) 抛出错误
    4) null

这种鬼东西根本没法猜。全是随机结果。

-- publicdebates

我觉得你搞反了。唯一方法就是猜,全是随机。

最让我抓狂的是内部不一致性。比如,我理解可能存在某些怪癖——可能是出于奇怪的向后兼容性或技术限制——但同一个接口内部居然存在多个相互矛盾的怪癖!太糟糕了,正是这类问题让JS长期被视为(有时至今仍是)一种不够优秀的语言。

-- just6979

我压根不知道居然存在无效日期对象,这简直疯了。还有些有趣的例子:

    new Date(Math.E)
    new Date(-1)

居然都是有效的日期哈哈。

-- dvt

new Date()构造函数融合了大约5种不同规范,除非输入符合其中一种,否则具体采用哪种规范取决于实现者的选择

-- winstonp

这个选择实在令人意外。我本以为会出现你遗漏的NaN。

标准JS库还有其他返回错误对象而非抛出异常的情况吗?我实在想不出其他例子。

-- marcosdumay

我觉得NaN本身就是种错误对象,尤其在后续数学函数传递中的表现,这与抛出异常是不同的选择。

但除此之外我同意你的观点,无效日期确实很奇怪,我居然从未遇到过。

其后果是:你仍可对无效日期对象调用Date方法,而数值结果会返回NaN。

-- jazzyjackson

有趣的是无效日期依然属于Date类型:

    > let invalid = new Date(‘not a date’)
    > invalid
    Invalid Date
    > invalid instanceof Date
    true

你预判NaN的结果只对了一半,这其实是无效日期在底层存储的特性:

    > invalid.getTime()
    NaN

无效日期(Invalid Date)本质上是“Unix纪元时间戳”为NaN的日期对象。它遵循NaN的比较逻辑:

    > invalid === new Date(NaN)
    false

这是与NaN直接相关的有趣现象。

-- WorldMaker

无效日期只是“Unix纪元时间戳”为NaN的Date对象。它同样遵循NaN比较逻辑:

invalid === new Date(NaN)
false

这仅仅因为JS Date是对象类型,与内部表示无关。

    > new Date(0) === new Date(0)
    false
-- tyilo

个人认为UTC作为默认时区很合理。日期处理应在标准化时区进行,仅在显示时才需转换为本地时间。

-- sholladay

UTC作为默认时区无可厚非,但这并非重点。

带时区的日期时间与不带时区的日期时间本质不同,二者各有价值。我的生日没有时区,但我的截止日期有。

公司要求提交文件的截止时间?可能有也可能没有,这取决于政策。

诗意地表达:我们诞生时不受时区束缚,逝去时却被时区所困。

-- sfink

> 我的生日没有时区,我的截止日期却有。

这似乎有些主观

-- throwaway290

在我看来并非如此。日期时间的有效应用场景显然涵盖两种情形:或指代精确的时间点,或表达特定计时体系中的时间概念。

生日没有时区,因为生日的概念更多关乎墙上日历上的日期,而非任何公认的特定瞬间;因此生日最重要的意义在于你所在的位置。即便你穿越地球另一端,生日也不会提前或推迟一天。

截止期限则存在时区概念,因为当老板要求项目在下午4点前完成时,他指的是双方所在地的当地时间4点——即便你在4点前乘火车跨越时区,他所指的具体时间点也不会改变。

实际上这可能确实涉及时区而非仅是UTC偏移量;因为若老板要求每日4点前提交特定报告,当你所在时区实行夏令时,报告截止时间绝不会因此自动延至5点。

在第一种情形中,日期本身不带时区属性,在任何解读场景下均有效。第二种情形下,时间点可能以UTC时间表示,并可附加或省略具体偏移量。第三种情形中,连续的时间点相对于UTC可能发生位移,但仍被固定表达所指代。

这些并非主观诠释,而是人类在日期/时间表示中叠加多重含义所导致的必然结果。

-- drysart

不知你是否如此,但知情者总能在我的出生整点送上生日祝福,而我从不会为截止时间的精确时刻做准备。这更像是以天为单位的概念。

-- throwaway290

并非如此。我是澳大利亚人,我们的时区领先于美国(新南威尔士州时间比美国东部时间早15-17小时)。若我在生日当天从悉尼飞往纽约(约22小时航程),美国海关官员会在次日我抵达时祝我生日快乐。

因此生日与时区完全无关。

-- devilsdata

在对方出生时刻(若知晓具体时间)送上生日祝福很酷,但准备截止日期就另当别论了。

不过海关不会给我生日祝福,真棒!

-- throwaway290

若在转换为UTC再转回原时区期间,时区数据库发生变更(这种情况比想象中常见),就会导致错误行为。

-- lysium

若时间以UTC存储,即使时区数据库损坏结果仍正确,因为时区仅是元数据,不影响时间本身。

-- GoblinSlayer

这取决于实际存储的内容。许多情况下时区并非元数据,而是直接决定日期时间的解析方式。

例如:街角小店每日的营业时间。用UTC存储这些时间是不正确的,因为店主开店关店并非依据UTC时间,而是基于本地时区。

-- drysart

你混淆了不同概念。实际的开店闭店时刻可以存储为UTC时间,因为这是标准时间。调度算法是算法本身而非时间。你可以使用类似时间的领域特定语言(DSL)编写该算法,但作为DSL,其实现能力存在局限。

-- GoblinSlayer

在此场景下采用UTC存储同样有效,其IANA时区字符串也应在“某处”同步存储。

-- ovao

你无需将时区存储在任何地方,只需在使用存储的UTC时间时知道当前本地时区即可。正因如此,存储UTC时间更为优越——它只需一次转换就能表示任意本地时间。

若以本地时间存储(即包含时区参数),当数据被转换至不同时区时,你将面临两个时区交织的复杂问题。这极易导致时间偏差累积至15分钟的倍数,甚至出现一两天误差!

更糟的是,即使在同一地点存储本地时间,若该地采用夏令时仍需转换!时区适配永远无法避免,因此采用最通用格式存储日期时间几乎永远是最佳选择。

-- just6979

若这算喜剧,那我宁愿选择悲剧。

这简直是无数细微却致命漏洞的温床。

-- netghost

这是客户端渲染网页应用中常见的日期格式化偏移错误根源,尤其在传递“YYYY-MM-DD”日期字符串的场景(常见于OpenAPI JSON接口)。

  const dateStringFromApiResponse = “2026-01-12”;
  const date = new Date(dateStringFromApiResponse);
  const formatter = new Intl.DateTimeFormat(‘en-US’, { dateStyle: ‘long’ });
  formatter.format(new Date(“2026-01-12”));

  // ‘2026年1月11日’
-- tshaddox

这让我想起当年编写Power Automate表达式,将Outlook元数据传递的日期转换为Excel可识别的格式

本质上就是从字符串索引或正则表达式中提取数字,重新组合成Excel可识别的字符串

-- jazzyjackson

本地时间无法解析,这种格式仅对人类可读——因为人类能即兴处理模糊性。将其解析为UTC是机器解析器的合理默认方案,至少是唯一可行的方案。

-- GoblinSlayer

Maggie是顶级开发者。Momentjs大概为人类节省了数百万小时的集体编码和调试时间

-- cush

JavaScript的Date对象问题很多,但它作为对象的特性其实排不上十大缺陷。

如果Date对象不可变会更好吗?当然。但可变对象的修改确实改变了对象本身,这本就不该令人震惊。

-- procaryote

我的遭遇是:将日期传递给外部库后,该库执行操作时竟擅自修改了日期。即便明知对象可变,这种行为依然令人抓狂。

-- RedShift1

当你持有的日期对象被外部篡改时,冲击感确实强烈。可变值的问题从来不在于你(即本地上下文)修改它。真正的隐患在于你无法确保其他地方(某些完全不相关的代码)不会擅自修改。

-- chowells

另:某款流行的Web应用开发库正是基于对象相等性来比较渲染状态差异的

-- agos

这不就是所有对象的运作方式吗?有什么好惊讶的?

-- petesergeant

这观察很中肯,但根据个人经验我认同楼主观点。职业生涯中我修复过不少日期在传递过程中意外被修改的 bug,却几乎不记得对象出现过类似问题(可能是选择性记忆作祟)。

若要推测原因,我认为是以下因素的综合:

– 认知模型与实现机制的差异。日期虽是对象却“感觉”像值:日期从单一值解析而来,存储/输出时又折叠回单一值(而自定义对象通常是属性集合,输出/存储时仍保持对象形态)

– 常见日期操作会导致原始日期对象被修改,这会诱使开发者在非本意时也修改传入值

因此典型场景是:调用方期望日期被当作值处理,被调用方却因操作便利性而意外修改数据。若此时原始值被保存回数据库,数据便会损坏。

多数经验丰富的开发者会记得在调用方和被调用方都复制日期对象,但默认机制仍存在危险的易错性。

-- arthens

缺乏const意味着将对象作为参数相当危险

-- kortilla

前文评论者指的是语言层面对不可变对象的支持。例如JavaScript虽有const,但仅支持变量不可变性而非对象本身。后者在C++等语言中可实现,const可同时用于两者。当然某些语言仍可通过接口伪造不可变性,或通过实现强制真实不可变性(如Temporals),但拥有强制不可变性的标识器显然更优雅。

我想补充的是,变量和对象的不变性都应作为默认行为,可变性需要通过关键字显式声明——这与C++和Java的做法恰恰相反。

-- ruszki

我认为这是技能问题。没错,修改引用对象确实会得到不同值。但开发者未关注这种变化,不能归咎于语言本身。

JavaScript存在无数其他合理缺陷,开发者不善于理解引用对象绝非其中之一。

-- bilekas

确实令人恼火的是,Temporal API 如同几乎所有其他日期时间 API 一样,完全不支持以任何形式查询闰秒信息。诸如 temporal-tai 之类的建议解决方案都要求加载闰秒文件并保持更新,这对客户端 JavaScript 尤其痛苦——由于同源策略限制,你无法直接从其他网站下载闰秒文件。而浏览器本可通过足够频繁的更新保持最新版本,但日期时间API却因“仅支持UTC”的固执立场,拒绝暴露闰秒信息。

(背景是我想编写天文计算的JS工具,但UTC转换需要闰秒信息,这种设计导致无法实现“Just Works™”的解决方案。)

-- LegionMammal978

  >本项目仅支持UTC时间

  >天文计算工具

可惜UTC客观上并非天文计算的正确时间标准。除其他问题外,UTC会随地球与太阳距离变化而略微快慢。UTC运行并不均匀(除海平面高度外),每秒时长会随太阳系当前配置略微增减。

如你所言,此类计算应采用TBD(即质心动力时),该时间尺度通过相对论修正使原子钟仿佛固定在太阳系质心运行。这是天文计算中唯一能真正实现“平滑运行”的时钟。

https://stjarnhimlen.se/comp/time.html

https://www2.mps.mpg.de/homes/fraenz/systems/systems2art/nod

-- schiffern

> 除其他问题外,协调世界时(UTC)会因地球与太阳的距离而略微快慢不一。UTC并非恒定运行(除地球海平面外),每秒长度会随太阳系当前配置略微增减。

此说法完全错误。UTC秒完全等同于国际单位制秒,所有秒长度均完全一致(由原子钟群定义)。

-- zokier

在地球海平面处,UTC秒确实完全相同。这正是UTC的定义。

关键在于,当涉及太阳系及更大尺度时,假设自身参考系位于“地球海平面”已不再合理。你的相对论参考系已发生位移,因此(感谢爱因斯坦!)时间在你脚下真实地发生了变化。

主要机制(但非唯一机制)在于:当地球更接近太阳时,由于引力时间膨胀效应,地球上的时钟走得会更慢[0]

因此,地球上的时钟始终以恒定速率运行。但由于宇宙本质上遵循爱因斯坦理论,这意味着当你研究木星轨道或毫秒脉冲星时,若使用协调世界时(UTC)或甚至不包含闰秒的UT1时标替代天体历时标(TBD),仍会引入微小的时间误差。

[0] https://en.wikipedia.org/wiki/Gravitational_time_dilation

-- schiffern

但一切都是相对的,所有参考系彼此不同且相互相对,并不存在某种特殊的参考系。TBD相对于UTC的运行不均匀性,恰如UTC相对于TBD的运行不均匀性。

-- zokier

完全同意,爱因斯坦教得真好。;) 关键在于为特定任务选择正确的参考系。

当需要高精度时,UTC绝非天文计算的理想参考系。仅此而已。

-- schiffern

> 由于安全操作协议(SOP),你无法直接从他人网站下载闰秒文件

此处所指为何?为何SOP会阻止网站托管闰秒文件?只需设置Access-Control-Allow-Origin允许访问即可。或直接提供JS文件——这种情况下甚至无需任何头部设置。单源策略仅禁止未经授权直接链接他人闰秒文件并占用其带宽的行为。

> 况且浏览器更新频率完全足以保持文件实时同步

这是真的吗?目前我不知道有哪个浏览器会随附闰秒数据文件。对于新浏览器开发者来说,添加并维护这样的数据文件恐怕是个相当棘手的任务——毕竟这只是浏览器自身永远用不上的东西。这不像ICU/CLDR文件,浏览器总得用它们来渲染自己的用户界面组件。

-- nightpool

> 他们只需设置Access-Control-Allow-Origin允许网站访问即可。单点策略(SOP)仅禁止未经授权直接链接他人闰秒文件并占用其带宽的行为。

他们可以这样做,但主流服务商(即我认为会及时更新的机构)并未实施。国际地球自转服务局(IERS)未开放[0],美国海军天文台(USNO)未开放[1],互联网号码分配机构(IANA)未开放[2],美国国家标准与技术研究院(NIST)仍使用FTP[3]。需注意这些文件正被各类NTP客户端持续下载,服务商并非限制公共访问,只是未设置允许JS请求的标头。

> 这是真的吗?目前我不知道有哪个浏览器会预装闰秒数据文件。

根据ECMA-262规范:

> 时区感知实现必须(其他实现建议)使用IANA时区数据库https://www.iana.org/time-zones/的时区信息。

任何预装 tzdb 数据库副本或能从操作系统获取副本的浏览器,都应能访问其闰秒文件。除非您指的是所有浏览器都仅通过 ICU 及其数据文件获取?这可能构成障碍,除非 ICU 开始公开这些数据。

[0] https://hpiers.obspm.fr/iers/bul/bulc/ntp/leap-seconds.list

[1] https://maia.usno.navy.mil/ser7/tai-utc.dat

[2] https://data.iana.org/time-zones/tzdb/leap-seconds.list

[3] ftp://ftp.boulder.nist.gov/pub/time/leap-seconds.list

-- LegionMammal978

能否在这些URL前添加某种CORS代理?(虽然不得不这么做很糟糕,但这就是现实。)

你甚至可以编写Cloudflare Worker或在val.town上部署Val来实现,并添加缓存功能以减少对提供商的频繁访问。

-- GeneralMaximus

> 但日期时间API拒绝暴露闰秒信息,因为它们过于执着于“本项目仅支持UTC”。

这种做法至少存在两个层面的逻辑矛盾。

首先,严格来说,UTC作为时间尺度本身就包含闰秒机制。既然承诺采用UTC,就意味着支持闰秒机制。

其次,更广泛而言,你应该说“他们过度坚持’本项目仅支持POSIX时间尺度’”。这更准确地反映现状,同时也揭示了核心问题:除特殊应用外,几乎所有系统都基于POSIX时间构建,而该标准刻意忽略了闰秒的存在。

-- burntsushi

当然,但我的不满甚至不在于我们应该改变“默认采用POSIX时间”的现状(毕竟“日历秒”计时早已成为行业标准),而在于底层库根本不提供足够信息供“特殊应用”可靠地修正误差——除非这些应用自身持续更新。

-- LegionMammal978

但它们为何要这么做?在我看来这属于小众中的小众。科学时间库完全有能力以优于通用时间库的方式满足这类需求(事实上此类库已相当丰富)。

作为曾在Jiff[1]预发布版中实现闰秒支持(包括读取leapsecond tzdb数据)却因故将其移除的人,我如此认为[2]

[1]: https://github.com/BurntSushi/jiff

[2]: https://github.com/BurntSushi/jiff/issues/7

-- burntsushi

若需与GPS时间戳同步处理事务(例如电信领域的大部分业务、众多电网系统等),这绝非“小众需求”。

任何以GPS作为锁定基准进行毫秒级同步的系统,都绝对无法容忍“闰秒延迟”这类现象。

-- kortilla

“小众”不等于“不重要”,仅指“非主流”。这是相对概念。

绝大多数使用通用日期时间库的人,根本不需要GPS处理或高精度科学计算。

-- burntsushi

绝大多数使用日期时间库的人无需处理1980年前或2036年后的日期。但这不意味着你可以视而不见,忽视时间表示机制中相当关键的部分。

-- kortilla

> (包括从闰秒时区数据库读取数据)

这确实是问题之一:若我编写的是能从时区数据库等渠道提取信息的独立程序,我乐意克服这些技术障碍,且不会影响其他库的使用。我并不在意操作体验。但对于JS脚本而言,所有信息都必须依赖浏览器API或第三方服务器提供——而这类服务器资源本就稀缺。

-- LegionMammal978

> 确实令人恼火的是,时间 API 与几乎所有其他日期时间 API 一样,完全不支持以任何形式查询闰秒信息。

这是因为人类计时体系并不采用闰秒机制。

-- paulddraper

难道不能直接镜像数据吗?甚至可以直接嵌入到JavaScript文件中。

-- mr_toad

> 如同其他几乎所有日期时间API,完全不支持查询闰秒信息

这可能是因为只有天文观测或GPS等特殊场景才需要闰秒级精度。在JavaScript领域,99%的客户端应用根本不需要这种精度。大多数日期时间库都基于POSIX时间工作,该标准默认每天有86,400秒。

-- hgs3

直接做成npm包不就行了。

-- GoblinSlayer

Temporal特性竟直到现在才在Chrome稳定版上线,实在令人惊讶。

本以为它早就该准备好广泛应用了。

https://caniuse.com/temporal

-- daveoc64

我们去年就在Deno服务器中爱用这个功能。但至今无法升级网页客户端的日期逻辑实在令人沮丧——尽管Firefox早已支持Temporal,Chrome却迟迟不行动。

-- promiseofbeans

这样评价Chrome是否公平?当时规范正经历大量变更,V8引擎选择等待规范稳定;与此同时,Anba持续推进Firefox的实现工作。此外,Deno去年暴露的Temporal版本已严重滞后于最新规范,且大量规范内容尚未实现。

-- nekevss

所有Chromium分支都计划在本周处理。WebKit仍处于测试阶段。

-- ZeWaka

我们一直在使用这个Temporal polyfill,目前表现非常出色:https://github.com/js-temporal/temporal-polyfill

-- crabl

需注意该文件达51kb,并非真正轻量级https://bundlephobia.com/package/@js-temporal/polyfill@0.5.1。仍适用于前向兼容或服务器端部署,但对小型应用而言体积显著。

-- mediumdeviation

没错,我一直在用这个更轻量版(20kB):https://github.com/fullcalendar/temporal-polyfill/

-- LtdJorge

它和moment相比如何?特别是和luxon相比?

-- whizzter

它比moment复杂得多,但这源于日期时间本身固有的复杂性——这些复杂性正是moment未处理的部分。因此你必须明确区分操作对象是日期、时间还是日期时间对象,是否包含时区等属性。Moment侧重便捷的API设计,而Temporal追求精确的API实现。

-- MrJohz

对我而言最大的优势在于它能正确处理日期、时间等数据类型。

我接触过的多数日期/时间库都只提供单一的“日期/时间”或“时间戳”类型,导致必须通过特殊处理来区分“2026年1月13日”代表“当地时间午夜”还是“UTC时间午夜”。

Temporal库则采用完整的数据类型体系:Instant表示时间点,PlainDate表示纯日期,PlainTime表示纯时间(如“我们每天上午11点吃午餐”),ZonedDateTime表示特定时区的Instant对象,等等。

Temporal 汲取了大量 Java Joda-Time 的设计灵感(该库同样启发了 .NET 的 Noda Time、Java 官方 java.time API 及 JavaScript 的 js-joda)。这种兼容性极具价值——意味着跨语言开发时部分概念可直接迁移。更重要的是,它承载了大量关于如何高效、符合人体工程学地呈现日期时间复杂性的深思熟虑。

-- joshkel

这是个轻量级的Web标准API,强烈推荐。

-- kristo

Moment团队不是明说要用Temporal吗?

-- socalgal2

Moment多年前就建议“使用Luxon”[1]。而Luxon尚未达到“推荐使用Temporal”的阶段[2]。

[1] https://momentjs.com/docs/#/-project-status/

[2] https://github.com/moment/luxon/discussions/1742#discussionc

-- WorldMaker

https://momentjs.com/docs/#/-project-status/future/

-- socalgal2

> 当不可变值被赋给变量时,JavaScript引擎会创建该值的副本并将其存储在内存中

并非完全如此。语言规范并未明确规定值是否被复制,而正因为值是不可变的,用户根本无法判断是否发生了复制。

例如字符串也是不可变值类型,但你可以确信没有任何 JS 引擎会在每次将字符串赋值给变量或传递给参数时完整复制整个字符串。

-- munificent

虽然来晚了,但我真心希望大家都借鉴Rails+Ruby的模式,特别是Rails的扩展功能。

它做对的两点:

1. 如同文章所述,优秀的API设计——Time.current.in_time_zone(‘America/Los_Angeles’) + 3.days – 4.months + 1.hour

2. Rails 重载了 Ruby 核心库 Time。全程操作统一对象,无需切换/困惑。

在 Python 世界中,pendulum 虽接近理想,但正如文章所述,它仍存在独立对象的繁琐(如 Temporal 与 Date 的区别),需要开发者“摸清”操作对象类型或进行强制转换。

覆盖核心库存在诸多风险,但对终端开发者而言却是极致体验。

若能直接使用new Date().add({ days: 1})该多便捷。

-- irjustin

>3天 – 4个月 + 1小时

这是否意味着时间单位这类特定概念会被定义为数字等更通用类型的成员?比如输入1.触发自动补全时,会出现days等所有选项?这种API设计模式简直是噩梦!

-- 乐于解答

在 Ruby 中,我猜这是通过猴子补丁实现的,所以确实会存在你提到的所有问题和担忧。

在 Kotlin 等现代语言中,存在扩展方法和属性的概念。你可以编写支持此语法的库,但 .days 属性仅在显式导入的文件中可用(本质上是静态函数调用的糖衣包装)。

-- greiskul

Ruby的妙处在于:你的自动补全几乎失效,IDE只能靠猜测而非语言服务来理解你的意图…

-- pprotas

我二十多年没用过自动补全了。这种“API”简单到根本不需要补全功能。

-- werdnapk

何必用这种轻蔑的语气?无论个人情况如何,很难相信你不知道几乎所有人都或多或少使用自动补全功能——比如记忆、发现或缩写类型/实例方法、参数列表等。而且为什么要把“API”加引号?将语言/平台中的接口称为API非常正常(例如“JavaScript的Date API”)。

无论如何,用户是否为这个API使用自动补全无关紧要——在此情境下,任何在API外部使用数字的人都会用到它,若该设计模式并非此API独有,那么整个平台都可能涉及。换言之,单个API的简洁性与问题本身无关。

-- happytoexplain

> 这种API设计模式简直是噩梦!

然而人们使用它数十年却鲜少出问题。我从未见过有人抱怨Ruby中为数字添加方法导致API爆炸,而Rails框架正是如此设计的。

可能的原因在于:流畅的API本身足够直观,无需依赖自动补全功能。(事实上,这一切发生在工具自动补全成为主流之前。)

-- michaelcampbell

> 3.天 – 4.月 + 1.小时

这有什么好处?

> Rails 重载了 Ruby 核心库 Time。无论做什么操作,你始终处于同一个对象中。

这有什么好处?

-- publicdebates

当你手里只有一把镐头时,所有东西都像铁轨…

-- shermantanktop

有点跑题了,但我真心希望JavaScript能分版本。让MDN能清理语法和API会很棒。

-- devilsdata

拼写错误:

    // 32至49之间的数字字符串默认表示2000年代:
    console.log( new Date( “49” ) );
    // 结果:Date Fri Jan 01 2049 00:00:00 GMT-0500 (Eastern Standard Time)

    // 33至99之间的数字字符串默认属于2000年代:
    console.log( new Date( “99” ) );
    // 结果:Date Fri Jan 01 1999 00:00:00 GMT-0500 (Eastern Standard Time)

第二个区间应从50开始,而非33

-- tensegrist

文章写得不错,但“Java早在1997年就弃用了Date类”的说法并不完全准确。虽然在JDK1.1引入Calendar类时确实废弃了大量Date的方法和构造器,但Date类本身从未被废弃,它一直是表示时间点的首选方式,直到JDK8(约2014年)引入java.time包提供“现代”方案。

-- rjrjrjrj

虽非完全准确,但他们确实废弃了所有构成日期功能的特性。注解中流露出深深的遗憾:

https://javaalmanac.io/jdk/1.2/api/java/util/Date.html

(虽未找到1.1版文档,但内容相同)

这恰是我最钟爱的例证之一——编程语言初始设计时往往对日期时间处理得一塌糊涂。如今Java拥有业内顶尖的时间API。

-- shellac

没错,它本质上成了长整型纪元毫秒值的类型包装器。根据惯例通常被视为不可变,尽管技术上并非如此——因为其设置器从未被移除。

最初的设计就糟糕透顶,后来添加的SQL Date/Timestamp等类更是雪上加霜。

但就我所见,java.time 确实是行业标杆。

-- rjrjrjrj

Java的时间与时长表示机制(深度借鉴Joda框架)应成为所有语言的标杆。

它几乎在每个方面都近乎完美——既能轻松实现正确操作,代码可读性也极佳。

-- cogman10

https://javaalmanac.io/jdk/1.1/api/java.util.Date.html

-- KwanEsq

该死,差一点就猜中了。另外:这个设计真是糟糕透顶。

-- shellac

仅时区处理功能就让Temporal值得等待。Date类会在你最不经意时悄然将所有数据转换为本地时间,我已数不清因此发布过多少次bug。

ZonedDateTime类型才是真正的亮点——终于能明确表示“这是纽约时间下午3点”,且在序列化/反序列化后仍保持纽约时间3点。使用Date时必须单独存储时区并自行重建,而这种操作最终总会出错。

唯一缺点是学习曲线。Date虽糟糕但至少逻辑一致,我们都背得滚瓜烂熟。Temporal虽更优却也更庞杂——需要在众多类型间反复切换。

-- jackfranklyn

>我已记不清因Date在最不经意时悄然将所有时间转换为本地时间而发布过多少次错误代码。

你是指getHours/getUTCHours这类方法吗?

-- GoblinSlayer

我怀疑ChatGPT的出现导致MDN日期API文档访问量至少下降了一半。

-- xeckr

说实话,这正是我巴不得永远不必再思考的类型。

-- qsort

用户流量减半,爬虫流量却暴增十倍。

-- ivanjermakov

用ChatGPT解答了所有moment/moment-tz疑问后,我搭建了调度程序。它最擅长爬取长期存活的API文档和解答。

-- thoughtpalette

Date API本身设计合理且相对规范。理解其原理后使用起来非常简单。其最大缺陷在于完全不支持时区处理,这正是使用Temporal的主要原因。

-- themafia

Date API简直糟糕透顶

-- LtdJorge

没错,这已是公认观点,平淡重复无益于讨论。

它很简单。但这种简单性导致许多功能缺失。我无法认同“必须时刻查阅MDN文档才能使用”的说法。它虽不至于违背逻辑,但也谈不上完美。

-- themafia

但这绝非简单。真正的简单应该是像1950年代COBOL程序员那样,用一个对象封装YYYY-MM-DD格式。现实中人们却围绕这种复杂性制造了成千上万种漏洞——即便基础用法也迫使你记住:月份编号从零起算,年份以1900为基准,而日期却遵循标准习惯从1起算。

-- acdha

它绝非简单,暗藏陷阱与自掘坟墓的机制。使用它堪称最快的自取灭亡之道(引用我共事过的一位资深开发者之言)。

-- devilsdata

确实。我曾为跨时区企业构建调度系统时,因日期API问题吃尽苦头

-- winstonp

查阅API文档时发现:Temporal.Duration竟提供带多年/月/日…直至纳秒级参数的构造函数,而Temporal.Instant却完全无法通过当前年/月/日创建实例,只能从Unix时间戳(或字符串)转换——这实在令人费解

这种功能似乎是必需的?还是说设计初衷是先将数字转换为字符串,再反转为 Temporal.Instant 对象?

-- Aardwolf

在 Duration 构造函数中将秒、分、时等默认设为零完全合理。但对于 Instant 而言,除非指定时区偏移量,否则将这些默认值设为零毫无意义。

事实上,静态方法 Instant.from 确实接受 RFC 9557 字符串格式,该格式要求包含两位小时数和时区偏移量,但可省略分秒:

Temporal.Instant.from(“2026-01-12T00+08:00”)

-- tshaddox

我认为设计者不希望用户混淆构造函数中字段的本地值与UTC值(尤其当采用本地值时需处理夏令时转换)。

-- Macha

这是因为仅凭年/月/日无法唯一标识时间点。不过你可以用这些值创建 Temporal.PlainDate(Time),再根据需求转换为其他类型(如需时区信息时)

-- sheept

虽已落后十年有余(JSR310发布于2014年),但仍是值得肯定的进展。我曾试图说服同事采用js-joda,但他们坚持使用moment.js认为这样更简洁。事实并非如此。

-- ckocagil

我记得遇到过 moment.js 的问题:添加日期库后包体积翻倍:https://github.com/moment/moment/issues/3376

-- gardnr

若你永远陷入这种境地:必须决定99是否解析为1999年,该如何处理100,以及如何划分90年代与当代年份的解析界限——你最好停下脚步,深呼吸,出去散散步。回来后请彻底放弃这个方案,重新设计。

实现这种功能不仅毫无价值,甚至具有负面价值。编写库或接口时,你应当着眼于用户95%的使用场景,力求让这些用例尽可能简单、可预测且远离潜在陷阱。而这恰恰背道而驰。这简直像十五岁时的我刚读完第一本PHP教程就写出来的代码,绝非任何有经验的人会认为是好方案的东西。

-- atoav

不过Temporal实际上并未被弃用。

https://caniuse.com/temporal

当前原生Temporal的全球可用率仅为1.81%。作为参考,IE11(!)的全球使用率都高于Temporal的原生支持率。对我们组织而言,这意味着距离能在生产环境使用Temporal可能还有数年之遥——因为通过polyfills审批实在太麻烦。

请注意,截至去年12月Chrome仍未内置该功能(即支持时间不足一个月),Safari至今仍未支持。

-- Someone1234

Chrome将在144版正式发布,即将上线。

-- senfiaj

一旦Chrome支持,多数开发者只需强制执行版本检测,告知用户“请使用最新版Chrome”即可解决问题。

-- winstonp

请永远不要做版本检测。应测试所需功能/方法是否存在——这在JS中轻而易举:if(Temporal)

依赖版本号只会巩固现有浏览器的垄断地位,阻碍新浏览器访问网站(即使浏览器完美实现了所有功能),更会助长伪造版本号/浏览器名称的行为,导致版本号作为识别信号的价值日益衰减。查看任何浏览器的User-Agent字符串即可见一斑。

-- promiseofbeans

值得庆幸的是,temporal-polyfill仅依赖于temporal-spec规范。通过审核相当顺利。

-- themafia

  >它完全不理解夏令时概念

既然要吹毛求疵(顺便说一句我完全支持这种态度),正确表述应为“daylight saving time”。

感谢分享,文章很棒。

-- schiffern

呃…两种写法都常见,BBC这类机构也这么用。

https://www.bbc.co.uk/future/article/20240308-daylight-savin… (此处同时出现“saving”和“savings”,估计他们拿不定主意该用哪个?)

-- azornathogron

这篇文章非常奇怪。它从未提及 Date.now() 方法,而是绕着主题打转,详尽地讨论了 Temporal 的等效约定。

若想让 Date 像 Temporal 那样工作,请仅将 Date.now() 作为起点。该方法返回自 1970 年 1 月 1 日起经过的毫秒数,这意味着初始输出是整数形式的数字类型。它并非静态值,而是表示当前时间与某个历史基准点之间的距离关系。没错,Temporal的API更友好,但其核心设计目标正是将时间作为关系性要素来呈现。

之后你可将Date.now()生成的数值格式化为任意所需格式。

-- austin-cheney

该文章还列举了时间戳引发的意外行为示例,所以…如何在不通过Date类的情况下转换为所需格式?请别提date-fns库

-- ffsm8

我认为你并未理解你回复的评论。日期数字(无论是来自Date.now()还是Temporal的等效实现)并非时间戳值,它只是一个数字。

-- austin-cheney

那么我认为你也没看懂你评论的文章,因为它本质上讨论的就是你轻描淡写带过的那个问题:

> 然后你可以将Date.now()生成的数字格式化成任何你想要的格式。

而整篇文章的核心在于如何将日期(无论是字符串还是时间戳)转换为日期对象。其思路是先将其格式化为字符串,再进行相等性检查、计算时长等操作。你提议使用时间戳的做法完全偏离了文章讨论的重点。

-- ffsm8

我认为你和我在这里采用的工作流,本质上是将时间戳(数字)视为唯一关键值。“转换后”的格式始终只是客户端渲染的临时结果,因此对转换过程的过度关注显得有些多余。不过我也承认,关于Date对象确实存在大量最好忽略的细节

-- elendee

等等——作者令人心碎的抱怨并非针对Date函数的所有“怪癖”和不一致性,而是它本身是个对象?更具体地说,在所有对象都具有可变属性的语言中,它竟是个具有可变属性的对象?

我的意思是,作者的结论没错。但我不同意他的理由。这就像因为独裁政权宣传用错了字体就憎恨它。

-- whiterook6

旧版JS Date API确实远非完美,我很高兴它被取代。但问题部分源于各种基于字符串的格式,以及人们使用时的粗心大意。更不用说时区、夏令时、闰秒等时间概念的复杂性了。

字符串格式只需遵循ISO 8601标准。若需解析非标准格式,可自行选用可靠的解析库。标准库本就不该承担解析无数冷门格式的责任。输出本地化/可读格式本就该由本地化API负责。

我认为许多涉及格式化的库/API都存在以美国为中心的设计缺陷,即倾向于将美国格式视为原生标准,而国际支持往往是事后补丁。尤其像JS Date API这类老旧实现更是如此。

-- Yaggo

日期格式的症结在于美国格式简直荒谬绝伦。凡涉及排序的场景都必须选择排序规则。日期格式方面,美国选了最荒谬的方案。Y-d-m这种写法根本不该存在。取消它就能解决90%基于字符串的格式问题。

-- ReptileMan

我住在澳大利亚,我们用d-m-Y格式,但连我都觉得直接采用ISO 8601标准就好了。拜托用标准格式吧,求你了,真的求你了。

-- devilsdata

我反对在时间对象前添加“Plain”前缀(如PlainDateTime)。该前缀无法说明类的行为特性——“Plain”究竟相对于什么?ZonedDateTime?我更倾向使用“Local”而非“Plain”,例如LocalDateTime。

-- mastermedo

Local表示日期采用当前机器时区,而PlainDateTime则无时区概念。它可能采用服务器时区或其他时区。关键区别在于:若不指定时区或偏移量,将其转换为Instant或ZonedDateTime毫无意义。

-- exyi

必须考虑PlainDate的存在——这是一种既无时间也无时区的Date类型

-- agos

Temporal库真的能用吗?上次检查时(大约去年),我必须使用Firefox的尖端版本才能获取它,其他任何浏览器都支持不了。不过我确实觉得它很棒,希望能看到原生支持。

-- LoganDark

你随时可查阅“Can I use _?”网站:https://caniuse.com/temporal

-- lobo_tuerto

自139[0]版本起已全面支持,即去年六月左右。

[0] https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe

-- halestock

似乎Chrome的下个版本会支持:https://caniuse.com/temporal

-- flenter

不,目前仅Firefox和Chrome的最新版本支持。Safari和Edge尚未支持。因此这篇文章有些操之过急(除非使用polyfill)。

-- noelwelsh

我的质疑不仅关乎解析、语法或“开发者体验”……我反对Date对象的核心在于:使用它意味着偏离了时间本身的本质属性。

我对这篇博文的内容本身并无异议,但这种夸张的写作风格实在令人难以忍受。这完全背离了写作的根本目的!

我不得不一直滚动到文末才找到核心观点,结果却令人失望。

> 与 Date 不同,我们与 Temporal 对象交互的方法会生成新的 Temporal 对象,而非要求我们在新实例上下文中使用它们

老兄,老实点吧。整篇博文本质上就是在讨论开发者体验优化,这无可厚非。我们都讨厌 JavaScript 中 Date 的工作方式。

-- sublinear

这绝非夸大其词——你惯用类似措辞的戏剧化表达(“时间本质的根本属性”),但这次纯粹是字面意义上的事实陈述。Date对象本应同时表示两种概念:时间戳与人类时间(日期、时间、时区)。但它实际只实现了前者。用它表示后者不过是我们被迫接受的权宜之计。

-- happytoexplain

严格来说,Temporal也违背了时间本身的本质。Temporal.Instant?在哪个加速参考系中?它支持等值,这根本是个荒谬的概念。

-- sfink

相关补充:Jiff (https://github.com/BurntSushi/jiff) 是受 Temporal API 启发的 Rust 库。

-- jrpelkonen

V8 实现时间序列所用的库同样基于 Rust 😉

-- nekevss

若执行new Date(‘2025-11-31T00:00:00Z’),结果会得到2025-12-01T00:00:00.000Z——这种怪异行为在解析输入日期字符串时极易引发错误。正是由于这种不一致性,我创建了一个基于正则表达式的轻量级包,用于在将日期字符串传递给new Date(stringDate)前进行验证。

https://www.npmjs.com/package/iso-8601-regex

-- lightningspirit

相关背景:最近重写uPlot的夏令时与时区处理时,我又不得不应对Date对象的各种限制。但最终方案让我相当满意——既规避了复杂逻辑、性能损失和依赖引入的问题: https://github.com/leeoniya/uPlot/pull/1072

趣味演示:https://leeoniya.github.io/uPlot/demos/timezones-dst.html

-- leeoniya

希望我们能改用DurationLike对象替代毫秒(或有时秒数?)作为纯数字参数,就像在setInterval中的用法那样。sleep({seconds 5})远比sleep(5000)更优雅。

-- krzkaczor

Temporal更像是对Date的修正,而非替代——它承认Date试图同时承载过多概念的局限性。

-- recdnd

尚未普及,当前采用率较低。

这无损于文章的质量,它对新API的介绍堪称一流。

-- moralestapia

仅凭Temporal不可变这一特性,就足以抵消我们即将承受的迁移阵痛。

-- nubskr

直到此刻我才意识到JavaScript的Date竟如此糟糕。

-- pmarreck

真正可悲的是,这个问题早在1995年就显而易见。若当时能多花些时间完善设计,数百万开发者本可避免被这些多余的怪癖绊倒。

-- acdha

公平地说,关于日期时间API的诸多教训其实更晚才被总结出来

-- agos

确实如此,但若观察C语言之外的通用方案,当时它已显陈旧。SQL开发者无需处理偏移量,且拥有更优化的日期运算原语。

-- acdha

我用Moment。

几年后,我仍在用Moment。

如今我依然用Moment。

现在…

-- noduerme

要是Safari能支持Temporal就好了…也许明年吧…

-- taf2

有个polyfill方案,不知道Safari能否兼容:
https://github.com/js-temporal/temporal-polyfill

-- devilsdata

有人知道Safari何时可能支持的链接吗?

-- nchmy

JSC团队在规范制定期间参与较深,目前已在TP版本中通过标志启用该功能。Safari大概会跟随Chrome的发布节奏,避免用户因网站故障投诉

-- promiseofbeans

目前可使用 polyfill:https://github.com/js-temporal/temporal-polyfill

-- devilsdata

我们终将需要构建Web2。以全新架构为基础,或许能实现更合理的网页架构——仅允许CSS与HTML,所有功能均通过Wasm运行。虽然Wasm需为此扩展功能,但至少能规避大量冗余设计。

-- mastermage

我不确定是否喜欢将值与格式化混入单一对象。但另一方面,任何方案都比那个糟糕的旧API要好。

-- zvqcMMV6Zcr

时间对象不存储格式化信息。除非您指的是省略时间、使用不同时区等操作——但这些并非格式化变更,而是逻辑上改变了数据的语义。正如myInt += 1并未改变myInt的“格式”。

请谨记:Date与Temporal对象在逻辑上截然不同。Date表示绝对时间点(时间戳),而Temporal对象表示人类时间(日历日期、时钟时间、时区)。由于缺乏更优结构,Date被用于表示人类时间——这正是问题的症结所在,也是Temporal等API试图填补的空白。

-- happytoexplain

我之前才真正理解,原以为Temporal.PlainDateTime中的“plain”指特定格式而非无时区模式。实际上它始终使用ISO8601进行内置toString转换,因此我并无异议。

-- zvqcMMV6Zcr

[已删除]

-- jdonaldson

更优的替代方案是什么?适用于哪些场景?

-- DarkNova6

兄弟这些广告我受不了

-- artursapek

没错,你应该用闪亮的新库取代漏洞百出的JavaScript标准…

除非你想让网站在一年前的旧浏览器上运行

或许十年后我们才能开始用新东西?

-- TZubiri

聚合库不是有吗?

-- wesselbindt

当然,但你同时增加了攻击面和几千字节的代码量。

-- TZubiri

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

你也许感兴趣的: