JavaScript 自定义日期格式化函数详细解析

作者:半夏

时间:2026-10-27

浏览:625

本文深入解析 JavaScript 中自定义日期格式化的多种实现方案。对比分析了扩展 Date.prototype 与独立函数的优劣,详细解读了 Steven Levithan 经典正则替换算法、meizz 简洁版及 Ext JS 框架的实现逻辑,并提供了从基础到高阶的代码示例与原理说明。

文章目录

JavaScript 日期格式化概述与原型扩展

在 JavaScript 开发中,扩展 Date.prototype 是一种常见做法。原生 Date 类仅提供了获取日期各部分的方法(如 getDate、getMonth 等),却缺乏直接转换为特定格式字符串的功能。因此,开发者通常利用这些基础方法封装出日期格式化函数。该函数既可以定义为 Date 对象的原型方法,也可以作为独立函数存在。例如,定义为 Date.prototype.format = function(date){……} 后,调用时可写作 new Date().format('YYYY:MM:DD'),使其看起来如同原生方法。然而,直接修改原型可能被视为对 JS 原型的“入侵”,在设计 API 时需权衡此问题。建议用户根据实际需求自行选择,只要调用方式不同且不影响逻辑即可。

以下示例展示了一个独立编写的 JavaScript 日期格式化函数。我们重点考察其实现原理及所用技术。虽然传统的字符串拼接方式(如使用 indexOf 和 substr)能实现功能,但效率较低且代码冗长。因此,引入正则表达式是更优选择:先定义字符串正则,再通过匹配结果生成格式化后的字符串。下面介绍来自 Steven Levithan 的经典实现案例。

代码如下:

/**
* Date Format 1.2.3
* @credit Steven Levithan <stevenlevithan.com> Includes enhancements by Scott Trenda <scott.trenda.net> and Kris Kowal <cixar.com/~kris.kowal/>
* Accepts a date, a mask, or a date and a mask.
* Returns a formatted version of the given date.
* The date defaults to the current date/time.
* The mask defaults to dateFormat.masks.default.
 */
