QQ浏览器Date相关函数实现不符合ECMA规范

1,890 阅读7分钟

前言

没错,标题起的有点唬人,但吸引你进来了,你不妨了解一下。本文源于用户反馈的在QQ浏览器上组件的日期与星期数无法对应的Bug,测试发现是QQ浏览器未正确按照ECMA标准实现Date相关的函数,本文给出了ECMA规范的定义及实际测试中的结果,分析Bug产生的原因及规避Bug的方法,最后再介绍了处理时间相关业务时需要注意的时区问题, 希望对读者能有所帮助。

Bug描述

用户反馈在QQ浏览器上,组件的日期与时间无法匹配。例如2018年11月14日为周三,组件在QQ浏览器上展示周二。

Bug复现环境

  • 系统macOS 10.14 系统时区 杭州-中国
  • QQ浏览器版本 Mac(4.4.119.400)(64-bit)
  • Chrome浏览器版本 Mac 70.0.3538.77(正式版本) (64 位)

Bug产生的原因

QQ浏览器上

  • Date.prototype.toString()
  • Date.prototype.getDay()
  • Date.prototype.getTimezoneOffset()
  • new Date(Time String)

等函数返回的值未按ECMA标准实现,共同的问题是没有参照系统当前时区计算返回结果。

ECMA标准定义

ECMC规范链接

15.9.5.2 Date.prototype.toString ( )

This function returns a String value. The contents of the String are implementation-dependent, but are intended to represent the Date in the current time zone in a convenient, human-readable form.

该函数返回基于当前时区的时间字符串。

ECMA规范链接

15.9.5.16 Date.prototype.getDay ( )

Let t be this time value.

If t is NaN, return NaN.

Return WeekDay(LocalTime(t)).

该函数返回基于当前时区的星期数。

ECMA规范链接

15.9.5.26 Date.prototype.getTimezoneOffset ( )

Returns the difference between local time and UTC time in minutes.

Let t be this time value.

If t is NaN, return NaN.

Return (t − LocalTime(t)) / msPerMinute.

该函数返回的是当前时区与0时区(格林尼治时间)的差值。

测试结果

//Chrome
new Date(1542124800000).toString() // Wed Nov 14 2018 00:00:00 GMT+0800 (中国标准时间)
new Date(1542124800000).getDay() // 3
new Date(1542124800000).getTimezoneOffset() // -480

//QQ
new Date(1542124800000).toString() // Tue Nov 13 2018 16:00:00 GMT+0000 (UTC)
new Date(1542124800000).getDay()  // 2
new Date(1542124800000).getTimezoneOffset()  // 0
new Date("2018/11/14") //Wed Nov 14 2018 00:00:00 GMT+0000 (UTC)

类似的问题还有

//Chrome 
new Date("2018/11/14") // Wed Nov 14 2018 00:00:00 GMT+0800 (中国标准时间)
new Date("2018/11/14").getTime() // 1542124800000

//QQ
new Date("2018/11/14") //Wed Nov 14 2018 00:00:00 GMT+0000 (UTC)
new Date("2018/11/14").getTime() // 1542153600000

从以上结果来看,QQ浏览器的时区参考都是UTC时间(即0时区时间),是因为获取不到当前系统时区,而默认用了UTC时区为参考吗?

基于以上猜想,我们再测试下Date.prototype.toLocaleString(),我们先来看下规范的定义

ECMA规范链接

15.9.5.5 Date.prototype.toLocaleString ( )

This function returns a String value. The contents of the String are implementation-dependent, but are intended to represent the Date in the current time zone in a convenient, human-readable form that corresponds to the conventions of the host environment’s current locale.

该函数返回基于当前时区的时间字符串,展示形式需要以当前主机的语言环境而定。

可以注意到规范定义的 toString()、toLocaleString()都是以当前时区为基准,仅展现语言有差别。

测试的结果

