原生Javascript实现多条件筛选

210 阅读8分钟

介绍

最近在开发的时候,需要前端单独实现数据的筛选。

写完觉得挺有意思的,所以就抽出来单独写了一个DEMO,分享给大家,也欢迎大家指正。

效果

DEMO可以通过码上掘金体验 多条件筛选 - 码上掘金 (juejin.cn)

代码

主要函数就三个,分别是:

筛选条件判断函数

目前支持的是包含/不包含/等于/不等于四种筛选条件。

function handleConditions(type = "include", leftVal, rightVal) {
  if (type === "equal") {
    return leftVal === rightVal;
  }
  if (type === "not-equal") {
    return leftVal !== rightVal;
  }
  if (type === "include") {
    return `${leftVal}`.indexOf(rightVal) !== -1;
  }
  if (type === "not-include") {
    return `${leftVal}`.indexOf(rightVal) === -1;
  }
}

数据交集函数

是一个纯函数,用于筛选出符合条件的所有数据,多个条件取交集。

第一次数据筛选和后续数据筛选使用的数据源要区分开,因为第一次筛选的数据源是原始数据,后续筛选的数据源是经过第一次筛选后的数据。

function intersectionFun(data, conditions) {
  let tmp = [];
  if (conditions.length === 0) {
    tmp = data;
  } else {
    for (let index = 0; index < conditions.length; index++) {
      const { type, key, value } = conditions[index];
      if (index === 0) {
        tmp = data.filter((item) => {
          return handleConditions(type, item[key], value);
        });
      } else {
        tmp = tmp.filter((item) => {
          return handleConditions(type, item[key], value);
        });
      }
    }
  }
  return tmp;
}

数据并集函数

是一个纯函数,用于筛选出符合条件的所有数据,多个条件取并集。

需要注意取并集后的数据会存在重复,需要去重。

function unionFun(data, conditions) {
  let tmp = [];
  if (conditions.length === 0) {
    tmp = data;
  } else {
    for (let index = 0; index < conditions.length; index++) {
      const { type, key, value } = conditions[index];
      tmp = tmp.concat(
        data.filter((item) => {
          return handleConditions(type, item[key], value);
        })
      );
    }
  }
  return [...new Set(tmp)];
}

界面

DEMO用到的元素也不多,所以使用的是原生HTML实现的界面,比较简陋(这个时候 UI 框架的作用就体现出来了😄😄😄)。

body {
  max-width: 980px;
  margin: 20px auto;
}
.form-wrap {
  display: flex;
  justify-content: space-around;
}
.form-item {
  margin-bottom: 15px;
}

label {
  display: block;
  margin-bottom: 5px;
}

input[type="text"],
button {
  margin-right: 5px;
}
table {
  border-collapse: collapse;
}
<form class="form-wrap" method="post" onsubmit="search(event)" onreset="resetForm()">
  <div class="form-item">
    <label for="id">编码:</label>
    <input type="text" id="id" name="id" placeholder="请输入内容" />
  </div>

  <div class="form-item">
    <label for="name">姓名:</label>
    <input type="text" id="name" name="name" placeholder="请输入内容" />
  </div>

  <div class="form-item">
    <label for="gender">性别:</label>
    <select id="gender" name="gender">
      <option value="男"></option>
      <option value="女"></option>
    </select>
  </div>

  <div class="form-item">
    <label for="age">年龄:</label>
    <input type="text" id="age" name="age" placeholder="请输入内容" />
  </div>

  <div class="form-item">
    <label for="hobby">兴趣:</label>
    <select id="hobby" name="hobby" multiple>
      <option value="篮球">篮球</option>
      <option value="足球">足球</option>
      <option value="网球">网球</option>
      <option value="排球">排球</option>
      <option value="羽毛球">羽毛球</option>
    </select>
  </div>

  <div class="form-item">
    <label for="description">描述:</label>
    <input
      type="text"
      id="description"
      name="description"
      placeholder="请输入内容"
    />
  </div>

  <div class="form-search">
    <button type="submit">搜索</button>
    <button type="reset">重置</button>
  </div>
</form>

<div>
  <table collapse="collapse">
    <thead>
      <th>编码</th>
      <th>姓名</th>
      <th>性别</th>
      <th>年龄</th>
      <th>兴趣</th>
      <th>描述</th>
    </thead>
    <tbody id="tableBody"></tbody>
  </table>
</div>

筛选条件的取值/处理,以及筛选后数据的渲染也都是使用原生Javascript实现,所以没有使用任何第三方库。

