由浅入深,走进中级工程师都未必知道的 JavaScript 时间处理冷知识

361次阅读  |  发布于3年以前

1 背景与基本概念

在过去,世界各地都各自订定当地时间,例如我国古代将一昼夜分为十二时辰,每一时辰相当于现代的两个小时。但随着交通和通信的发达,各地交流日益频繁,不同的地方时间给人们造成了许多困扰。于是在1884年的国际经度会议上制定了全球性的标准时,确定以英国伦敦格林威治区这个地方为零度经线的起点(本初子午线),并以地球由西向东每24小时自转一周360°,规定经度每隔15°,时差1小时,而每15°的经线则称为该时区的中央经线。全球被划分为24个时区,其中包含23个整时区及180°经线左右两侧的2个半时区。东经的时间比西经要早,也就是如果格林威治时间是中午12时,则中央经线15°E的时区为下午1时,中央经线30°E时区的时间为下午2时;反之,中央经线15°W的时区时间为上午11时,中央经线30°W时区的时间为上午10时。如果两人同时从格林威治的0°各往东、西方前进,当他们在经线180°时,就会相差24小时,所以经线180°被定为国际换日线,由西向东通过此线时日期要减去一日,反之,若由东向西则增加一日。

1.1 标准时间

1.2 时区

2 计算机中的时间表示

以前的Unix操作系统中存储时间,是以32位有符号数来存储的。用32位来表示时间的最大间隔是68年,而最早出现的UNIX操作系统考虑到计算机产生的年代和应用的时限综合取了1970年1月1日0时0分0秒作为UNIX TIME的纪元时间(开始时间),将1970年作为中间点,向左向右偏移都可以照顾到更早或者更后的时间,因此将1970年1月1日0点作为计算机表示时间的原点,从1970年1月1日开始经过的秒数存储为一个32位整数。以后计算时间就把这个时间(1970年1月1日00:00:00)当做时间的零点。这种高效简洁的时间表示法,就被称为"Unix时间纪元"。

2.1 时间戳

2.2 2038年问题

Unix时间戳是从1970年1月1日(UTC/GMT的午夜)开始所经过的秒数,不考虑闰秒。在32位系统上,time_t能表示的最大值为0x7ffffffff,当time_t取最大值时表示系统时间为2038-01-19 03:14:07,但时间再往后走时,那time_t会溢出变成一个负值,此时系统时间会倒流回到1901年,届时操作系统和上层软件都会运行出错。

解决这个问题最简单粗暴的方法是用64位来表示时间。64位表示时间的最大值是2900亿年后的292,277,026,596年12月4日15:30:08,星期日(UTC)(北京时间292,277,026,596年12月4日23:30:08)。实际上,大部份64位操作系统已经把time_t改为64位整型,对于这些机器来说,2038年问题不复存在。然而对于嵌入式设备来说,现在还有大量32位系统在全球各地运行,谁也无法保证这些系统在2038年之前就能光荣退役。另外对于64位操作系统,上面还会运行着32位的应用程序,依旧会发生2038年问题。

2.3 两种国际时间标准:ISO8601 与 RFC2822

ISO8601,全称为《数据存储和交换形式·信息交换·日期和时间的表示方法》,规定了国际标准日期与时间表示法。

RFC2822:用于在 HTTP 和电子邮件标题等位置统一表示日期和时间的互联网信息格式。RFC 2822 包括星期几(短)、数字日期、月份的三字母缩写、年、时间和时区,显示为 Wed 01 Jun 2016 14:31:46 -0700

3 前端中的时间表示

后端一般返回的是时间的秒数或毫秒数,而在前端页面中的显示可能就多种多样,可能是:

在javascipt中,时间的处理需要用到内置对象Date

  1. 构造函数 var now = new Date(); 即可获取以当前时间构造的Date对象。以下方式都可以构造Date对象
new Date("month dd,yyyy hh:mm:ss");   new Date("January 1,2020 22:10:35"); 
new Date("month dd,yyyy");            new Date("July 12,2013"); 
new Date(yyyy,mth,dd,hh,mm,ss);       new Date(2006,0,12,22,19,35); 
new Date(yyyy,mth,dd);                new Date(2008,3,27); 
new Date(ms);                         new Date(1234567890000);