dateFormat = (function(){
// 正则笔记,1、token,(?:)表示非捕获分组;/1 反向引用(思考:{1,2}可否和/1一样意思?);根据这里的意义[LloSZ]表示括号内的任意一个字符拿去匹配,很简单,但暂时不明白/L|l|o|S|Z/在解析日期时的作用;最后的两组“或”是匹配引号和引号内的内容(无所谓双引号或单引号)。
var token = /d{1,4}|m{1,4}|yy(?:yy)?|([HhMsTt])/1?|[LloSZ]|"[^"]*"|'[^']*'/g,
// 2、timezone, [PMCEA][SDP]产生两个字符的消耗;该reg的都是非捕获分组,可加快正则速度。
timezone = //b(?:[PMCEA][SDP]T|(?:Pacific|Mountain|Central|Eastern|Atlantic) (?:Standard|Daylight|Prevailing) Time|(?:GMT|UTC)(?:[-+]/d{4})?)/b/g,
timezoneClip = /[^-+/dA-Z]/g,
// 不足两位填充字符,或可指定位数
pad = function (val, len){
val = String(val);
len = len || 2;
while (val.length < len) val = "0" + val;
return val;
        };
// 为什么返回一个function,因为前面说明的变量都变作常量,下面返回的参数才是真正到时执行的函数。这一点透过闭包的写法来实现。如英文注释说的,可以提速。
// Regexes and supporting functions are cached through closure
// 参数说明:date: Date 被解析的日期或新日期;mask:String 格式化日期的模板;utc:Stirng 可选的UTC。
return function (date, mask, utc) {
var i18n = dateFormat.i18n;
var masks = dateFormat.masks;
// You can't provide utc if you skip other args (use the "UTC:" mask prefix)
// 如果只有一个参数,其该参数是不包含数字的字符串,则视作这个参数为mask。date由下一个if中的new Date产生,那么date就是现在的日期。
if (arguments.length == 1 && Object.prototype.toString.call(date) == "[object String]" && !//d/.test(date)) {
mask = date;
date = undefined;
        }
// Passing date through Date applies Date.parse, if necessary
date = date ? new Date(date) : new Date;
if (isNaN(date)) throw SyntaxError("invalid date");
// 通过判断多种情况明确mask是什么,不论前面是如何指定的。留意 || 的技巧。
mask = String(masks[mask] || mask || masks["default"]);
// Allow setting the utc argument via the mask
if (mask.slice(0, 4) == "UTC:") {
mask = mask.slice(4);
utc = true;
        }
// 分两种情况,用UTC格式的情况和一般的。注意通过JS的字面索引也可以返回方法的成员。
var _ = utc ? "getUTC" : "get",
d = date[_ + "Date"](),
D = date[_ + "Day"](),
m = date[_ + "Month"](),
y = date[_ + "FullYear"](),
H = date[_ + "Hours"](),
M = date[_ + "Minutes"](),
s = date[_ + "Seconds"](),
L = date[_ + "Milliseconds"](),
o = utc ? 0 : date.getTimezoneOffset(),
flags = {
d: d,
dd: pad(d),
ddd: i18n.dayNames[D],
dddd: i18n.dayNames[D + 7],// 位宽:7, 见 dateFormat.dayNames。
m: m + 1, // 从0开始起月份
mm: pad(m + 1),
mmm: i18n.monthNames[m],
mmmm: i18n.monthNames[m + 12], // 位宽:12,见 dateFormat.monthNames
yy: String(y).slice(2),// 字符串slice()的用法
yyyy: y,
h: H % 12 || 12, // h表示12小时制,h除以12(因为十二进制),取余的结果为12小时制的。
hh: pad(H % 12 || 12),
H: H,
HH: pad(H),
M: M,
MM: pad(M),
s: s,
ss: pad(s),
l: pad(L, 3), // Max,999ms
L: pad(L > 99 ? Math.round(L / 10) : L),
// 大小写有影响
t: H < 12 ? "a" : "p",
tt: H < 12 ? "am" : "pm",
T: H < 12 ? "A" : "P",
TT: H < 12 ? "AM" : "PM",
// 这一步求 timezone,就是要处理一下。
// 前文有timezone,timezoneClip = /[^-+/dA-Z]/g,
// String返回日期的字符串形式,包括很长的……UTC……信息
// 假如没有,则[""].pop() 返回空字符
Z: utc ? "UTC" : (String(date).match(timezone) || [""]).pop().replace(timezoneClip, ""),
// 4位的TimezoneOffset
o: (o > 0 ? "-" : "+") + pad(Math.floor(Math.abs(o) / 60) * 100 + Math.abs(o) % 60, 4),
// 求英文的["th", "st", "nd", "rd"],依据是d的个位数
S: ["th", "st", "nd", "rd"][d % 10 > 3 ? 0 : (d % 100 - d % 10 != 10) * d % 10]
            };
return mask.replace(token, function ($0 /*很好$0,须知$1、$2由系统占用了*/) {
// 怎么检测某个对象身上有指定的属性?用 in 检测即可!
// $0.slice(1, $0.length - 1);?什么意思?
return $0 in flags ? flags[$0] : $0.slice(1, $0.length - 1);
        });
    };
})();

Steven Levithan 经典正则实现解析

该段代码对日期处理考虑得比较周全,我们就进入原理看看它的奥秘,——是怎么处理日期的!

