四、数组

声明

创建一个空数组有两种语法:

  1. let arr = new Array();
  2. let arr = [];

数组元素从 0 开始编号。
我们可以通过方括号中的数字获取元素:

  1. let fruits = ["Apple", "Orange", "Plum"];
  2. alert( fruits[0] ); // Apple
  3. alert( fruits[1] ); // Orange
  4. alert( fruits[2] ); // Plum

可以替换元素:

  1. fruits[2] = 'Pear'; // 现在变成了 ["Apple", "Orange", "Pear"]

……或者向数组新加一个元素:

  1. fruits[3] = 'Lemon'; // 现在变成 ["Apple", "Orange", "Pear", "Lemon"]

length 属性的值是数组中元素的总个数:

  1. let fruits = ["Apple", "Orange", "Plum"];
  2. alert( fruits.length ); // 3

也可以用 alert 来显示整个数组。

  1. let fruits = ["Apple", "Orange", "Plum"];
  2. alert( fruits ); // Apple,Orange,Plum

数组可以存储任何类型的元素。
例如:

  1. // 混合值
  2. let arr = [ 'Apple', { name: 'John' }, true, function() { alert('hello'); } ];
  3. // 获取索引为 1 的对象然后显示它的 name
  4. alert( arr[1].name ); // John
  5. // 获取索引为 3 的函数并执行
  6. arr[3](); // hello

pop/push, shift/unshift 方法

作用于数组末端的方法:
pop
取出并返回数组的最后一个元素:

  1. let fruits = ["Apple", "Orange", "Pear"];
  2. alert( fruits.pop() ); // 移除 "Pear" 然后 alert 显示出来
  3. alert( fruits ); // Apple, Orange

push
在数组末端添加元素:

  1. let fruits = ["Apple", "Orange"];
  2. fruits.push("Pear");
  3. alert( fruits ); // Apple, Orange, Pear

调用 fruits.push(…) 与 fruits[fruits.length] = … 是一样的。
作用于数组首端的方法:
shift
取出数组的第一个元素并返回它:

  1. let fruits = ["Apple", "Orange", "Pear"];
  2. alert( fruits.shift() ); // 移除 Apple 然后 alert 显示出来
  3. alert( fruits ); // Orange, Pear

unshift
在数组的首端添加元素:

  1. let fruits = ["Orange", "Pear"];
  2. fruits.unshift('Apple');
  3. alert( fruits ); // Apple, Orange, Pear

push 和 unshift 方法都可以一次添加多个元素:

  1. let fruits = ["Apple"];
  2. fruits.push("Orange", "Peach");
  3. fruits.unshift("Pineapple", "Lemon");
  4. // ["Pineapple", "Lemon", "Apple", "Orange", "Peach"]
  5. alert( fruits );

内部

数组是一种特殊的对象。使用方括号来访问属性 arr[0] 实际上是来自于对象的语法。它其实与 obj[key] 相同,其中 arr 是对象,而数字用作键(key)。
例如,它是通过引用来复制的:

  1. let fruits = ["Banana"]
  2. let arr = fruits; // 通过引用复制 (两个变量引用的是相同的数组)
  3. alert( arr === fruits ); // true
  4. arr.push("Pear"); // 通过引用修改数组
  5. alert( fruits ); // Banana, Pear — 现在有 2 项了

性能

push/pop 方法运行的比较快,而 shift/unshift 比较慢。image.png
为什么作用于数组的末端会比首端快呢?让我们看看在执行期间都发生了什么:

  1. fruits.shift(); // 从首端取出一个元素

只获取并移除数字 0 对应的元素是不够的。其它元素也需要被重新编号。
shift 操作必须做三件事:

  1. 移除索引为 0 的元素。
  2. 把所有的元素向左移动,把索引 1 改成 0,2 改成 1 以此类推,对其重新编号。
  3. 更新 length 属性。

image.png
数组里的元素越多,移动它们就要花越多的时间,也就意味着越多的内存操作。

循环

