我们对 JavaScript 扩展其中一个较常的做法便是对 Date.prototype 的扩展。因为我们知道,Date 类只提供了若干获取日期元素的方法,如 getDate(),getMinute()……却没有一个转换为特定字符串的格式化方法。故所以,利用这些细微的方法,加以封装,组合我们想要的日期字符串形式。一般来说,该格式化函数可以定义在 Date 对象的原型身上,也可以独立一个方法写出。定义原型方法的操作如 Date.prototype.format = function(date){……},使用时候直接 new Date().format(YYYY:MM:DD) 即可,仿佛就是 Date 对象的原生方法。但是定义原型方法却略嫌有“入侵” JS 原型的不足。设计 API 之时必须考虑这个问题。我的建议是,用户按照自己的判断去做决定,只是调用的方式不同,不影响过程的逻辑即可。
下面的一个例子就是以独立函数写出的 JavaScript 日期格式化函数,独立的 format 函数。回到格式化的这一知识点上,我们考查的是怎么实现的、运用了哪些原理。传统字符串拼接如 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);
});
};
})();
该段代码对日期处理考虑得比较周全,我们就进入原理看看它的奥秘,——是怎么处理日期的!