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() 下手,走微創手術路線。

Post a comment