for循环

  1. let arr = ["Apple", "Orange", "Pear"];
  2. for (let i = 0; i < arr.length; i++) {
  3. alert( arr[i] );
  4. }

for..of

  1. let fruits = ["Apple", "Orange", "Plum"];
  2. // 遍历数组元素
  3. for (let fruit of fruits) {
  4. alert( fruit );
  5. }

for..of 不能获取当前元素的索引,只是获取元素值,但大多数情况是够用的。而且这样写更短

for..in

技术上来讲,因为数组也是对象,所以使用 for..in 也是可以的:

  1. let arr = ["Apple", "Orange", "Pear"];
  2. for (let key in arr) {
  3. alert( arr[key] ); // Apple, Orange, Pear
  4. }

多维数组

数组里的项也可以是数组。我们可以将其用于多维数组,例如存储矩阵:

  1. let matrix = [
  2. [1, 2, 3],
  3. [4, 5, 6],
  4. [7, 8, 9]
  5. ];
  6. alert( matrix[1][1] ); // 最中间的那个数

toString

数组有自己的 toString 方法的实现,会返回以逗号隔开的元素列表。
例如:

  1. let arr = [1, 2, 3];
  2. alert( arr ); // 1,2,3
  3. alert( String(arr) === '1,2,3' ); // true

五、JSON

假设我们有一个复杂的对象,我们希望将其转换为字符串,以通过网络发送,或者只是为了在日志中输出它。
当然,这样的字符串应该包含所有重要的属性。
我们可以像这样实现转换:

  1. let user = {
  2. name: "John",
  3. age: 30,
  4. toString() {
  5. return `{name: "${this.name}", age: ${this.age}}`;
  6. }
  7. };
  8. alert(user); // {name: "John", age: 30}

……但在开发过程中,会新增一些属性,旧的属性会被重命名和删除。每次更新这种 toString 都会非常痛苦。我们可以尝试遍历其中的属性,但是如果对象很复杂,并且在属性中嵌套了对象呢?我们也需要对它们进行转换。
幸运的是,不需要编写代码来处理所有这些问题。这项任务已经解决了。

JSON.stringify

JavaScript 提供了如下方法:

  • JSON.stringify 将对象转换为 JSON。
  • JSON.parse 将 JSON 转换回对象。

一、例如,在这里我们 JSON.stringify 一个 student 对象:

  1. let student = {
  2. name: 'John',
  3. age: 30,
  4. isAdmin: false,
  5. courses: ['html', 'css', 'js'],
  6. wife: null
  7. };
  8. let json = JSON.stringify(student);
  9. alert(typeof json); // we've got a string!
  10. alert(json);
  11. /* JSON 编码的对象:
  12. {
  13. "name": "John",
  14. "age": 30,
  15. "isAdmin": false,
  16. "courses": ["html", "css", "js"],
  17. "wife": null
  18. }
  19. */

方法 JSON.stringify(student) 接收对象并将其转换为字符串。
请注意,JSON 编码的对象与对象字面量有几个重要的区别:

  • 字符串使用双引号。JSON 中没有单引号或反引号。所以 ‘John’ 被转换为 “John”。
  • 对象属性名称也是双引号的。这是强制性的。所以 age:30 被转换成 “age”:30。

二、JSON.stringify 也可以应用于原始(primitive)数据类型。
JSON 支持以下数据类型:

  • Objects { … }
  • Arrays [ … ]
  • Primitives:
    • strings,
    • numbers,
    • boolean values true/false,
    • null。

例如:

  1. // 数字在 JSON 还是数字
  2. alert( JSON.stringify(1) ) // 1
  3. // 字符串在 JSON 中还是字符串,只是被双引号扩起来
  4. alert( JSON.stringify('test') ) // "test"
  5. alert( JSON.stringify(true) ); // true
  6. alert( JSON.stringify([1, 2, 3]) ); // [1,2,3]