日期字符串模板中,我们约定用 yyyy/mm/dd 等符号分别表示日期的元素,如 y 代表年份,m 代表月份,d 代表日期。注意大小写区分,大写 M 代表分钟,小写 m 代表月份。这是一份人为规范的约定,即代码中的“mask”。遵循此约定输入格式化模板,即可将日期对象转换为字符串。解析过程是:先按 Mask 要求逐个获取日期元素(利用 getDate、getMonth 等方法),再根据 Mask 的真实条件,通过 Mask.replace(正则, 元素) 方法进行字符串模板与元素的替换。替换过程以 flags 为对照表逐一匹配。正则部分的关键在于理解 token 定义和 replace() 函数的执行过程,详见上述代码注释。

如果每次都要输入冗长的 Mask 字符串岂不是很累?我们可以通过定义常量的方法缩减工作量:

代码如下:

dateFormat.masks = {
"default": "ddd mmm dd yyyy HH:MM:ss",
shortDate: "m/d/yy",
shortDate2: "yy/m/d/h:MM:ss",
mediumDate: "mmm d, yyyy",
longDate: "mmmm d, yyyy",
fullDate: "dddd, mmmm d, yyyy",
shortTime: "h:MM TT",
mediumTime: "h:MM:ss TT",
longTime: "h:MM:ss TT Z",
isoDate: "yyyy-mm-dd",
isoTime: "HH:MM:ss",
isoDateTime: "yyyy-mm-dd'T'HH:MM:ss",
isoUtcDateTime: "UTC:yyyy-mm-dd'T'HH:MM:ss'Z'"
// 加入中国类型的日期 @Edit 2010.8.11
,ChineseDate :'yyyy年mm月dd日 HH时MM分'
}
dateFormat.i18n = {
dayNames: [
"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat",
"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"
    ],
monthNames: [
"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"
    ]
};

meizz 简洁版与可读性重构

Steve 的 dateFormat 足可以完成大多数日期转化的任务,不过在茫茫代码中,我们找到了更优的解法,不出20行代码,把正则运用得收放自如,就是来自月影前辈的JS !


代码如下:

Date.prototype.format = function(format) //author: meizz
{
var o = {
"M+" : this.getMonth()+1, //month
"d+" : this.getDate(), //day
"h+" : this.getHours(), //hour
"m+" : this.getMinutes(), //minute
"s+" : this.getSeconds(), //second
"q+" : Math.floor((this.getMonth()+3)/3), //quarter
"S" : this.getMilliseconds() //millisecond
  }
if(/(y+)/.test(format)) format=format.replace(RegExp.$1,
(this.getFullYear()+"").substr(4 - RegExp.$1.length));
for(var k in o)if(new RegExp("("+ k +")").test(format))
format = format.replace(RegExp.$1,
RegExp.$1.length==1 ? o[k] :
("00"+ o[k]).substr((""+ o[k]).length));
return format;
}
alert(new Date().format("yyyy-MM-dd hh:mm:ss"));

原理上与 Steve 方法相似,但代码更浓缩,集技巧性与全面性于一身。从源码第12行开始,test() 方法不仅能检测匹配,还能记忆匹配结果,产生 RegExp.$1 结果组来处理年份(起初认为 test() 效率高不产生结果,实则不然)。接着使用 new RegExp 在字符串形式创建正则表达式实例,直接与 o 的 hash 表对接。随后测试是否命中匹配,若命中则进行替换。

另外,代码中的 ("00" + o[k]).substr(String(o[k]).length) 也是有趣的地方。前面加上两个零的目的是为了取数组的最后两位。这是综合利用 substr() 方法的技巧:substr 第一个参数是开始截取的 index,若不指定第二个参数则保留字符串到最后(str.length)。于是,我们事先加多了多少位,原本固定的字符串长度不变(String(o[k].length))的情况下,那么就留下多少个位。(p.s “00”相当于占位符,亦可用其他字符串“XX”代替无区别)

仍然觉得这段代码有不少的困难?我们尝试把月影的函数重写为可读性较强的代码,原理上趋于一致可是没那么多的技巧,相信这样可以节省大家的时间,回头再去看月影的代码也不迟。

代码如下:

