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


Posted in Javascript onJanuary 14, 2014

我们对 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 相关文章推荐
php对mongodb的扩展(小试牛刀)
Nov 11 Javascript
javascript中全局对象的isNaN()方法使用介绍
Dec 19 Javascript
AngularJS基础学习笔记之表达式
May 10 Javascript
jQuery实现图片轮播效果代码(基于jquery.pack.js插件)
Jun 02 Javascript
详细讲解JavaScript中的this绑定
Oct 10 Javascript
AngularJS自定义服务与fliter的混合使用
Nov 24 Javascript
JS实现的简单图片切换功能示例【测试可用】
Feb 14 Javascript
vue中实现点击按钮滚动到页面对应位置的方法(使用c3平滑属性实现)
Dec 29 Javascript
javscript 数组扁平化的实现
Feb 03 Javascript
js函数柯里化的方法和作用实例分析
Apr 11 Javascript
vue中实现拖动调整左右两侧div的宽度的示例代码
Jul 22 Javascript
Element Card 卡片的具体使用
Jul 26 Javascript
javascript日期对象格式化为字符串的实现方法
Jan 14 #Javascript
JS获取各种浏览器窗口大小的方法
Jan 14 #Javascript
js鼠标滑轮滚动事件绑定的简单实例(兼容主流浏览器)
Jan 14 #Javascript
Eclipse下jQuery文件报错出现错误提示红叉
Jan 13 #Javascript
节点的插入之append()和appendTo()的用法介绍
Jan 13 #Javascript
移动节点的jquery代码
Jan 13 #Javascript
删除节点的jquery代码
Jan 13 #Javascript
You might like
使用PHP模拟HTTP认证
2006/10/09 PHP
ajax+php打造进度条 readyState各状态
2010/03/20 PHP
php中防止伪造跨站请求的小招式
2011/09/02 PHP
解析如何屏蔽php中的phpinfo()函数
2013/06/06 PHP
windows环境下php配置memcache的具体操作步骤
2013/06/09 PHP
PHP中ini_set和ini_get函数的用法小结
2014/02/18 PHP
详解配置 Apache 服务器支持 PHP 文件的解析
2017/02/15 PHP
js静态作用域的功能。
2006/12/25 Javascript
jquery EasyUI的formatter格式化函数代码
2011/01/12 Javascript
jquery 取子节点及当前节点属性值
2014/07/25 Javascript
JavaScript的ExtJS框架中表格的编写教程
2016/05/21 Javascript
JS简单实现无缝滚动效果实例
2016/08/24 Javascript
ionic实现可滑动的tab选项卡切换效果
2020/04/15 Javascript
利用imgareaselect辅助后台实现图片上传裁剪
2017/03/02 Javascript
AngularJS表格添加序号的方法
2017/03/03 Javascript
微信小程序实现获取准确的腾讯定位地址功能示例
2019/03/27 Javascript
2020京东618叠蛋糕js脚本(亲测好用)
2020/06/02 Javascript
js实现自定义滚动条的示例
2020/10/27 Javascript
[03:15]2014DOTA2国际邀请赛 专访国士无双信心满满
2014/07/12 DOTA
[04:50]DOTA2亚洲邀请赛小组赛第四日 TOP10精彩集锦
2015/02/02 DOTA
Python里disconnect UDP套接字的方法
2015/04/23 Python
spyder常用快捷键(分享)
2017/07/19 Python
Sanic框架路由用法实例分析
2018/07/16 Python
对python中的try、except、finally 执行顺序详解
2019/02/18 Python
python自动化工具之pywinauto实例详解
2019/08/26 Python
python爬取代理ip的示例
2020/12/18 Python
python画图时设置分辨率和画布大小的实现(plt.figure())
2021/01/08 Python
python 装饰器的基本使用
2021/01/13 Python
CSS3属性box-sizing使用指南
2014/12/09 HTML / CSS
CSS3实现的炫酷菜单代码分享
2015/03/12 HTML / CSS
Timberland法国官网:购买靴子、鞋子、衣服、夹克和配饰
2019/11/30 全球购物
Blue Nile蓝色尼罗河香港官网:世界最大在线钻石珠宝销售商
2020/05/07 全球购物
快餐店的创业计划书范文
2014/01/29 职场文书
拒绝盗图!教你怎么用python给图片加水印
2021/06/04 Python
MySQL修改默认引擎和字符集详情
2021/09/25 MySQL
Python echarts实现数据可视化实例详解
2022/03/03 Python