三、JSON 是语言无关的纯数据规范,因此一些特定于 JavaScript 的对象属性会被 JSON.stringify 跳过。
即:

  • 函数属性(方法)。
  • Symbol 类型的键和值。
  • 存储 undefined 的属性。 ```javascript let user = { sayHi() { // 被忽略 alert(“Hello”); }, [Symbol(“id”)]: 123, // 被忽略 something: undefined // 被忽略 };

alert( JSON.stringify(user) ); // {}(空对象)

  1. 通常这很好。如果这不是我们想要的方式,那么我们很快就会看到如何自定义转换方式。<br />最棒的是支持嵌套对象转换,并且可以自动对其进行转换。<br />例如:
  2. ```javascript
  3. let meetup = {
  4. title: "Conference",
  5. room: {
  6. number: 23,
  7. participants: ["john", "ann"]
  8. }
  9. };
  10. alert( JSON.stringify(meetup) );
  11. /* 整个解构都被字符串化了
  12. {
  13. "title":"Conference",
  14. "room":{"number":23,"participants":["john","ann"]},
  15. }
  16. */

重要的限制:不得有循环引用。
例如:

  1. let room = {
  2. number: 23
  3. };
  4. let meetup = {
  5. title: "Conference",
  6. participants: ["john", "ann"]
  7. };
  8. meetup.place = room; // meetup 引用了 room
  9. room.occupiedBy = meetup; // room 引用了 meetup
  10. JSON.stringify(meetup); // Error: Converting circular structure to JSON

在这里,转换失败了,因为循环引用:room.occupiedBy 引用了 meetup,meetup.place 引用了 room:
image.png

排除和转换:replacer

JSON.stringify 的完整语法是:

  1. let json = JSON.stringify(value[, replacer, space])


value
要编码的值。
replacer
要编码的属性数组或映射函数 function(key, value)。
space
用于格式化的空格数量

大部分情况,JSON.stringify 仅与第一个参数一起使用。但是,如果我们需要微调替换过程,比如过滤掉循环引用,我们可以使用 JSON.stringify 的第二个参数。

如果我们传递一个属性数组给它,那么只有这些属性会被编码。
例如:

  1. let room = {
  2. number: 23
  3. };
  4. let meetup = {
  5. title: "Conference",
  6. participants: [{name: "John"}, {name: "Alice"}],
  7. place: room // meetup 引用了 room
  8. };
  9. room.occupiedBy = meetup; // room 引用了 meetup
  10. alert( JSON.stringify(meetup, ['title', 'participants']) );
  11. // {"title":"Conference","participants":[{},{}]}

这里我们可能过于严格了。属性列表应用于了整个对象结构。所以 participants 是空的,因为 name 不在列表中。
让我们包含除了会导致循环引用的 room.occupiedBy 之外的所有属性:

  1. let room = {
  2. number: 23
  3. };
  4. let meetup = {
  5. title: "Conference",
  6. participants: [{name: "John"}, {name: "Alice"}],
  7. place: room // meetup 引用了 room
  8. };
  9. room.occupiedBy = meetup; // room 引用了 meetup
  10. alert( JSON.stringify(meetup, ['title', 'participants', 'place', 'name', 'number']) );
  11. /*
  12. {
  13. "title":"Conference",
  14. "participants":[{"name":"John"},{"name":"Alice"}],
  15. "place":{"number":23}
  16. }
  17. */

现在,除 occupiedBy 以外的所有内容都被序列化了。但是属性的列表太长了。
幸运的是,我们可以使用一个函数代替数组作为 replacer。
该函数会为每个 (key,value) 对调用并返回“已替换”的值,该值将替换原有的值。如果值被跳过了,则为 undefined。
在我们的例子中,我们可以为 occupiedBy 以外的所有内容按原样返回 value。为了 occupiedBy,下面的代码返回 undefined:

  1. let room = {
  2. number: 23
  3. };
  4. let meetup = {
  5. title: "Conference",
  6. participants: [{name: "John"}, {name: "Alice"}],
  7. place: room // meetup 引用了 room
  8. };
  9. room.occupiedBy = meetup; // room 引用了 meetup
  10. alert( JSON.stringify(meetup, function replacer(key, value) {
  11. alert(`${key}: ${value}`);
  12. return (key == 'occupiedBy') ? undefined : value;
  13. }));
  14. /* key:value pairs that come to replacer:
  15. : [object Object]
  16. title: Conference
  17. participants: [object Object],[object Object]
  18. 0: [object Object]
  19. name: John
  20. 1: [object Object]
  21. name: Alice
  22. place: [object Object]
  23. number: 23
  24. occupiedBy: [object Object]
  25. */

