
🎯 起因
昨天刷面试题的时候,看到一道很有意思的题:
如果不允许使用 Date 对象和任何时间格式化函数,给你一个时间戳,你能手动计算出对应的年月日时分秒吗?
于是决定挑战一下自己:不查任何现成的库,纯手工实现一个时间戳转换器。
🤔 什么是时间戳?
首先介绍一下什么是时间戳,Unix 时间戳就是从 1970 年 1 月 1 日 00:00:00 UTC 开始,到现在过了多少秒。
javascript
// 比如 1609459200 这个数字
// 表示从1970-01-01 00:00:00开始过了1609459200秒
// 对应北京时间就是:2021-01-01 08:00:00
为什么是 1970 年?据说是因为 Unix 系统诞生的年代...反正就这么约定的,我们照着来就行。
🧠 问题分析
要把一个纯数字转成2024-12-02 08:00:00
这种格式,需要解决几个问题:
- 时区问题 - UTC 时间要加 8 小时变成北京时间
- 闰年问题 - 闰年 2 月有 29 天,平年只有 28 天
- 年份计算 - 从总天数推算是哪一年
- 月份计算 - 每月天数不同,要逐个计算
- 时分秒计算 - 从剩余秒数中提取
看起来很复杂,但拆开来其实就是小学数学题。
🚀 开始实现
最终效果如下

🌍 步骤 1:时区调整
这个最简单,UTC+8 就是加 8 小时的秒数:
javascript
adjustTimezone(timestamp) {
return timestamp + 8 * 3600 // 加28800秒
}

📅 步骤 2:算天数
用除法和取模,小学就学过:
javascript
calculateDays(adjustedTimestamp) {
const totalDays = Math.floor(adjustedTimestamp / 86400) // 一天86400秒
const secondsInDay = adjustedTimestamp % 86400
return { totalDays, secondsInDay }
}
totalDays
是从 1970 年 1 月 1 日开始的总天数,secondsInDay
是当天已经过去的秒数。
🗓️ 步骤 3:闰年和年份
上网查询一番:
- 能被 400 整除的是闰年
- 能被 4 整除但不能被 100 整除的也是闰年
- 其他都不是
javascript
isLeapYear(year) {
return year % 400 === 0 || (year % 4 === 0 && year % 100 !== 0)
}
calculateYear(totalDays) {
let year = 1970
let remainingDays = totalDays
while (remainingDays >= 0) {
const daysInYear = this.isLeapYear(year) ? 366 : 365
if (remainingDays >= daysInYear) {
remainingDays -= daysInYear
year++
} else {
break
}
}
return { year, remainingDays }
}
就是从 1970 年开始一年年往后数,直到剩余天数不够一年为止。