date = {
format: function(date, format){
date = new Date(date); // force con.
date = {
year : date.getFullYear()
,month : date.getMonth() + 1 // 月份, 月份从零算起
,day : date.getDate()
,hour : date.getHours()
,minute : date.getMinutes()
,second : date.getSeconds()
,milute : date.getMilliseconds()
  };
var
match
,reg = /(y+)|(Y+)|(M+)|d+|h+|m+|s+|u+/g;
while((match = reg.exec(format)) != null){
match = match[0];
if(/y/i.test(match)){
format = format.replace(match, date.year);
      }
if(match.indexOf('M') != -1){
format = format.replace(match, date.month);
      }
if(match.indexOf('d') != -1){
format = format.replace(match, date.day);
      }       
if(match.indexOf('h') != -1){
format = format.replace(match, date.hour);
      }
if(match.indexOf('m') != -1){
format = format.replace(match, date.minute);
      } 
if(match.indexOf('s') != -1){
format = format.replace(match, date.second);
      } 
if(match.indexOf('u') != -1){
format = format.replace(match, date.milute);
      }           
  }
return format;
 }
};

Ext JS 框架日期格式化实现

2011--1-7:

从 ext 4.0 淘到的日期格式化的代码,怎么讲字符串转为 js 标准日期?看看新 ext 是怎么做的?

代码如下:

    /**
* 按照特定的格式模式格式化日期。
* Parse a value into a formatted date using the specified format pattern.
* @param {String/Date} value 要格式化的值(字符串必须符合JavaScript日期对象的格式要求,参阅<a href="http://www.w3schools.com/jsref/jsref_parse.asp" mce_href="http://www.w3schools.com/jsref/jsref_parse.asp">parse()</a>)The value to format (Strings must conform to the format expected by the javascript
* Date object's <a href="http://www.w3schools.com/jsref/jsref_parse.asp" mce_href="http://www.w3schools.com/jsref/jsref_parse.asp">parse()</a> method)
* @param {String} format (可选的)任意的日期格式化字符串。(默认为'm/d/Y')(optional) Any valid date format string (defaults to 'm/d/Y')
* @return {String} 已格式化字符串。The formatted date string
     */
date: function(v, format) {
if (!v) {
return "";
        }
if (!Ext.isDate(v)) {
v = new Date(Date.parse(v));
        }
return v.dateFormat(format || Ext.util.Format.defaultDateFormat);
    }

date 构造器还可以通过算出距离1970起为多久的毫秒数来确定日期?——的确,这样也行,——也就说,举一反三,从这个问题说明,js 日期最小的单位是毫秒。

最终版本:

代码如下:

/**
* 日期格式化。详见博客文章:http://blog.csdn.net/zhangxin09/archive/2011/01/01/6111294.aspx
* e.g: new Date().format("yyyy-MM-dd hh:mm:ss")
* @param {String} format
* @return {String}
*/
Date.prototype.format = function (format) {
var $1, o = {
"M+": this.getMonth() + 1, // 月份,从0开始算
"d+": this.getDate(), // 日期
"h+": this.getHours(), // 小时
"m+": this.getMinutes(), // 分钟
"s+": this.getSeconds(), // 秒钟
// 季度 quarter
"q+": Math.floor((this.getMonth() + 3) / 3),
"S": this.getMilliseconds() // 千秒
    };
var key, value;
if (/(y+)/.test(format)) {
 $1 = RegExp.$1, 
 format = format.replace($1, String(this.getFullYear()).substr(4 - $1));
 }
for (key in o) { // 如果没有指定该参数,则子字符串将延续到 stringvar 的最后。
 if (new RegExp("(" + key + ")").test(format)) {
 $1 = RegExp.$1,
 value = String(o[key]),
 value = $1.length == 1 ? value : ("00" + value).substr(value.length),
 format = format.replace($1, value);
 }
 }
 return format;
}

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章