请注意 replacer 函数会获取每个键/值对,包括嵌套对象和数组项。它被递归地应用。replacer 中的 this 的值是包含当前属性的对象。
第一个调用很特别。它是使用特殊的“包装对象”制作的:{“”: meetup}。换句话说,第一个 (key, value) 对的键是空的,并且该值是整个目标对象。这就是上面的示例中第一行是 “:[object Object]” 的原因。
这个理念是为了给 replacer 提供尽可能多的功能:如果有必要,它有机会分析并替换/跳过整个对象。

格式化:space

JSON.stringify(value, replacer, spaces) 的第三个参数是用于优化格式的空格数量。
以前,所有字符串化的对象都没有缩进和额外的空格。如果我们想通过网络发送一个对象,那就没什么问题。space 参数专门用于调整出更美观的输出。
这里的 space = 2 告诉 JavaScript 在多行中显示嵌套的对象,对象内部缩进 2 个空格:

  1. let user = {
  2. name: "John",
  3. age: 25,
  4. roles: {
  5. isAdmin: false,
  6. isEditor: true
  7. }
  8. };
  9. alert(JSON.stringify(user, null, 2));
  10. /* 两个空格的缩进:
  11. {
  12. "name": "John",
  13. "age": 25,
  14. "roles": {
  15. "isAdmin": false,
  16. "isEditor": true
  17. }
  18. }
  19. */
  20. /* 对于 JSON.stringify(user, null, 4) 的结果会有更多缩进:
  21. {
  22. "name": "John",
  23. "age": 25,
  24. "roles": {
  25. "isAdmin": false,
  26. "isEditor": true
  27. }
  28. }
  29. */

第三个参数也可以是字符串。在这种情况下,字符串用于缩进,而不是空格的数量。
spaces 参数仅用于日志记录和美化输出。

自定义 “toJSON”

像 toString 进行字符串转换,对象也可以提供 toJSON 方法来进行 JSON 转换。如果可用,JSON.stringify 会自动调用它。
例如:

  1. let room = {
  2. number: 23
  3. };
  4. let meetup = {
  5. title: "Conference",
  6. date: new Date(Date.UTC(2017, 0, 1)),
  7. room
  8. };
  9. alert( JSON.stringify(meetup) );
  10. /*
  11. {
  12. "title":"Conference",
  13. "date":"2017-01-01T00:00:00.000Z", // (1)
  14. "room": {"number":23} // (2)
  15. }
  16. */

在这儿我们可以看到 date (1) 变成了一个字符串。这是因为所有日期都有一个内建的 toJSON 方法来返回这种类型的字符串。

现在让我们为对象 room 添加一个自定义的 toJSON:

  1. let room = {
  2. number: 23,
  3. toJSON() {
  4. return this.number;
  5. }
  6. };
  7. let meetup = {
  8. title: "Conference",
  9. room
  10. };
  11. alert( JSON.stringify(room) ); // 23
  12. alert( JSON.stringify(meetup) );
  13. /*
  14. {
  15. "title":"Conference",
  16. "room": 23
  17. }
  18. */

正如我们所看到的,toJSON 既可以用于直接调用 JSON.stringify(room) 也可以用于当 room 嵌套在另一个编码对象中时。

JSON.parse

要解码 JSON 字符串,我们需要另一个方法 JSON.parse。
语法:

  1. let value = JSON.parse(str, [reviver]);

str
要解析的 JSON 字符串。
reviver
可选的函数 function(key,value),该函数将为每个 (key, value) 对调用,并可以对值进行转换。
例如:

  1. // 字符串化数组
  2. let numbers = "[0, 1, 2, 3]";
  3. numbers = JSON.parse(numbers);
  4. alert( numbers[1] ); // 1