📆 步骤 4:月份和日期
这里有个坑,我一开始算错了好几次。每个月天数不一样,2 月还要看闰年:
javascript
calculateMonth(year, remainingDays) {
let month = 1
const daysInMonths = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
while (month <= 12) {
let daysInThisMonth = daysInMonths[month - 1]
if (month === 2 && this.isLeapYear(year)) {
daysInThisMonth = 29 // 闰年2月29天
}
if (remainingDays >= daysInThisMonth) {
remainingDays -= daysInThisMonth
month++
} else {
break
}
}
const day = remainingDays + 1 // 注意要+1,因为日期从1开始
return { month, day }
}
特别注意最后的+1
,我忘记加过,结果每个月 1 号都变成了 0 号。
⏰ 步骤 5-7:时分秒
这个就是纯粹的数学计算了:
javascript
calculateHour(secondsInDay) {
return Math.floor(secondsInDay / 3600)
}
calculateMinute(secondsInDay) {
return Math.floor((secondsInDay % 3600) / 60)
}
calculateSecond(secondsInDay) {
return secondsInDay % 60
}
🔧 代码整合
把所有步骤串起来,就是一个完整的转换器:
javascript
class TimestampConverter {
constructor(timezoneOffset = 8) {
this.TIMEZONE_OFFSET = timezoneOffset * 3600 // 时区偏移秒数
this.SECONDS_PER_DAY = 86400 // 一天86400秒
this.SECONDS_PER_HOUR = 3600 // 一小时3600秒
this.SECONDS_PER_MINUTE = 60 // 一分钟60秒
// 各月天数:1月31天,2月28天...
this.DAYS_IN_MONTHS = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
}
/**
* 主函数:把所有步骤串起来
*/
convert(timestamp) {
// 第1步: 调整时区
const adjustedTimestamp = this.adjustTimezone(timestamp)
// 第2步: 算天数
const { totalDays, secondsInDay } = this.calculateDays(adjustedTimestamp)
// 第3步: 算年份
const { year, remainingDays } = this.calculateYear(totalDays)
// 第4步: 算月份和日期
const { month, day } = this.calculateMonth(year, remainingDays)
// 第5-7步: 算时分秒
const hour = this.calculateHour(secondsInDay)
const minute = this.calculateMinute(secondsInDay)
const second = this.calculateSecond(secondsInDay)
// 格式化成好看的样子
return `${year}-${this.pad(month)}-${this.pad(day)} ${this.pad(
hour
)}:${this.pad(minute)}:${this.pad(second)}`
}
// 数字补零:1 → 01
pad(num) {
return num.toString().padStart(2, '0')
}
// ... 其他方法见上面的代码
}
用起来超简单:
javascript
const converter = new TimestampConverter()
console.log(converter.convert(1609459200)) // 2021-01-01 08:00:00
🧪 测试效果
咱们弄几个经典测试用例试试:
javascript
const tests = [
{ ts: 0, expect: '1970-01-01 08:00:00', desc: 'Unix纪元起点' },
{ ts: 946684800, expect: '2000-01-01 08:00:00', desc: '千禧年' },
{ ts: 951782400, expect: '2000-02-29 08:00:00', desc: '闰年2月29日' },
{ ts: 1609459200, expect: '2021-01-01 08:00:00', desc: '2021新年' },
{ ts: 1733097600, expect: '2024-12-02 08:00:00', desc: '随机日期' }
]
tests.forEach((test) => {
const result = converter.convert(test.ts)
const isOK = result === test.expect
console.log(`${test.desc}: ${isOK ? '✅ 通过' : '❌ 失败'} ${result}`)
})

✨ 最后的效果

