我们对 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); }); }; })();
该段代码对日期处理考虑得比较周全,我们就进入原理看看它的奥秘,——是怎么处理日期的!
日期字符串模板中,我们约定用 yyyy/mm/dd 等的有意义的符号分别表示日期中某一个元素,像 y 即 year 某一年份,m 即 month 某一月份,d 即 day 某一天,如果是大写的话还要注意区分开来,大写 M 代表分钟,小写 m 是月份。总之,这是一份我们人为规范好的约定,即上述代码所谓的“mask”,遵照此约定输入欲格式化模式的参数,便可将日期类型的值输出可供打印的字符串。至于解析的日期过程是,先按照 Mask 的全部要求,逐个获取到日期的每一个元素(getDate(),getMinute()……可以很快获取到),接着按照 Mask 真实的条件是什么,即Mask.replace(正则, 元素)方法进行字符串模板与元素之间的替换,替换的过程还是以 flag 为标志去逐一匹配的对照表。至于正则部分,关键在于理解 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" ] };
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,若不指定第二个参数 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; } };
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; }
JavaScript自定义日期格式化函数详细解析
声明:登载此文出于传递更多信息之目的,并不意味着赞同其观点或证实其描述。
Reply on: @reply_date@
@reply_contents@