对于嵌套对象:

  1. let userData = '{ "name": "John", "age": 35, "isAdmin": false, "friends": [0,1,2,3] }';
  2. let user = JSON.parse(userData);
  3. alert( user.friends[1] ); // 1

JSON 可能会非常复杂,对象和数组可以包含其他对象和数组。但是它们必须遵循相同的 JSON 格式。
以下是手写 JSON 时的典型错误(有时我们必须出于调试目的编写它):

  1. let json = `{
  2. name: "John", // 错误:属性名没有双引号
  3. "surname": 'Smith', // 错误:值使用的是单引号(必须使用双引号)
  4. 'isAdmin': false // 错误:键使用的是单引号(必须使用双引号)
  5. "birthday": new Date(2000, 2, 3), // 错误:不允许使用 "new",只能是裸值
  6. "friends": [0,1,2,3] // 这个没问题
  7. }`;

此外,JSON 不支持注释。向 JSON 添加注释无效。

使用 reviver

想象一下,我们从服务器上获得了一个字符串化的 meetup 对象。
它看起来像这样:

  1. // title: (meetup title), date: (meetup date)
  2. let str = '{"title":"Conference","date":"2017-11-30T12:00:00.000Z"}';

……现在我们需要对它进行 反序列(deserialize),把它转换回 JavaScript 对象。
让我们通过调用 JSON.parse 来完成:

  1. let str = '{"title":"Conference","date":"2017-11-30T12:00:00.000Z"}';
  2. let meetup = JSON.parse(str);
  3. alert( meetup.date.getDate() ); // Error!

啊!报错了!
meetup.date 的值是一个字符串,而不是 Date 对象。JSON.parse 怎么知道应该将字符串转换为 Date 呢?
让我们将 reviver 函数传递给 JSON.parse 作为第二个参数,该函数按照“原样”返回所有值,但是 date 会变成 Date:

  1. let str = '{"title":"Conference","date":"2017-11-30T12:00:00.000Z"}';
  2. let meetup = JSON.parse(str, function(key, value) {
  3. if (key == 'date') return new Date(value);
  4. return value;
  5. });
  6. alert( meetup.date.getDate() ); // 现在正常运行了!

顺便说一下,这也适用于嵌套对象:

  1. let schedule = `{
  2. "meetups": [
  3. {"title":"Conference","date":"2017-11-30T12:00:00.000Z"},
  4. {"title":"Birthday","date":"2017-04-18T12:00:00.000Z"}
  5. ]
  6. }`;
  7. schedule = JSON.parse(schedule, function(key, value) {
  8. if (key == 'date') return new Date(value);
  9. return value;
  10. });
  11. alert( schedule.meetups[1].date.getDate() ); // 正常运行了

六、日期和时间

创建

new Date()
不带参数 —— 创建一个表示当前日期和时间的 Date 对象:

  1. let now = new Date();
  2. alert( now ); // 显示当前的日期/时间
  3. //Fri May 13 2022 11:54:11 GMT+0800 (中国标准时间)

new Date(milliseconds)
创建一个 Date 对象,其时间等于 1970 年 1 月 1 日 UTC+0 之后经过的毫秒数(1/1000 秒)。

  1. // 0 表示 01.01.1970 UTC+0
  2. let Jan01_1970 = new Date(0);
  3. alert( Jan01_1970 );
  4. // 现在增加 24 小时,得到 02.01.1970 UTC+0
  5. let Jan02_1970 = new Date(24 * 3600 * 1000);//毫秒*1000 ->1秒 *3600->1小时
  6. alert( Jan02_1970 );
  7. // 31 Dec 1969, 可以用负数 减少一小时
  8. let Dec31_1969 = new Date(-24 * 3600 * 1000);
  9. alert( Dec31_1969 );