完整代码如下
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>时间戳转换</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 20px auto;
padding: 20px;
line-height: 1.6;
}
.section {
margin-bottom: 30px;
padding: 20px;
border: 1px solid #ddd;
border-radius: 5px;
}
input,
button {
padding: 8px 12px;
margin: 5px;
border: 1px solid #ccc;
border-radius: 3px;
}
button {
background: #007bff;
color: white;
cursor: pointer;
}
button:hover {
background: #0056b3;
}
.result {
background: #f8f9fa;
padding: 15px;
margin: 10px 0;
border-radius: 3px;
font-family: monospace;
}
.test-item {
margin: 5px 0;
padding: 5px;
background: #f0f0f0;
}
.pass {
color: green;
}
.fail {
color: red;
}
</style>
</head>
<body>
<h1>时间戳转换演示</h1>
<div class="section">
<h2>基础转换</h2>
<input
type="number"
id="timestamp"
placeholder="输入时间戳"
value="1723276800"
/>
<button onclick="convert()">转换</button>
<button onclick="useNow()">使用当前时间</button>
<div class="result" id="result">点击转换查看结果...</div>
</div>
<div class="section">
<h2>测试</h2>
<button onclick="runTest()">运行测试</button>
<div id="testResult"></div>
</div>
<script>
class TimestampConverter {
constructor(timezoneOffset = 8) {
this.TIMEZONE_OFFSET = timezoneOffset * 3600 // 时区偏移秒数
this.SECONDS_PER_DAY = 86400 // 每天秒数
this.SECONDS_PER_HOUR = 3600 // 每小时秒数
this.SECONDS_PER_MINUTE = 60 // 每分钟秒数
this.DAYS_IN_MONTHS = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
}
/**
* 判断是否为闰年
*/
isLeapYear(year) {
return year % 400 === 0 || (year % 4 === 0 && year % 100 !== 0)
}
/**
* 获取指定年月的天数
*/
getDaysInMonth(year, month) {
if (month === 2 && this.isLeapYear(year)) {
return 29
}
return this.DAYS_IN_MONTHS[month - 1]
}
/**
* 第1步: 时区调整
*/
adjustTimezone(timestamp) {
return timestamp + this.TIMEZONE_OFFSET
}
/**
* 第2步: 计算总天数和当日秒数
*/
calculateDays(adjustedTimestamp) {
const totalDays = Math.floor(adjustedTimestamp / this.SECONDS_PER_DAY)
const secondsInDay = adjustedTimestamp % this.SECONDS_PER_DAY
return { totalDays, secondsInDay }
}
/**
* 第3步: 计算年份
*/
calculateYear(totalDays) {
let year = 1970
let remainingDays = totalDays
while (remainingDays >= 0) {
const daysInYear = this.isLeapYear(year) ? 366 : 365
if (remainingDays >= daysInYear) {
remainingDays -= daysInYear
year++
} else {
break
}
}
return { year, remainingDays }
}
/**
* 第4步: 计算月份和日期
*/
calculateMonth(year, remainingDays) {
let month = 1
while (month <= 12) {
const daysInMonth = this.getDaysInMonth(year, month)
if (remainingDays >= daysInMonth) {
remainingDays -= daysInMonth
month++
} else {
break
}
}
const day = remainingDays + 1
return { month, day }
}
/**
* 第5步: 计算小时
*/
calculateHour(secondsInDay) {
return Math.floor(secondsInDay / this.SECONDS_PER_HOUR)
}
/**
* 第6步: 计算分钟
*/
calculateMinute(secondsInDay) {
return Math.floor(
(secondsInDay % this.SECONDS_PER_HOUR) / this.SECONDS_PER_MINUTE
)
}
/**
* 第7步: 计算秒
*/
calculateSecond(secondsInDay) {
return secondsInDay % this.SECONDS_PER_MINUTE
}
/**
* 格式化数字为两位数
*/
pad(num) {
return num.toString().padStart(2, '0')
}
/**
* 主转换函数 - 调用各个步骤
*/
convert(timestamp) {
// 第1步: 时区调整
const adjustedTimestamp = this.adjustTimezone(timestamp)
// 第2步: 计算天数
const { totalDays, secondsInDay } =
this.calculateDays(adjustedTimestamp)
// 第3步: 计算年份
const { year, remainingDays } = this.calculateYear(totalDays)
// 第4步: 计算月份和日期
const { month, day } = this.calculateMonth(year, remainingDays)
// 第5-7步: 计算时分秒
const hour = this.calculateHour(secondsInDay)
const minute = this.calculateMinute(secondsInDay)
const second = this.calculateSecond(secondsInDay)
// 格式化输出
return `${year}-${this.pad(month)}-${this.pad(day)} ${this.pad(
hour
)}:${this.pad(minute)}:${this.pad(second)}`
}
}
// 全局转换器实例
const converter = new TimestampConverter()
/**
* 转换时间戳
*/
function convert() {
const timestamp = parseInt(document.getElementById('timestamp').value)
if (isNaN(timestamp)) {
alert('请输入有效的时间戳')
return
}
const result = converter.convert(timestamp)
const nativeResult = new Date(timestamp * 1000)
.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false
})
.replace(/\//g, '-')
const isMatch =
result.replace(/\s+/g, ' ') === nativeResult.replace(/\s+/g, ' ')
document.getElementById('result').innerHTML = `
<strong>自实现结果:</strong> ${result}<br>
<strong>原生方法对比:</strong> ${nativeResult}<br>
<strong>匹配状态:</strong> ${isMatch ? '✅ 正确' : '❌ 不匹配'}
`
}
/**
* 使用当前时间
*/
function useNow() {
document.getElementById('timestamp').value = Math.floor(
Date.now() / 1000
)
convert()
}
/**
* 运行测试用例
*/
function runTest() {
const tests = [
{ ts: 0, expect: '1970-01-01 08:00:00', desc: 'Unix纪元' },
{ ts: 946684800, expect: '2000-01-01 08:00:00', desc: '2000年' },
{ ts: 951782400, expect: '2000-02-29 08:00:00', desc: '闰年2月29日' },
{ ts: 1609459200, expect: '2021-01-01 08:00:00', desc: '2021年' },
{ ts: 1733097600, expect: '2024-12-02 08:00:00', desc: '2024年12月' }
]
let html = '<h3>测试结果:</h3>'
let passed = 0
tests.forEach((test) => {
const result = converter.convert(test.ts)
const isPass = result === test.expect
if (isPass) passed++
html += `
<div class="test-item">
<span class="${isPass ? 'pass' : 'fail'}">
${isPass ? '✅' : '❌'}
</span>
${test.desc}: ${
isPass ? '通过' : `期望 ${test.expect}, 得到 ${result}`
}
</div>
`
})
html += `<p><strong>通过率: ${passed}/${tests.length}</strong></p>`
document.getElementById('testResult').innerHTML = html
}
// 初始化
convert()
</script>
</body>
</html>