const mockData = [
  {
    id: 1,
    name: "张三",
    gender: "男",
    age: 18,
    hobby: ["篮球", "网球"],
    description: "我是张三",
  },
  {
    id: 2,
    name: "李四",
    gender: "男",
    age: 19,
    hobby: ["排球", "足球"],
    description: "我是李四",
  },
  {
    id: 3,
    name: "王五",
    gender: "男",
    age: 20,
    hobby: ["网球", "羽毛球"],
    description: "我是王五",
  },
  {
    id: 4,
    name: "赵六",
    gender: "女",
    age: 21,
    hobby: ["羽毛球"],
    description: "我是赵六",
  },
  {
    id: 5,
    name: "孙七",
    gender: "女",
    age: 22,
    hobby: ["篮球", "网球", "羽毛球"],
    description: "我是孙七",
  },
];
const search = (e) => {
  e.preventDefault();
  var form = e.target;
  const id = form.elements["id"];
  const name = form.elements["name"];
  const gender = form.elements["gender"];
  const age = form.elements["age"];
  const hobby = form.elements["hobby"];
  const description = form.elements["description"];
  let selectedHobby = [];
  for (var i = 0; i < hobby.selectedOptions.length; i++) {
    selectedHobby.push(hobby.selectedOptions[i].value);
  }
  // 清空选中的值,以便下次使用
  hobby.selectedIndex = -1;

  console.log({
    id: id.value,
    name: name.value,
    gender: gender.value,
    age: age.value,
    hobby: selectedHobby,
    description: description.value,
  });

  let conditions = [];
  if (id.value) {
    conditions.push({
      type: "include",
      key: 'id',
      value: id.value,
    })
  }
  if (name.value) {
    conditions.push({
      type: "include",
      key: 'name',
      value: name.value,
    })
  }
  if (gender.value) {
    conditions.push({
      type: "equal",
      key: 'gender',
      value: gender.value,
    })
  }
  if (age.value) {
    conditions.push({
      type: "include",
      key: 'age',
      value: age.value,
    })
  }
  if (description.value) {
    conditions.push({
      type: "include",
      key: 'description',
      value: description.value,
    })
  }

  const intersectionResult = intersectionFun(mockData, conditions);
  
  let conditions2 = [];

  if (selectedHobby.length > 0) {
    selectedHobby.forEach(item => {
      conditions2.push({
        type: "include",
        key: 'hobby',
        value: item,
      })
    });
  }

  const result = unionFun(intersectionResult, conditions2);

  rendTbody(result.sort((a, b) => a.id - b.id));

  return false;
};
const rendTbody = (data) => {
  const tbody = document.querySelector("#tableBody");
  if (data.length === 0) {
    tbody.innerHTML = 'No Data';
    return;
  }
  tbody.innerHTML = '';
  // 循环生成表格行和单元格
  for (let i = 0; i < data.length; i++) {
    const row = document.createElement("tr");
    const person = data[i];
    Object.keys(person).map((key) => {
      const td = document.createElement("td");
      td.textContent = `${person[key]}`;
      td.style.border = "1px solid black";
      row.appendChild(td);
    });
    tbody.appendChild(row);
  }
};
const resetForm = () => {
  rendTbody(mockData);
};
window.onload = () => {
  rendTbody(mockData);
};

AI优化代码

写完找AI帮忙优化了一下,确实帮助发现了一些问题,也学到了不少东西。

筛选条件判断函数

请优化下面的函数:

function handleConditions(type = "include", leftVal, rightVal) {
  if (type === "equal") {
    return leftVal === rightVal;
  }
  if (type === "not-equal") {
    return leftVal !== rightVal;
  }
  if (type === "include") {
    return `${leftVal}`.indexOf(rightVal) !== -1;
  }
  if (type === "not-include") {
    return `${leftVal}`.indexOf(rightVal) === -1;
  }
}

代码检查与优化

检查问题:

  • 类型安全:代码没有检查leftValrightVal的类型,这可能导致类型不匹配时的错误比较。

  • 性能:handleConditions函数中对leftVal使用了字符串模板(${leftVal}),这意味着无论leftVal的原始类型是什么,它都会被转换为字符串。这可能导致不必要的类型转换和性能开销。

  • 代码风格:虽然这种if-else结构是有效的,但使用switch-case可能会使代码更简洁和易读。

优化后的代码:

function handleConditions(type = "include", leftVal, rightVal) {
  switch (type) {
    case "equal":
      return leftVal === rightVal;
    case "not-equal":
      return leftVal !== rightVal;
    case "include":
      // 此处不采纳,因为数字类型也需要被筛选
      // 只对字符串进行include检查,其他类型返回false
      // return typeof leftVal === 'string' && leftVal.includes(rightVal);
      return `${leftVal}`.includes(rightVal);
    case "not-include":
      // 此处不采纳,因为数字类型也需要被筛选
      // 只对字符串进行not-include检查,其他类型返回true
      // return typeof leftVal === 'string' && leftVal.includes(rightVal);
      return !`${leftVal}`.includes(rightVal);
    default:
      return false; // 未知类型,默认返回false
  }
}

优化点:

  • 类型安全:在includenot-include条件下,增加了对leftVal类型的检查,确保只对字符串执行includes操作。

  • 性能:避免了不必要的字符串转换,只在字符串上执行includes操作。

  • 代码风格:使用switch-case结构提高了代码的可读性。