new Date(datestring)
如果只有一个参数,并且是字符串,那么它会被自动解析。该算法与 Date.parse 所使用的算法相同,我们将在下文中进行介绍。

  1. let date = new Date("2017-01-26");
  2. alert(date);
  3. // 该时间未被设定,因此被假定为格林尼治标准时间(GMT)的午夜(midnight)
  4. // 并会根据你运行代码时的时区进行调整
  5. // 因此,结果可能是
  6. // Thu Jan 26 2017 11:00:00 GMT+1100 (Australian Eastern Daylight Time)
  7. // 或
  8. // Wed Jan 25 2017 16:00:00 GMT-0800 (Pacific Standard Time)

new Date(year, month, date, hours, minutes, seconds, ms)
使用当前时区中的给定组件创建日期。只有前两个参数是必须的。

  • year 必须是四位数:2013 是合法的,98 是不合法的。
  • month 计数从 0(一月)开始,到 11(十二月)结束。
  • date 是当月的具体某一天,如果缺失,则为默认值 1。
  • 如果 hours/minutes/seconds/ms 缺失,则均为默认值 0。

例如:

  1. new Date(2011, 0, 1, 0, 0, 0, 0); // 1 Jan 2011, 00:00:00
  2. new Date(2011, 0, 1); // 同样,时分秒等均为默认值 0
  3. //时间度量最大精确到 1 毫秒(1/1000 秒):
  4. let date = new Date(2011, 0, 1, 2, 3, 4, 567);
  5. alert( date ); // 1.01.2011, 02:03:04.567

访问日期组件

从 Date 对象中访问年、月等信息有多种方式:
getFullYear()
获取年份(4 位数)
getMonth()
获取月份,从 0 到 11
getDate()
获取当月的具体日期,从 1 到 31,这个方法名称可能看起来有些令人疑惑。
getHours(),getMinutes(),getSeconds(),getMilliseconds()
获取相应的时间组件。
另外,我们还可以获取一周中的第几天:
getDay()
获取一周中的第几天,从 0(星期日)到 6(星期六)。第一天始终是星期日,在某些国家可能不是这样的习惯,但是这不能被改变。
以上的所有方法返回的组件都是基于当地时区的。

当然,也有与当地时区的 UTC 对应项,它们会返回基于 UTC+0 时区的日、月、年等:getUTCFullYear(),getUTCMonth(),getUTCDay()。只需要在 “get” 之后插入 “UTC” 即可。
如果你当地时区相对于 UTC 有偏移,那么下面代码会显示不同的小时数:

  1. // 当前日期
  2. let date = new Date();
  3. // 当地时区的小时数
  4. alert( date.getHours() );
  5. // 在 UTC+0 时区的小时数(非夏令时的伦敦时间)
  6. alert( date.getUTCHours() );

除了上述给定的方法,还有两个没有 UTC 变体的特殊方法:
getTime()
返回日期的时间戳 —— 从 1970-1-1 00:00:00 UTC+0 开始到现在所经过的毫秒数。
getTimezoneOffset()
返回 UTC 与本地时区之间的时差,以分钟为单位:

  1. // 如果你在时区 UTC-1,输出 60
  2. // 如果你在时区 UTC+3,输出 -180
  3. alert( new Date().getTimezoneOffset() );

设置日期组件

下列方法可以设置日期/时间组件:

  • setFullYear(year, [month], [date])
  • setMonth(month, [date])
  • setDate(date)
  • setHours(hour, [min], [sec], [ms])
  • setMinutes(min, [sec], [ms])
  • setSeconds(sec, [ms])
  • setMilliseconds(ms)
  • setTime(milliseconds)(使用自 1970-01-01 00:00:00 UTC+0 以来的毫秒数来设置整个日期)

以上方法除了 setTime() 都有 UTC 变体,例如:setUTCHours()。
我们可以看到,有些方法可以一次性设置多个组件,比如 setHours。未提及的组件不会被修改。
举个例子:

  1. let today = new Date();
  2. today.setHours(0);
  3. alert(today); // 日期依然是今天,但是小时数被改为了 0
  4. today.setHours(0, 0, 0, 0);
  5. alert(today); // 日期依然是今天,时间为 00:00:00。

