在处理预约时间段时,需要将一组可能存在连续时间段的数组进行整理,合并相邻的连续时间范围。时间连续的定义是:一个时间段的结束时间等于下一个时间段的开始时间。最终结果是一个新的数组,其中每个元素表示一个合并后的连续时间范围。

1
2
3
4
5
6
[
{ startTime: '09:00', endTime: '09:30' },
{ startTime: '09:30', endTime: '10:00' },
{ startTime: '10:00', endTime: '10:30' },
{ startTime: '10:30', endTime: '11:00' }
]

输出:

1
2
3
[
{ startTime: '09:00', endTime: '11:00' }
]

刚开始有这样一段函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function mergeTimeIntervals(intervals) {
if (intervals.length === 0) return [];

  // 初始化结果数组和第一个区间
  const mergedIntervals = [intervals[0]];
 
  for (let i = 1; i < intervals.length; i++) {
const lastMerged = mergedIntervals[mergedIntervals.length - 1];
const current = intervals[i];
// 如果前一个区间的 endTime 等于当前区间的 startTime,则合并
if (lastMerged.endTime === current.startTime) {
  lastMerged.endTime = current.endTime;
} else {
  // 否则,直接添加到结果数组中
  mergedIntervals.push(current);
}
  }
 
  return mergedIntervals;
}

该函数诈一看没有问题,要说问题的话,也就是[intervals[0]] 地址和传入进来的地址一样,但是对我来说无所谓嘛,反正我用的是返回结果。并不关心之前的值。但是你别说,不关心地址的话,在使用中还真出现了问题。

问题原因

  • [intervals[0]] 是引用: mergedIntervals 中的第一个元素直接引用了 intervals[0],因此对 mergedIntervals 的修改实际上也会影响 intervals 的数据。
  • 原数据的隐式修改: 尽管你只使用返回值,但因为 intervals 被间接修改了,可能会导致调用 mergeTimeIntervals 后的数据状态变得不可控,特别是在 Vue 或 React 这样的框架中,数据的修改会触发不必要的重渲染或其他副作用。

下面就是一个错误的场景(VUE3+ElementPlus):

1
2
3
4
5
6
7
8
9
10
11
12
<template>
  <el-table :data="tableData" style="width: 100%">
    <el-table-column prop="date" label="Date" width="180" />
    <el-table-column prop="name" label="Name" width="180" />
    <el-table-column prop="address" label="Address" />
    <el-table-column  label="时间范围">
      <template #default="{ row }">
        {{mergeTimeIntervals(row.intervals)}}
      </template>
    </el-table-column>
  </el-table>
</template>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<script lang="ts" setup>
const tableData = [
  {
    date: '2016-05-03',
    name: 'Tom',
    address: 'No. 189, Grove St, Los Angeles',
    intervals: [
      { startTime: '09:00', endTime: '9:30', bookingFlag: 0 },
      { startTime: '9:30', endTime: '10:00', bookingFlag: 0 },
      { startTime: '10:00', endTime: '10:30', bookingFlag: 0 },
      { startTime: '10:30', endTime: '11:00', bookingFlag: 0 }
    ]
  }
]

function mergeTimeIntervals(intervals) {
if (intervals.length === 0) return [];

  // 初始化结果数组和第一个区间
  const mergedIntervals = [intervals[0]];
 
  for (let i = 1; i < intervals.length; i++) {
const lastMerged = mergedIntervals[mergedIntervals.length - 1];
const current = intervals[i];
// 如果前一个区间的 endTime 等于当前区间的 startTime,则合并
if (lastMerged.endTime === current.startTime) {
  lastMerged.endTime = current.endTime;
} else {
  // 否则,直接添加到结果数组中
  mergedIntervals.push(current);
}
  }
  console.log(mergedIntervals, 'mergedIntervals')
  return mergedIntervals;
}

</script>

按道理说,这种才是正常的效果9:00 - 11:00

image.png

但是执行完成总是不尽人意:

image.png

展示的数据多了好多,而且在仔细看看自己的代码,感觉逻辑上没有问题啊,怎么会出现这么多的数据呢?

048c7d47a2ba2b38f47e52caa9ae8fa.png

总的来说,在函数中,const mergedIntervals = [intervals[0]]; 初始化时,mergedIntervals 中的第一个元素直接引用了原始数据 intervals[0]。由于 JavaScript 的对象是引用类型,这导致 mergedIntervalsintervals 共享同一个内存地址。

当代码执行到 lastMerged.endTime = current.endTime; 时,mergedIntervals 的内容被修改,同时也直接影响了 intervals 的原始数据。对于表格组件来说,这种数据变化会触发视图重新渲染,而每次渲染都会重新调用该函数,形成了意想不到的循环执行。

下面是对代码进行的更改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function mergeTimeIntervals(intervals) {
if (intervals.length === 0) return [];

  // 初始化结果数组和第一个区间 拷贝第一个区间
  const mergedIntervals = [{...intervals[0]}];
 
  for (let i = 1; i < intervals.length; i++) {
const lastMerged = mergedIntervals[mergedIntervals.length - 1];
const current = intervals[i];
// 如果前一个区间的 endTime 等于当前区间的 startTime,则合并
if (lastMerged.endTime === current.startTime) {
  lastMerged.endTime = current.endTime;
} else {
  // 否则,直接拷贝添加到结果数组中
  mergedIntervals.push({ ...current });
}
  }
 
  return mergedIntervals;
}
  • 不修改原始数据: 所有的区间在合并时都经过浅拷贝,返回的新数组和输入的 intervals 互不干扰。
  • 安全性提高: 在任何框架中使用都不会导致原始数据的意外污染,避免了隐式副作用。