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 标准时间
IAT(International Atomic Time),即原子时。在国际计量体系中,时间是七个基本量之一,以天文学为检测基础的格林威治时间,肯定没法满足科学精度的须要。于是,人类发明了原子钟,也就是借助原子内部电子在两个基态间跳跃时幅射下来的电磁波频度作为标准,来规定1秒的时长。GMT与IAT每年会有约0.9s的偏差,主要是由月球不规则自转以及潮汐效应引发。
UTC(Universal Time Coordinated),即协调世界时,是世界时的一个版本,用于修正GMT
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 与 RFC2822ISO8601,全称为《数据储存和交换方式·信息交换·日期和时间的表示方式》,规定了国际标准日期与时间表示法。RFC2822:用于在 HTTP 和电子邮件标题等位置统一表示日期和时间的互联网信息格式。RFC 2822 包括星期几(短)、数字日期、月份的三字母简写、年、时间和时区,显示为 Wed 01 Jun 2016 14:31:46 -07003 前端中的时间表示
后端通常返回的是时间的秒数或微秒数,而在后端页面中的显示可能就多种多样,可能是:
在javascipt中,时间的处理须要用到外置对象Date
构造函数 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);
Date.parse()var someDate = new Date(Date.parse('May 25,2004')); 解析字符串,转为时间戳(毫秒)如果传入Data.parse()的方式的字符串不能表示日期格式,会返回NaN。实际上,如果直接将表示日期的字符串传递给Date构造函数,也会在后台调用Date.parse()方法。
Date.now()获取当前时间戳可以用Date.now()统计程序运行的时间
//取得开始时间
var start = Date.now();
//调用函数
dosomething();
//取得结束时间
var stop = Date.now(),
与其它引用类型一样,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"
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。
momentdayjsmiment
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 种代替方案:
不使用库
对于一些简单的时间处理需求,其实 JavaScript 自带的 Date 和 Intl 对象完全可以满足。强大的 Intl 对象可以展示不同时区不同语言的时间日期格式,在多数现代浏览器上早已有挺好的支持。
Temporal
也许今后的某三天,我们再也不需要使用任何库。Temporal被看作是未来的全新外置的时间日期方案 Temporal 很值得期盼。ECMA TC39临时议案正在努力为JavaScript语言编撰更好的日期和时间API。它目前处于TC39流程的第二阶段。这是一个 JS 语言外置的重新设计的时间和日期 API,现在可以通过实验性的 polyfill 来尝试 Temporal,但离生产上大规模可用还有很长的路要走。
其他代替库
3.2.3 以dayjs为例(2KB immutable date time library alternative to Moment.js with the same modern API)
API 分为3类
第一类是返回其他对象的,比如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
}
第二类是返回dayjs对象的,可以在调完一个api旁边继续调用另一个api,也就是链式调用
来看一个真实需求:
//链式调用
dayjs()
.startOf('month')
.add(1, 'day')
.subtract(1, 'year')
第三类是从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()
}
}
谢谢支持
喜欢的话别忘了分享、点赞、在看三连哦~。
点击下方名片,关注ELab团队,获取一手的大厂技术文章。