自动校准(Autocorrection)

自动校准 是 Date 对象的一个非常方便的特性。我们可以设置超范围的数值,它会自动校准。
举个例子:

  1. let date = new Date(2013, 0, 32); // 32 Jan 2013 ?!?
  2. alert(date); // ……是 1st Feb 2013!

超出范围的日期组件将会被自动分配。
假设我们要在日期 “28 Feb 2016” 上加 2 天。结果可能是 “2 Mar” 或 “1 Mar”,因为存在闰年。但是我们不需要去考虑这些,只需要直接加 2 天,剩下的 Date 对象会帮我们处理:

  1. let date = new Date(2016, 1, 28);
  2. date.setDate(date.getDate() + 2);
  3. alert( date ); // 1 Mar 2016

这个特性经常被用来获取给定时间段后的日期。例如,我们想获取“现在 70 秒后”的日期:

  1. let date = new Date();
  2. date.setSeconds(date.getSeconds() + 70);
  3. alert( date ); // 显示正确的日期信息

我们还可以设置 0 甚至可以设置负值。例如:

  1. let date = new Date(2016, 0, 2); // 2016 年 1 月 2 日
  2. date.setDate(1); // 设置为当月的第一天
  3. alert( date );
  4. date.setDate(0); // 天数最小可以设置为 1,所以这里设置的是上一月的最后一天
  5. alert( date ); // 31 Dec 2015

日期转化为数字,日期差值

当 Date 对象被转化为数字时,得到的是对应的时间戳,与使用 date.getTime() 的结果相同:

  1. let date = new Date();
  2. alert(+date); // 以毫秒为单位的数值,与使用 date.getTime() 的结果相同
  3. //1652420078324

有一个重要的副作用:日期可以相减,相减的结果是以毫秒为单位时间差。
这个作用可以用于时间测量:

  1. let start = new Date(); // 开始测量时间
  2. // do the job
  3. for (let i = 0; i < 100000; i++) {
  4. let doSomething = i * i * i;
  5. }
  6. let end = new Date(); // 结束测量时间
  7. alert( `The loop took ${end - start} ms` );
  8. //The loop took 2 ms

Date.now()

如果我们仅仅想要测量时间间隔,我们不需要 Date 对象。
有一个特殊的方法 Date.now(),它会返回当前的时间戳。
它相当于 new Date().getTime(),但它不会创建中间的 Date 对象。因此它更快,而且不会对垃圾处理造成额外的压力。
这种方法很多时候因为方便,又或是因性能方面的考虑而被采用,例如使用 JavaScript 编写游戏或其他的特殊应用场景。
因此这样做可能会更好:

  1. let start = Date.now(); // 从 1 Jan 1970 至今的时间戳
  2. // do the job
  3. for (let i = 0; i < 100000; i++) {
  4. let doSomething = i * i * i;
  5. }
  6. let end = Date.now(); // 完成
  7. alert( `The loop took ${end - start} ms` ); // 相减的是时间戳,而不是日期

度量(Benchmarking)

如果我们想要为一个很耗 CPU 性能的函数提供一个可靠的度量(benchmark),我们应该小心一点。
例如,我们想判断两个计算日期差值的函数:哪个更快?
这种性能测量通常称为“度量(benchmark)”。

  1. // 我们有 date1 和 date2,哪个函数会更快地返回两者的时间差?
  2. function diffSubtract(date1, date2) {
  3. return date2 - date1;
  4. }
  5. // or
  6. function diffGetTime(date1, date2) {
  7. return date2.getTime() - date1.getTime();
  8. }