这个优化版本的handleConditions函数在处理类型不匹配时更加健壮,并且针对字符串操作进行了优化,提高了性能。对于非字符串类型,includenot-include条件将分别默认返回falsetrue。如果需要进一步处理其他类型的includenot-include逻辑,可以在函数内部添加相应的代码。

这个优化后代码做了字符串类型的判断,但是这样的话数字类型就不会被筛选了,所以不能采纳。

数据交集函数

请优化下面的函数:

function intersectionFun(data, conditions) {
  let tmp = [];
  if (conditions.length === 0) {
    tmp = data;
  } else {
    for (let index = 0; index < conditions.length; index++) {
      const { type, key, value } = conditions[index];
      if (index === 0) {
        tmp = data.filter((item) => {
          return handleConditions(type, item[key], value);
        });
      } else {
        tmp = tmp.filter((item) => {
          return handleConditions(type, item[key], value);
        });
      }
    }
  }
  return tmp;
}

问题分析

代码片段中intersectionFun函数的主要逻辑是遍历conditions数组,并使用每个条件对data数组进行过滤。在每次迭代中,它都会更新tmp数组以包含符合当前条件的元素。

从逻辑上看,代码没有明显的错误,但是有一些地方可以进行优化以提高代码的清晰度和效率。

  • 冗余的初始化:在函数开始时,tmp被初始化为一个空数组。然后在if (conditions.length === 0)条件下,tmp被赋值为data。这个初始化步骤是多余的,因为当conditions为空数组时,函数将直接返回data,而不会使用tmp

  • 不必要的判断:if (index === 0)条件用于区分第一次迭代和其他迭代。这实际上是不必要的,因为无论在哪次迭代中,tmp都使用filter方法进行更新。因此,这个判断可以移除。

  • 可读性:将conditions[index]解构为{ type, key, value }是一个好的做法,但为了提高可读性,可以考虑将其放在循环体内部,这样可以避免在循环的每次迭代中重复进行解构操作。

优化后的代码

function intersectionFun(data, conditions) {
  if (conditions.length === 0) {
    return data; // 直接返回data,不需要额外的tmp数组
  }

  let tmp = data.filter((item) => {
    return handleConditions(conditions[0].type, item[conditions[0].key], conditions[0].value);
  });

  for (let index = 1; index < conditions.length; index++) {
    const { type, key, value } = conditions[index]; // 将解构操作放在循环体内
    tmp = tmp.filter((item) => {
      return handleConditions(type, item[key], value);
    });
  }

  return tmp;
}

优化点总结

  • 移除了不必要的tmp初始化。

  • 删除了if (index === 0)条件判断,简化了循环逻辑。

  • 将解构操作移入循环体内,提高了代码的可读性。

数据并集函数

请优化下面的函数:

function unionFun(data, conditions) {
  let tmp = [];
  if (conditions.length === 0) {
    tmp = data;
  } else {
    for (let index = 0; index < conditions.length; index++) {
      const { type, key, value } = conditions[index];
      tmp = tmp.concat(
        data.filter((item) => {
          return handleConditions(type, item[key], value);
        })
      );
    }
  }
  return [...new Set(tmp)];
}

代码问题与优化点

  • 重复过滤:在unionFun函数中,每次循环都通过data.filter来过滤数据,这会导致多次遍历data数组,影响性能。

  • 去重方式:使用Set来去除重复项是正确的,但转换为Set和再转回数组可能会降低性能。

  • 代码风格:let tmp = [];tmp = tmp.concat(...) 的写法不够简洁。

  • 逻辑优化:如果conditions数组为空,则直接返回data,这一点做得很好。

修复与优化后的代码

function unionFun(data, conditions) {
  // 此处不能采纳,因为后面是concat拼接的,初始需要为空数组
  // let result = [...data]; // 初始结果集为data的副本
  let result = [];
  if (conditions.length === 0) {
    result = data;
  }
  for (const { type, key, value } of conditions) {
    // 使用filter一次性过滤满足条件的项,并扩展到结果集中
    result = result.concat(data.filter(item => handleConditions(type, item[key], value)));
  }

  // 使用Set去重,并转回数组
  return [...new Set(result)];
}

优化点总结

  • 减少重复过滤:通过只遍历data一次并累积结果,避免了重复过滤。

  • 简化代码风格:使用扩展运算符...来简化数组操作。

  • 性能优化:虽然去重部分仍然使用了Set,但在其他部分进行了优化,使得整体性能更佳。

请注意,虽然这里进行了一些性能优化,但在处理大数据集时,Set去重可能仍然是一个瓶颈。如果性能是首要考虑因素,可能需要考虑其他去重策略或数据结构。

AI 优化版 DEMO可以通过码上掘金体验多条件筛选-AI优化版 - 码上掘金 (juejin.cn)

总结

  • 前端也可以进行数据筛选,但是需要考虑到性能问题。
  • AI 优化代码还是很强的,但也不能完全依赖 AI,需要我们自己结合 AI 建议进行优化。