2 . Date.parse()var someDate = new Date(Date.parse('May 25,2004')); 解析字符串,转为时间戳(毫秒) 如果传入Data.parse()的方法的字符串不能表示日期格式,会返回NaN。实际上,如果直接将表示日期的字符串传递给Date构造函数,也会在后台调用Date.parse()方法。 3 . Date.now()获取当前时间戳 可以用Date.now()统计程序运行的时间

//取得开始时间
var start = Date.now();
//调用函数
dosomething();
//取得结束时间
var stop = Date.now(),

4 . 与其它引用类型一样,Date类型也重写了toLocaleString()、toString()和valueOf()方法。valueOf()方法返回的不是字符串,而是返回日期的毫秒时间戳。因此可以方便使用比较操作符(大于或小于)来比较日期值。

let date=new Date()
date.toString()           "Tue Jan 26 2021 18:24:40 GMT+0800 (中国标准时间)"
date.toDateString()       "Tue Jan 26 2021"
date.toGMTString()        "Tue, 26 Jan 2021 10:24:40 GMT"
date.toUTCString()        "Tue, 26 Jan 2021 10:24:40 GMT"
date.toISOString()        "2021-01-26T10:24:40.224Z"
date.toJSON()             "2021-01-26T10:24:40.224Z"
date.toLocaleString()     "2021/1/26 下午6:24:40"
date.toLocaleTimeString() "下午6:24:40"
date.toLocaleDateString() "2021/1/26"

5 . get和set

let date=new Date();
date.getFullYear() - 获取4位数年份
date.getMonth() - 获取月份,取值0~11,0对应1月份
date.getDay() - 获取星期,取值0~6,0对应星期天,1对应星期一,6对应星期六
date.getDate() - 获取一个月中的某天,取值1~31。1即1号,31即31号
date.getHours() - 获取小时数,取值0~23
date.getMinutes() - 获取分钟数,取值0~59
date.getSeconds() - 获取秒数,取值0~59
date.getMilliseconds() - 获取毫秒数,取值0~999
date.getTime() - 返回1970年1月1日至当前时间的毫秒数

Date对象还有对应的UTC方法, 包括getUTC和setUTC
> new Date().getHours()
21
> new Date().getUTCHours()
13

3.1 常见时间处理场景

JS判断某年某月有多少天

JavaScript里面的new Date("xxxx/xx/xx")这个日期的构造方法当传入的是"xxxx/xx/0"(0号)的话,得到的日期是"xx"月的前一个月的最后一天("xx"月的最大取值是69),如果传入2019/12/0"(注意month是从0开始的),会得到"2018/12/31"。而且最大的好处是当传入"xxxx/3/0",会得到xxxx年2月的最后一天,它会自动判断当年是否是闰年来返回28或29,不用自己判断。所以,我们想知道某年某月有多少天的话,只需要在构造Date函数时月份传下个月,日期传0,这样就可以得到当月最后一天的Date对象

function getDaysInMonth(year,month){
  let temp = new Date(year,month,0);
  return temp.getDate();
}
getDaysInMonth(2019,2) //28 
getDaysInMonth(2020,2) //29

JS生成倒数7天日期

比如今天是10月1号,生成的数组是["9月25号","9月26号","9月27号","9月28号","9月29号","9月30号","10月1号"]。这个难点就是需要判断这个月(或上个月份)是30天还是31天,而且还有可能遇到闰2月的29天的情况

let now = new Date(); 
let s = '';
let i = 0;
while (i < 7) {
    s += now.getFullYear() + '/' + (now.getMonth() + 1) + '/' + now.getDate() + '\n';
    now = new Date(now - 24 * 60 * 60 * 1000); 
    i++;
}
console.log(s);

JS format函数

// 对Date的扩展,将 Date 转化为指定格式的String
// 月(M)、日(d)、小时(h)、分(m)、秒(s)、季度(q) 可以用 1-2 个占位符, 
// 年(y)可以用 1-4 个占位符,毫秒(S)只能用 1 个占位符(是 1-3 位的数字) 
// 例子: 
// (new Date()).Format("YYYY-MM-DD HH:mm:ss.S") ==> 2006-07-02 08:09:04.423 
// (new Date()).Format("YYYY-M-D H:m:s.S")      ==> 2006-7-2 8:9:4.18 
Date.prototype.Format = function (fmt) {
    const o = {
        "M+": this.getMonth() + 1, //月份 
        "D+": this.getDate(), //日 
        "H+": this.getHours(), //小时 
        "m+": this.getMinutes(), //分 
        "s+": this.getSeconds(), //秒 
        "q+": Math.floor((this.getMonth() + 3) / 3), //季度 
        "S": this.getMilliseconds() //毫秒 
    };
    if (/(Y+)/.test(fmt)){
        fmt = fmt.replace(RegExp.$1, (this.getFullYear() + "").substr(4 - RegExp.$1.length));
    }
    for (const k in o){
         if (new RegExp("(" + k + ")").test(fmt)){
             fmt = fmt.replace(RegExp.$1, (RegExp.$1.length == 1) ? (o[k]) : (("00" + o[k]).substr(("" + o[k]).length)));    
         }
    }
    return fmt;
}
let date=new Date();
date.Format("YYYY-MM-DD HH:mm:ss");

