JavaScript 小把戲 - 讓 JSON.stringify 日期時保留時區資訊
2 |
JavaScript 在 JSON.stringify() 時會將 Date 型別轉成 ISO 8601 格式,而時區則一律轉成 UTC 時間 參考,故在網頁 new Date() 取得台北時間轉成 JSON 時會減 8 小時變成 YYYY-MM-DDTHH:mm:ss.sssZ
格式。
然而 ISO 8601 除了 YYYY-MM-DDTHH:mm:ss.sssZ
,也可以在後方加上 +HH:mm
或 -HH:mm
標註時區,例如:2024-01-01T12:34:56.789+08:00
。
而 .NET 端在處理時,能區別出含時區的 ISO 8601 正確解析,例如:
因此,我想擴充 JavaScript JSON.stringify(),視需求可將 Date 轉成含時區的 ISO 8601 格式:YYYY-MM-DDTHH:mm:ss+08:00
,這樣就可以直接傳台北時間給 API,伺服器端不需再做轉換。
這個問題在 JSON 日期轉換的時區陷阱有研究過,改寫 Date.toJSON() 即可,但我不想全面覆寫 JSON 行為,而是擴充成靠額外設定改變行為,所以決定玩點小把戲,為 Date 擴充一個 jsonDateKind 屬性,當設成 'local' 時再改傳有 '+08:00' 的格式,另外,我加一個 Date.newLocalKindDate()
方法,傳入參數與 new Date(...)
一致,但建立的 Date 物件會設定 .jsonDateKind = 'local'。程式碼很簡單,透過 Date.prototype 修改 Date 屬性及行為而已,另外,有用到上回研究的 JSON UTC 時間轉 yyyy-MM-mm HH:mm:ss 格式極簡解法,平日累積的小東西總會有適當時機發揮功效。
Date.prototype.jsonDateKind = 'utc';
Date.prototype.toJSON = function () {
if (this.jsonDateKind === 'local') {
var tzDiffHours = this.getTimezoneOffset() / 60;
var tzDiffMinutes = this.getTimezoneOffset() % 60;
return this.toLocaleString('sv').replace(' ', 'T') +
(this.getMilliseconds() > 0 ?
'.' + this.getMilliseconds().toString().padStart(3, '0') : '') +
(tzDiffHours > 0 ? '-' : '+') +
Math.abs(tzDiffHours).toString().padStart(2, '0') + ':' +
tzDiffMinutes.toString().padStart(2, '0');
} else {
return this.toISOString();
}
};
Date.newLocalKindDate = function () {
const date = new Date(...arguments);
date.jsonDateKind = 'local';
return date;
}
測試成功。
附上原始碼跟線上展示:
<!DOCTYPE html>
<html>
<head>
<title>Date jsonDateKind Property</title>
<style>
#script {
width: calc(100% - 24px);
height: 210px;
padding: 12px;
}
#result {
width: calc(100% - 24px);
height: 150px;
background-color: #eee;
padding: 10px;
margin-top: 10px;
}
</style>
</head>
<body>
<textarea id="script">
const now = new Date();
now.jsonDateKind = 'local';
const date = new Date(2024, 0, 1, 12, 34, 56, 789);
date.jsonDateKind = 'local';
const cust = Date.newLocalKindDate(2012, 11, 21, 12, 21, 0, 0);
var data = {
s: "Hello World!",
i: 1234,
d1: new Date(),
d2: now,
d3: date,
d4: cust
};
</textarea>
<button id="btn">Convert to Json</button>
<pre id="result">
</pre>
<script>
Date.prototype.jsonDateKind = 'utc';
Date.prototype.toJSON = function () {
if (this.jsonDateKind === 'local') {
var tzDiffHours = this.getTimezoneOffset() / 60;
var tzDiffMinutes = this.getTimezoneOffset() % 60;
return this.toLocaleString('sv').replace(' ', 'T') +
(this.getMilliseconds() > 0 ?
'.' + this.getMilliseconds().toString().padStart(3, '0') : '') +
(tzDiffHours > 0 ? '-' : '+') +
Math.abs(tzDiffHours).toString().padStart(2, '0') + ':' +
tzDiffMinutes.toString().padStart(2, '0');
} else {
return this.toISOString();
}
};
Date.newLocalKindDate = function () {
const date = new Date(...arguments);
date.jsonDateKind = 'local';
return date;
}
document.getElementById('btn').addEventListener('click', function () {
var script = document.querySelector('#script').textContent;
eval(script);
var json = JSON.stringify(data, null, 2);
document.querySelector('#result').textContent = json;
});
</script>
</body>
</html>
This article demonstrates how to use JavaScript prototype to extend the Date object, implementing control over whether to include timezone information when JSON is stringified.
Comments
# by Masu
也可以試試 JSON.stringify 的第二個參數:replacer function replacer(key, value) { if (this[key] instanceof Date) { var self = this[key]; var tzDiffHours = self.getTimezoneOffset() / 60; var tzDiffMinutes = self.getTimezoneOffset() % 60; return ( self.toLocaleString("sv").replace(" ", "T") + (self.getMilliseconds() > 0 ? "." + self.getMilliseconds().toString().padStart(3, "0") : "") + (tzDiffHours > 0 ? "-" : "+") + Math.abs(tzDiffHours).toString().padStart(2, "0") + ":" + tzDiffMinutes.toString().padStart(2, "0") ); } return value; } const obj = { name: "example", date: new Date(), }; JSON.stringify(obj, replacer);
# by Jeffrey
to Masu, 有考慮過,但如此每個 JSON.stringify() 呼叫點都要改,還可能位於第三方程式庫內。若覆寫 stringify() 要考慮原本就有傳 replacer,要合併兩邊邏輯更複雜些,故評估從 Date.toJSON() 下手,走微創手術路線。