您好,欢迎访问一九零五行业门户网

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

我们对 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 includes enhancements by scott trenda and 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             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                 tt:   h                 t:    h                 tt:   h                 // 这一步求 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日期对象的格式要求,参阅parse())the value to format (strings must conform to the format expected by the javascript
     * date object's parse() 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;
}
其它类似信息

推荐信息