3.2 实用的时间处理库--moment, dayjs,miment

这三个都是非常好用的JS时间处理库,且三个库都极易上手,连API使用方式都高度一致,后两者都借鉴了moment。在日常时间处理上dayjs和miment基本可以替代moment。

moment dayjs miment
Github stars 45k 33k 332
大小 200kb 2kb 1kb
可变性
支持扩展
方法数量
是否维护

下面这一段是moment的官方声明:

“ Moment.js 宣布停止开发,进入维护状态。这是一个大而全的时间日期库,极大方便了我们在 JavaScript 中计算时间和日期,每周下载量超过 1200 万,已成功用于数百万个项目中。但是,作为一个诞生于 2011 年的元老级明星项目,以现在的眼光来看 Moment.js 并非完美无缺,官方总结了两大问题:

3.2.1 可变对象

Moment 对象是可变对象(mutable),简单点说,任何时间上的加减,包括startOf()等操作都改变了其本身。这种设计让代码变得十分不可控,而且很容易带来各种隐蔽且难以调试的 bug。以至于我们在每步修改之前,都要先调用 .clone() 克隆一次才能放心操作。

3.2.2 包体积过大

因为 Momnet.js 将全部的功能和所有支持的语言都打到一个包里,包的大小也是到了 280.9 kB 这样一个夸张的数字,而且对于 Tree shaking 无效。如果要使用时区相关的功能,包体积更是有 467.6 kB 的大小。简单点说,我们可能只需要一个 .format 格式化时间的方法,用户就需要加载数百 kB 的库,这是十分不划算的。” 官方给了 3 种替代方案:

  1. 不使用库

对于一些简单的时间处理需求,其实 JavaScript 自带的 Date 和 Intl 对象完全可以满足。强大的 Intl 对象可以展示不同时区不同语言的时间日期格式,在多数现代浏览器上已经有很好的支持。

2 . Temporal

也许今后的某一天,我们再也不需要使用任何库。Temporal被看作是未来的全新内置的时间日期方案 Temporal 很值得期待。ECMA TC39临时提案正在努力为JavaScript语言编写更好的日期和时间API。它目前处于TC39流程的第二阶段。这是一个 JS 语言内置的重新设计的时间和日期 API,现在可以通过实验性的 polyfill 来尝试 Temporal,但离生产上大规模可用还有很长的路要走。

3 . 其他替代库

3.2.3 以dayjs为例(2KB immutable date time library alternative to Moment.js with the same modern API)

API 分为3类

  1. 第一类是返回其他对象的,比如format(),返回的是字符串。json()返回的是一个json对象

dayjs().format('YYYY年MM月DD日 hh:mm:ss')  // 2021-01-26 20:49:36
dayjs().format('YYYY/MM/DD hh-mm-ss SSS') // 2021/01/26 20-49-36 568
dayjs().format('YYYY年MM月DD日 星期WW')     // 2021年01月26日 星期二
dayjs().format('YYYY年MM月DD日 星期ww')     // 2021年01月26日 星期2
也可以只传一部分
dayjs().format('YYYY')   // 2021
dayjs().format('MM')     // 01
dayjs().format('DD')     // 26
var date=dayjs().json()
{
    "year": 2021,
    "month": 1,
    "date": 26,
    "hour": 19,
    "minute": 42,
    "second": 41,
    "day": 2,
    "milliSecond": 87
}

2 . 第二类是返回dayjs对象的,可以在调完一个api后面继续调用另一个api,也就是链式调用

来看一个真实需求:

//链式调用
dayjs()
  .startOf('month')
  .add(1, 'day')
  .subtract(1, 'year')

3 . 第三类是从Date对象继承的,也就是说Date对象有的方法,dayjs也同样有。由于是继承而来的方法,所以方法无法返回dayjs对象,无法链式调用。(不推荐使用)

