相信只要对前端有所了解的同学,对window对象的Date
、Math
和JSON
这三个原生对象(或构造函数)都不会陌生,不过可能并不是所有同学都全面学习过这三个原生对象。本文就将帮大家梳理一下关于这三个原生对象的主要知识点。
首先我们先来简述这三个原生对象的作用。
window.Date
是浏览器提供的日期接口对象,主要用于访问日期相关的函数或属性。比如我们想获取当前日期,可以像下面一样:
> let date = new Date();
> date
< Tue Aug 04 2020 22:11:28 GMT+0800 (中国标准时间)
这样就得到了当前时间。
调用日期对象的原型方法,还可以单独获取年月日时分秒
等信息,另外它还包含对时区的支持。基本上与时间相关的操作都可以借助window.Date完成。
window.Math
提供了对常见的数学运算,以及一些数学常量的支持。下面是一些常用的函数或常量:
Math.abs(); // 求绝对值
Math.sin(); // 求正弦值
Math.floor(); // 数值的向下取整
Math.pow(); // 幂运算
Math.PI; // 圆周率
Math.E; // 自然常量
Math.SQRT2; // 2的平方根
.....
还有非常多的函数和常量,我们后续会一一列举。
window.JSON
是浏览器提供的操作JSON对象的接口。这个对象最重要的两个方法就是JSON.parse
和JSON.stringify
,前者可以将一个JSON字符串解析为一个JavaScript对象,而后者可以将一个JavaScript对象字符串化(即转为JSON字符串)。两者是互逆的操作。
Date是浏览器为开发者提供的访问日期的构造函数,该对象基于Unix Time Stamp,即自1970年1月1日起所经过的毫秒数,因此Date对象理论上最多可以精确到毫秒(实际上一般只能精确到秒,参考自MDN文档)。
构造一个Date对象需要使用new关键字,如new Date()
。在中国,构造出的时间默认为北京时间(但是当用getTime()获取Unix时间戳时,是以格林尼治标准时间为基准)。它接受的参数可以有四种方式:
> new Date(); // 2020年8月5日
< Wed Aug 05 2020 21:18:28 GMT+0800 (中国标准时间)
1970年1月1日0时
(格林尼治标准时间)起经过的毫秒数,是一个整数,如本文的写作时的时间戳为1596633508424
。> new Date(1596633508424);
< Wed Aug 05 2020 21:18:28 GMT+0800 (中国标准时间)
new Date('2020-8-5')
。这种方式对字符串的格式要求不是很严格,如new Date('2020-08-05 12:00')
也可以被正确解析。该类字符串的规范请参考IETF-compliant RFC 2822 timestamps,不过一般直接打开浏览器控制台,测试一下是否能解析即可。> new Date('2020-8-5'); // 2020年8月5日
< Wed Aug 05 2020 00:00:00 GMT+0800 (中国标准时间)
> new Date('2020-08-05 12:00'); // 2020年8月5日12点
< Wed Aug 05 2020 12:00:00 GMT+0800 (中国标准时间)
> new Date('Wed Aug 05 2020 12:00:00 GMT+0800 (中国标准时间)');
< Wed Aug 05 2020 12:00:00 GMT+0800 (中国标准时间)
new Date(year, monthIndex [, day [, hours [, minutes [, seconds [, milliseconds]]]]])
。这种格式的构造函数要求至少需要传入年份和月份(这里月份是从0开始计算的,因此传入7时表示8月份),其他未传的参数自动填充为最小值(日期最小为1,其他最小为0)。当只传入年份时,得到的时间会是1970年1月1日。> new Date(2020); // 只传年份返回的结果是1970年1月1日
< Thu Jan 01 1970 08:00:02 GMT+0800 (中国标准时间)
> new Date(2020, 7); // 2020-8-1 00:00:00
< Tue Aug 01 2020 00:00:00 GMT+0800 (中国标准时间)
> new Date(2020, 7, 5, 21); // 2020-8-5 21:00:00
< Wed Aug 05 2020 21:00:00 GMT+0800 (中国标准时间)
再次提醒,Date的月份是从0
开始计算的,因此第四种传参方式中的月份传入整数7
时,实际上代表的是8月份
。同样的,调用date.getMonth()
获取月份时返回的值也是7
。传入字符串格式的月份时没有该问题(如第三种传参方式)。
除了上面四种格式,Date也可以不使用new关键字(不建议这样做),此时返回的是字符串格式的时间,而不是Date对象。
> Date()
< "Wed Aug 05 2020 21:38:01 GMT+0800 (中国标准时间)"
如果第四种方式所传入的参数超过正确范围,则相邻的参数可能会自动调整,如:
> new Date(2019, 13, 1); // 调整为2020年2月1日
< Sat Feb 01 2020 00:00:00 GMT+0800 (中国标准时间)
月份的最大值为11,即12月。当传入13时,年份自动加一,月份变为2月份。
这些属性和方法属于Date构造函数本身,直接通过Date调用。
(new Date()).getTime()
。Date.parse(timeString)
与new Date(timeString)
的值一致。由于浏览器的差异,这种方式不完全可靠。以下示例均以let date = new Date()
语句构造出的date实例为基准,转换为常规时间为2020-8-5 22:10:00
。
> let date = new Date();
< Wed Aug 05 2020 22:10:00 GMT+0800 (中国标准时间)
< // 2020-8-5 22:10:00
> Date.prototype.constructor
< ƒ Date() { [native code] }
以下方法用于从date实例中提取一部分参数,如年份、月份等,仍以2020-08-05 22:10:00
为例。
date.getDate()
值为5,即日期为5号。date.getDay()
返回3,即周三。date.getFullYear()
返回2020。date.getHours()
返回22,值区间为0~23。date.getMinutes()
返回10。date.getMonth()
返回7,如需转为实际月份,请手动加一。date.getSeconds()
返回0。date.getTimezoonOffset()
返回-480。getFullYear()
代替。返回非标准的年份,一般只返回年份的后2~3位。date.setDate(6)
可将date实例的日期设为6号,以下方法同理。date.setUTCHours(0)
时,实际设置的是北京时间8点钟(格林尼治的0点正好是北京时间8点),也就是说再调用date.getHours()
会返回8。setFullYear()
。这类方法用于转换日期格式。
> let date = new Date();
< Wed Aug 05 2020 22:10:00 GMT+0800 (中国标准时间)
> date.toDateString();
> "Wed Aug 05 2020" // 2020年8月5号,周三
> date.toISOString()
< "2020-08-05T14:10:00.000Z"
toISOString
方法,返回上述的值。该方法是为了提供给JSON.stringify()
将日期对象字符串化使用。toUTCString()
。> date.toLocaleDateString();
< "2020/8/5"
"2020/8/5 下午10:10:00"
Object.protorype.toSource
,目前版本的Chrome仍不可用。> date.toString();
< "Wed Aug 05 2020 22:10:00 GMT+0800 (中国标准时间)"
> date.toTimeString();
< "22:10:00 GMT+0800 (中国标准时间)"
> date.toUTCString(); // 22时转为了14时
< "Wed, 05 Aug 2020 14:10:00 GMT"
> date.valueOf();
< 1596636600000
以上就是Date相关的主要知识点,最后分享一个简单的日期的格式化函数:
function formatDate (date, formatter) {
let normalDate = new Date(date); // 尝试转为日期对象
if(normalDate !== 'Invalid Date'){
let year = normalDate.getFullYear().toString();
let month = normalDate.getMonth() + 1;
month = month > 9 ? month.toString() : '0' + month.toString();
let day = normalDate.getDate();
day = day > 9 ? day.toString() : '0' + day.toString();
let hours = normalDate.getHours();
hours = hours > 9 ? hours.toString() : '0' + hours.toString();
let minutes = normalDate.getMinutes();
minutes = minutes > 9 ? minutes.toString() : '0' + minutes.toString();
let seconds = normalDate.getSeconds();
seconds = seconds > 9 ? seconds.toString() : '0' + seconds.toString();
return formatter.replace('yyyy', year).replace('MM', month).replace('dd', day).replace('hh', hours).replace('mm', minutes).replace('ss',seconds);
} else {
throw new Error('Invalid date input:' + date);
}
}
> formatDate(new Date, 'yyyy-MM-dd hh:mm:ss');
< "2020-08-05 22:10:00"
window.Math
封装了与数学运算相关的一些常量和函数,用于进行一些简单的数学计算。Math本身并不是函数,因此无法用来调用或构造实例。它内置的所有属性和方法都是直接通过Math来调用的。
e
,在数学上称为欧拉常量,值约为2.718
。ln2
,即以常量e
为底数的2的对数,值约为0.693
。10
的自然对数ln10
,值约为2.303
。10
为底数的e
的对数,值约为0.434
。3.14159
。0.707
。1.414
。以上内容摘自MDN文档,只需大致阅读一遍,记住几个常用的函数,其余的用到时查阅文档即可。
这里我们着重强调几个用于取近似值的函数:Math.ceil()
、Math.floor()
、Math.round()
和Math.trunc()
。这四个函数都是用于数值取整的,但是有一些差异。
Math.ceil()
,向上取整(ceil
的中文释义为天花板
),即获取大于或等于当前值的最小整数,如:Math.ceil(1.2) === 2;
Math.ceil(3.9) === 4;
Math.ceil(1.0) === 1;
Math.ceil(-1.2) === -1; // -1 > -1.2
Math.floor()
,向下取整(floor
的中文释义为地板
),即获取小于或等于当前值的最大整数。如:Math.floor(1.2) === 1;
Math.floor(3.9) === 3;
Math.ceil(1) === 1;
Math.ceil(-1.2) === -2; // -2 < -1.2
Math.round()
,以四舍五入法取整。如:Math.round(1.2) === 1;
Math.round(3.5) === 4;
Math.round(-1.2) === -1;
Math.trunc()
,直接截掉数值的小数位,保留整数位。对于正数来讲,它相当于向下取整;而对于负数来说,则相当于向上取整。如:Math.trunc(1.2) === 1;
Math.trunc(-1.2) === -1;
Math没有提供对小数部分的近似方法,比如我们想四舍五入保留圆周率的五位小数,需要先将其乘以100000
,使用Math.round()
四舍五入后再除以100000
,如:
Math.round(Math.PI * 100000) / 100000;
// 3.14159
// 以下代码则可以从百位四舍五入
Math.round(1233 / 100) * 100;
// 1200
向上、向下取整也是类似的。
Math对象还有一个极其常用的函数:Math.random()
,我们也着重介绍一下。
这个函数的调用非常简单,不用传任何参数,直接调用即可产生一个0~1
(含0,但不含1)之间的伪随机数。注意,该函数生成的随机数不是绝对随机的,因此不要用于安全方面,如果需要生成更安全的随机数,请使用window.crypto.getRandomValues()
。
该函数虽然只能生成0~1之间的数值,但经过公式转换,也可以生成一组整数。生成一个min
~max
(含min,但不包含max)之间的整数的公式为:
Math.floor(Math.random() * (max - min) + min);
// 生成5~10(包含5,不包含10)之间的整数
Math.floor(Math.random()*5 + 5); // 5,6,7,8,9随机
// 生成5~10(包含5,不包含10)之间的数字
Math.random()*5 + 5;
去掉Math.floor()
产生的就是5~10(包含5,不包含10)之间的随机数。这里不能用Math.round()或Math.ceil()替换Math.floor(),当使用Math.round(),各个整数的概率分布是不均匀的;而使用Math.ceil()时,出现最小值的概率无限趋近于0,这两种情况通常都不是想要的。
如果想包含上限值,将max-min
改为max-min+1
即可。即:
Math.floor(Math.random() * (max - min + 1) + min);
// 生成5~10(包含5和10)之间的整数
Math.floor(Math.random()*6 + 5); // 5,6,7,8,9,10随机
// 生成介于min和max的随机整数,包含上限和下限
function getRandomIntInclusive(min, max) {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min + 1)) + min; // 含最大值,含最小值
}
JSON(JavaScript Object Notation,JavaScript对象简谱,可理解为JavaScript对象的简洁表示法)是符合ECMAScript规范的一个js子集,是一种轻量级的数据交换格式。目前它已经基本替代了xml(xml目前主要作为配置文件使用)格式,成为了事实的数据传输标准。
从某种意义上来说,JSON数据可以看作是一个满足特定格式的字符串。以下是一个JSON数据的例子:
{
"name": "Carter",
"age": 25
}
它可以大致看成这种结构:'{"name": "Carter","age": 25}'
。也就是说,JSON以某种规则把js对象变成了字符串的形式,从而方便在网络中传输。
JSON仅支持6个构造字符:
[
]
{
}
:
,
它们的作用与js对象基本是一致的。各个字符之间允许有任意多的空格或换行符。
JSON支持的数据类型包括对象
、数组
、数字
、字符串
,以及三个特殊的字面值:false
,true
和null
。js中其他类型的数据必须转为这几类数据,才可以变成JSON数据用于网络传输。
window.JSON
本身并不是普通函数,也不是构造函数,因此不能被调用或用于构造JSON实例。
window.JSON
仅对外提供了两个实例方法:JSON.parse()
和JSON.stringify()
,前者将一个JSON数据解析为JavaScript对象,后者将一个JavaScript对象处理为JSON数据,两者是一个互逆的过程。如:
> JSON.parse('{"name": "carter"}');
< { name: "Carter" }
> JSON.stringify({ name: "Carter" });
< "{"name":"carter"}"
从上述JSON.parse
的用法可以看出,它可以直接用于将符合JSON标准的字符串转化为js对象,这是非常常见的用法。
而从JSON.stringify
的结果则可以看出,JSON数据并不能算是真正的字符串(如果是真正的字符串,两侧应该是单引号)。这个问题在某些情况下可能会造成数据传输错误,比如当后端使用JSON包强行将JSON数据处理成字符串发送给前端时,前端很可能无法正确解析(此时前端得到的是类似"{"name":"carter"}"
这样的伪字符串,它无法被还原成js对象)。
JSON的这两个互逆的方法可以非常便捷地实现对象的深度克隆,这在实际开发中非常实用和简便。如:
function clone (obj) {
return JSON.parse(JSON.stringify(obj));
}
let a = {name: 'carter', data: {id: 123}}
let b = clone(a); // b是由a克隆而来,两者有不同的内存地址
b !== a;
该方法的问题主要在于,它无法克隆symbol
类的属性,也无法克隆函数类属性(当然还包括原型对象上的属性和方法,不过一般克隆对象时也不会克隆原型)。另外,当对象较为复杂时,它在性能上存在一定的问题。
JSON.parse
其实还支持第二个参数,即reviver
函数,用于在返回解析结果前执行一次处理。比如我们希望在解析之后输出所有的key,并且将所有的属性值转化为字符串,可以这样写:
JSON.parse('{"name": "carter", "age": 25}', (key, value) => {
console.log(key);
if (!key) return value;
return String(value);
})
> name
> age
> // 这里实际输出了一个空字符串
> {name: "carter", age: "25"}
注意,这里虽然只有两个key,但是reviver
函数却执行了三次。多执行的那次传入的key
为空字符串,而value
是最终的解析结果(即我们最终得到的js对象)。所以我们专门加了一个判断,当key为空时不作任何处理,直接返回对应的value。
对应的, JSON.stringify()
最多可以传三个参数,即JSON.stringify(value [, replacer [, space]])
。
第一个参数value
是要转化为JSON的js变量,可能是对象、数组、数字、字符串、布尔值或null。
第二个参数是一个替换器,可以是函数或数组。当是函数时,它的返回值将替换该属性的原始值,如果它返回undefined,则该属性不会被字符串化到最终结果中:
JSON.stringify({name: 'carter', age: 25}, (k, v) => {
if (!k) return v;
return k === 'age' ? v : undefined
})
> "{"age": 25}"
与parse一样,这个replacer也被执行了三次,最后一次key为空字符串,value为字符串化的结果,所以我们先写一个判断条件过滤掉这次调用。之后我们检查key是否为age
,如果是则返回对应的value,否则就返回undefined,这就使得只有age
属性会被字符串化到最终结果中。当然, k !== 'age' ? v : undefined
这样写就可以去掉age
属性,而把其他属性字符串化。
当这个替换器是一个数组时,它就是一个白名单,只有被它罗列的属性会被字符串化:
JSON.stringify({name: 'carter', age: 25}, ['age']);
> "{"age": 25}"
需要注意的是,symbol
类型的属性永远不会被字符串化,这类属性无论是否存在于白名单中,最终都会被丢弃。
第三个参数是字符串化后每个属性前要插入的空白字符数或要插入的字符,默认不插入任何字符。它可以是数字或字符串,当是数字时,会尝试在每个属性前插入该数量的空格(空格数量介于1~10之间,当传入的值小于1,会强制转为1,当大于10,会强制置为10),如:
> JSON.stringify({a: 'foo'}, ['a'], 3);
> "{
"a": "foo"
}"
当它是字符串时,会把它拼到属性前面,如果字符串长度大于10,则截取前10个字符:
> JSON.stringify({a: 'foo'}, ['a'], 'abc');
< "{
abc"a": "foo"
}"
经过测试,在不传第二个参数的情况下,直接传字符串或数字是无效的(此时它可能被当成了无效的replacer
,而不是space
),此时可以将第二个参数传为null来解决:
> JSON.stringify({a: 'foo'}, 3);
< "{"a":"foo"}"
> JSON.stringify({a: 'foo'}, null, 3);
< "{
"a": "foo"
}"
我们平时可能很少用到JSON.parse
和JSON.stringify
的额外参数,但是当js对象和要转化的JSON属性不对等时,使用该参数可以避免额外的js操作(如使用delete删除额外属性,这会改变原始js变量的值)。
本文讨论了window对象上三个重要的原生对象的相关知识点,大部分内容不需要强制记忆,但是必须掌握一些常用的方法。