这两个函数做的事情完全相同,但是其中一个函数使用显性的 date.getTime() 来获取毫秒形式的日期,另一个则依赖于“日期 — 数字”的转换。它们的结果是一样的。
那么,哪个更快呢?
首先想到的方法可能是连续运行它们很多次,并计算时间差。就我们的例子而言,函数非常简单,所以我们必须执行至少 100000 次。
让我们开始测量:

  1. function diffSubtract(date1, date2) {
  2. return date2 - date1;
  3. }
  4. function diffGetTime(date1, date2) {
  5. return date2.getTime() - date1.getTime();
  6. }
  7. function bench(f) {
  8. let date1 = new Date(0);
  9. let date2 = new Date();
  10. let start = Date.now();
  11. for (let i = 0; i < 100000; i++) f(date1, date2);
  12. return Date.now() - start;
  13. }
  14. alert( 'Time of diffSubtract: ' + bench(diffSubtract) + 'ms' );
  15. alert( 'Time of diffGetTime: ' + bench(diffGetTime) + 'ms' );

哇!使用 getTime() 这种方式快得多!原因是它没有类型转化,这样对引擎优化来说更加简单。
好,我们得到了结论,但是这并不是一个很好的度量的例子。
想象一下当运行 bench(diffSubtract) 的同时,CPU 还在并行处理其他事务,并且这也会占用资源。然而,运行 bench(diffGetTime) 的时候,并行处理的事务完成了。
这是对于现代多进程操作系统来说的一个非常真实的场景。
结果就是,第一个函数相比于第二个函数,缺少 CPU 资源。这可能导致错误的结论。
为了得到更加可靠的度量,整个度量测试包应该重新运行多次。
例如,像下面的代码这样:

  1. function diffSubtract(date1, date2) {
  2. return date2 - date1;
  3. }
  4. function diffGetTime(date1, date2) {
  5. return date2.getTime() - date1.getTime();
  6. }
  7. function bench(f) {
  8. let date1 = new Date(0);
  9. let date2 = new Date();
  10. let start = Date.now();
  11. for (let i = 0; i < 100000; i++) f(date1, date2);
  12. return Date.now() - start;
  13. }
  14. let time1 = 0;
  15. let time2 = 0;
  16. // 交替运行 bench(diffSubtract) 和 bench(diffGetTime) 各 10 次
  17. for (let i = 0; i < 10; i++) {
  18. time1 += bench(diffSubtract);
  19. time2 += bench(diffGetTime);
  20. }
  21. alert( 'Total time for diffSubtract: ' + time1 );
  22. alert( 'Total time for diffGetTime: ' + time2 );

总结上面
现代的 JavaScript 引擎的先进优化策略只对执行很多次的 “hot code” 有效(对于执行很少次数的代码没有必要优化)。因此,在上面的例子中,第一次执行的优化程度不高。我们可能需要增加一个升温步骤:

  1. // 在主循环中增加“升温”环节
  2. bench(diffSubtract);
  3. bench(diffGetTime);
  4. // 开始度量
  5. for (let i = 0; i < 10; i++) {
  6. time1 += bench(diffSubtract);
  7. time2 += bench(diffGetTime);
  8. }

对一个字符串使用 Date.parse

Date.parse(str) 方法可以从一个字符串中读取日期。
字符串的格式应该为:YYYY-MM-DDTHH:mm:ss.sssZ,其中:

  • YYYY-MM-DD —— 日期:年-月-日。
  • 字符 “T” 是一个分隔符。
  • HH:mm:ss.sss —— 时间:小时,分钟,秒,毫秒。
  • 可选字符 ‘Z’ 为 +-hh:mm 格式的时区。单个字符 Z 代表 UTC+0 时区。

简短形式也是可以的,比如 YYYY-MM-DD 或 YYYY-MM,甚至可以是 YYYY。
Date.parse(str) 调用会解析给定格式的字符串,并返回时间戳(自 1970-01-01 00:00:00 起所经过的毫秒数)。如果给定字符串的格式不正确,则返回 NaN。
举个例子:

  1. let ms = Date.parse('2012-01-26T13:51:50.417-07:00');
  2. alert(ms); // 1327611110417 (时间戳)

我们可以通过时间戳来立即创建一个 new Date 对象:

  1. let date = new Date( Date.parse('2012-01-26T13:51:50.417-07:00') );
  2. alert(date);//Fri Jan 27 2012 04:51:50 GMT+0800 (中国标准时间)