3.2.4 dayjs部分源码解析

// d 是否为 Dayjs 的实例对象
var isDayjs = d => d instanceof Dayjs
var wrapper = (date, instance) => dayjs(date, { locale: instance.$L })
var parseDate = function parseDate(cfg) {
  var date = cfg.date,
      utc = cfg.utc;
  if (date === null) return new Date(NaN); // null is invalid
  if (Utils.u(date)) return new Date(); // today
  if (date instanceof Date) return new Date(date);
  if (typeof date === 'string' && !/Z$/i.test(date)) {
    var d = date.match(C.REGEX_PARSE);
    if (d) {
      var m = d[2] - 1 || 0;
      var ms = (d[7] || '0').substring(0, 3);
      if (utc) {
        return new Date(Date.UTC(d[1], m, d[3] || 1, d[4] || 0, d[5] || 0, d[6] || 0, ms));
      }
      return new Date(d[1], m, d[3] || 1, d[4] || 0, d[5] || 0, d[6] || 0, ms);
    }
  }
  return new Date(date); // everything else
};
// dayjs 函数,用于返回新的 Dayjs 实例对象的函数(工厂模式)
var dayjs = (date, c) => {
  // 若date 为 Dayjs 的实例对象,则返回克隆的 Dayjs 实例对象(immutable)
  if (isDayjs(date)) {
    return date.clone()
  }
  const cfg = c || {}
  cfg.date = date
  return new Dayjs(cfg)
}
// Dayjs构造函数
var Dayjs = /*#__PURE__*/function () {
     function Dayjs(cfg) {
        this.$L = parseLocale(cfg.locale, null, true);//解析本地语言
        this.parse(cfg); //核心
     }
     var _proto = Dayjs.prototype;
    _proto.parse = function parse(cfg) {
        this.$d = parseDate(cfg);
        this.init();
    };
    _proto.init = function init() {
        var $d = this.$d;
        this.$y = $d.getFullYear();
        this.$M = $d.getMonth();
        this.$D = $d.getDate();
        this.$W = $d.getDay();
        this.$H = $d.getHours();
        this.$m = $d.getMinutes();
        this.$s = $d.getSeconds();
        this.$ms = $d.getMilliseconds();
    }; // eslint-disable-next-line class-methods-use-this
    clone() {
        return wrapper(this.toDate(), this)
    }
    // 转换为新的原生的 JavaScript Date 对象
    toDate() {
        return new Date(this.$d)
    }
        ......
     return Dayjs;
}();

参数 c 其实是当 date 参数为 Dayjs 实例对象时,最后又会调用 dayjs() 函数,此时才会传入参数 c。参数 c 为一个包含 locale 属性的对象(locale 的值为上一个 Dayjs 实例对象所用的语言,是一个字符串类型)

startOf(units, startOf) { // startOf -> endOf
    const isStartOf = !Utils.isUndefined(startOf) ? startOf : true
    const unit = Utils.prettyUnit(units)
    const instanceFactory = (d, m) => {
      const ins = wrapper(new Date(this.$y, m, d), this)
      return isStartOf ? ins : ins.endOf(C.D)
    }
    const instanceFactorySet = (method, slice) => {
      const argumentStart = [0, 0, 0, 0]
      const argumentEnd = [23, 59, 59, 999]
      return wrapper(this.toDate()[method].apply( // eslint-disable-line prefer-spread
        this.toDate(),
        isStartOf ? argumentStart.slice(slice) : argumentEnd.slice(slice)
      ), this)
    }
    switch (unit) {
      case C.Y:
        return isStartOf ? instanceFactory(1, 0) :
          instanceFactory(31, 11)
      case C.M:
        return isStartOf ? instanceFactory(1, this.$M) :
          instanceFactory(0, this.$M + 1)
      case C.W:
        return isStartOf ? instanceFactory(this.$D - this.$W, this.$M) :
          instanceFactory(this.$D + (6 - this.$W), this.$M)
      case C.D:
      case C.DATE:
        return instanceFactorySet('setHours', 0)
      case C.H:
        return instanceFactorySet('setMinutes', 1)
      case C.MIN:
        return instanceFactorySet('setSeconds', 2)
      case C.S:
        return instanceFactorySet('setMilliseconds', 3)
      default:
        return this.clone()
    }
  }

Copyright© 2013-2020

All Rights Reserved 京ICP备2023019179号-8