//Chrome
new Date(1542124800000).toString() // Wed Nov 14 2018 00:00:00 GMT+0800 (中国标准时间)
new Date(1542124800000).toLocaleString() // "2018/11/14 上午12:00:00"
new Date(1542124800000).toLocaleDateString() //"2018/11/14"


//QQ
new Date(1542124800000).toString() // Tue Nov 13 2018 16:00:00 GMT+0000 (UTC)
new Date(1542124800000).toLocaleString() // "2018/11/14 上午12:00:00"
new Date(1542124800000).toLocaleDateString() //"2018/11/14"

测试发现QQ是可以正确展示toLocaleXXX()的结果的,看来是能获取到时区的嘛。

那么基于以上测试,我们的结论是:QQ浏览器是能知道当前的系统时区的,但未正确实现 Date.prototype.toString()、Date.prototype.getDay()、Date.prototype.getTimezoneOffset()、new Date(Time String)等方法。

如何规避可能产生的Bug

以北京时区2018/11/14 上午12点(凌晨)的时间为例, 先展示出bug的代码:

日期的获取使用toLocaleDateString():

new Date(1542124800000).toLocaleDateString() //"2018/11/14"

星期使用了getDay()

"星期" + "日一二三四五六".charAt(new Date(1542124800000).getDay()); 

// chrome中为 星期三
// QQ中为 星期二

在QQ浏览器中,会导致星期数比正确值少一的情况。

解决的方法

知识点铺垫

Date.now()获取的是当前时间与格林尼治时间1970/1/1 0:0:0 的毫秒数之差, 全球所有时区的用户,在同一物理时刻,调用该方法返回的数值是一致的。

假如Date.now()返回的是 1542124800000。这个毫秒数对应北京时间2018/11/14 00:00,对应格林尼治时间为2018/11/13 16:00 在浏览器控制台可以进行验证得到:

new Date(1542124800000) // Wed Nov 14 2018 00:00:00 GMT+0800 (中国标准时间)
new Date(1542124800000).toUTCString() //Tue, 13 Nov 2018 16:00:00 GMT

同一个毫秒数,表示的是同一个物理时刻,体现在不同时区的时间上,会有时差。

解决思路

了解了这个知识点后,我们的解决思路就是统一使用Date.getUTCxxx()方法,该函数返回的以0时区为准的时间度量值。我们只需要在测量的时间上,增加8个小时的时区偏移,即可获取到基于当前时区的时间度量值。

例如获取星期数:

"星期" + "日一二三四五六".charAt(new Date(time + 8 * 3600 * 1000).getUTCDay()); 

可能刚看到这样的计算方式会比较绕,我们可以这么想:北京时区比0时区快8个小时,那么0时区8个小时候后的星期数,就是我们现在的星期数。大家可以停下来想一想。

其他注意事项

系统时区

业务场景: 倒计时

var endTime = new Date('2018/11/11 0:0:0')
console.log(endTime - Date.now())

这个方法可能存在的问题是物理世界的同一时刻,不同时区用户获取到的Date.now()是一致的,但 new Date('2018/11/11 0:0:0')的值会根据用户机器设定的时区而变化,所以会导致同一物理时刻,不同时区的用户看到的倒计时时间不一致。

解决方式是构造函数传入时区值

var endTime = new Date('2018/11/11 0:0:0 GMT+0800')
console.log(endTime - Date.now())

或者 按照上述的UTC转换法

var endTime = new Date(Date.UTC(2018,10,11,0,0,0)) - 8*3600*1000
console.log(endTime - Date.now())

这样不同时区的用户,看到的倒计时是一致的,能在同一时间参与秒杀。

结语

业务场景中与时间相关的处理,都需要额外打一个心眼,考虑时区等因素。本文分析了QQ浏览器上的实现Bug,并给出了基于UTC时间转化的方式,保证了不同时区用户的一致性,希望能给大家带来参考价值。