把这页想成"在一堆衣服里挑衣服":你先定条件"只要红色",再叠加"只要 M 码",再叠加"价格不超过 200"——条件一个一个往上加,范围越收越窄。管理后台 80% 的列表页都是这个逻辑。这页要搞懂三件事:前端筛选和后端搜索怎么选、多个条件怎么叠加、点表头怎么排序。核心武器就是数组的 filter 和 sort。
| 姓名 | 角色 | 年龄 | 状态 |
|---|
试试:姓名输入"张" + 角色选"管理员",条件是叠加生效的;点表头可以升序/降序切换。
| 对比维度 | 前端筛选 | 后端搜索 |
|---|---|---|
| 数据来源 | 已经加载到浏览器里的数据 | 服务器数据库里的数据 |
| 在哪执行 | 浏览器(JS 操作数组) | 服务器(跑 SQL 查询) |
| 响应速度 | 毫秒级,不联网,飞快 | 依赖网络,慢一些 |
| 数据量限制 | 建议 ≤ 2000 条 | 无限制 |
| 典型场景 | 管理后台小列表 | 电商搜索、海量用户 |
记个口诀:数据少用前端切,数据多用后端查;页面不刷新、不发请求的是前端,要发请求、等接口的是后端。本页演示是前端筛选——数据早就躺在浏览器的数组里了,点"筛选"只是在内存里重新挑一遍,所以秒出结果。
filter // 筛出符合条件的元素,返回【新数组】(不改原数组)→ 挑出留下的,原数组不动 find // 找出【第一个】符合条件的元素(只返回一个)→ 只找一个 some // 有没有【至少一个】符合 → true / false → 有一个满足就行 every // 是不是【所有】都符合 → true / false → 全都满足才算 sort // 排序(⚠️ 会【改原数组】,记得先拷贝)→ 直接在原数组上排,危险 // 单条件:年龄 ≥ 30 → 只要 30 岁及以上 const result = users.filter(user => user.age >= 30); // 多条件叠加:北京 且 年龄 ≥ 25 → 既要在北京、又要 25 岁以上 const result2 = users.filter(user => user.city === "北京" && user.age >= 25);
重点记住 filter 的脾气:它返回一个新数组,原数组一动不动,这正好适合"反复筛选"。而 sort 反过来——它直接改原数组,所以排序前一定要先拷贝一份,别把原始数据排乱了。
function applyFilters() { // 点"筛选"时执行
// 1. 先把所有条件从页面上收集下来 → 把用户填的东西都取出来
const keyword = $("#kw").val.trim(); // 姓名关键词(去掉两头空格)
const role = $("#roleSel").val(); // 选的角色
const status = $("#statusSel").val(); // 选的状态
// 2. 永远从【原始数据】开始,不从上一次的结果接着筛 → 每次都从头开始
let result = [...originalUsers]; // 先把原始数据整个复制一份当起点
// 3. 条件一个一个叠加:哪个条件填了,就用它再筛一遍 → 剥洋葱,填了哪个剥哪层
if (keyword) result = result.filter(u => u.name.includes(keyword)); // 填了关键词:再按姓名筛
if (role) result = result.filter(u => u.role === role); // 填了角色:再按角色筛
if (status !== "") result = result.filter(u => u.status === parseInt(status)); // 选了状态:再按状态筛(转成数字再比)
// 4. 渲染 → 把最终结果画到表格上
renderTable(result);
}
这套"条件累加"是面试常考、工作常用的标准写法。思路就像剥洋葱:先拿全部数据,填了哪个条件就剥一层,没填的就跳过。用 if (条件) 判断"这个条件到底生不生效",生效才 filter,不生效就原样保留。这样不管用户填了 1 个条件还是 3 个条件,代码都不用改,自动适配。
const result = originalUsers // 从原始数据开始 .filter(u => !filters.keyword || u.name.includes(filters.keyword)) // 关键词没填就直接放行,填了才按姓名筛 .filter(u => !filters.role || u.role === filters.role) // 角色没填就放行,填了才按角色筛 .filter(u => filters.status === "" || u.status === parseInt(filters.status)); // 状态没选就放行,选了才按状态筛
这是把"条件为空就放行"用短路运算写进 filter 里:!条件 || 判断。意思是——如果条件是空的(!条件 为 true),整个表达式直接 true,这条就放行;只有条件非空时,才去做真正的判断。看不懂没关系,3.4 的 if 累加写法是首选,这段了解即可。
let currentSort = { field: "", order: "" }; // 记录当前按哪列、升还是降 → 记住现在排的是哪列、正还是反
function sortData(data, field, order) { // 传进数据、排哪列、升还是降
return data.sort((a, b) => { // 开始两两比较排序
if (typeof a[field] === "string") { // 如果这一列是文字(比如姓名)
// 字符串:用 localeCompare 按字典序(中文也能正确比)→ 文字不能相减,得用专门的比较法
return order === "asc" ? a[field].localeCompare(b[field]) : b[field].localeCompare(a[field]); // 升序正着比,降序反着比
}
// 数字:直接相减(升序 a-b,降序 b-a)→ 数字直接减就行
return order === "asc" ? a[field] - b[field] : b[field] - a[field]; // 升序小在前,降序大在前
});
}
排序的小规矩:点同一个表头就反向(升变降、降变升),点新表头默认升序。字符串(比如姓名)不能直接用减法,要用 localeCompare,否则中文会排错。还有,排序前先 [...数组] 拷贝一份,因为 sort 会改原数组,别把原始数据排乱了。
$.ajax({ // 找后端要筛选结果
url: "/api/users/search", // 搜索接口地址
type: "GET", // 用 GET 方式,参数拼在网址后面
data: { // 把用户填的条件打包发过去
keyword: params.keyword, // 关键词
role: params.role, // 角色
page: params.page || 1, // 第几页,没传就默认第 1 页
pageSize: params.pageSize || 10 // 每页几条,没传就默认 10 条
},
success: function (res) { // 后端把结果送回来
if (res.code === 0) { // 0 表示成功
renderTable(res.data.list); // 把后端返回的这页列表画出来
renderPagination(res.data.total); // 后端还给了总条数,据此画分页
}
}
});
后端搜索时,前端只是个"传话筒":把用户填的条件拼成 query 参数发给服务器,服务器在数据库里跑 SQL,只把符合条件的结果返回给前端渲染。前端不负责筛选逻辑本身。
// utils/filter.js —— 用户列表、商品列表、订单列表都能复用 → 写一次到处用
function applyFilters(data, filters) { // 传数据 + 一组筛选条件
return data.filter(item => { // 逐个数据挑
return filters.every(filter => { // 必须【全部】条件都满足才算留下
const { field, type, value } = filter; // 这条条件:筛哪个字段、用什么方式、值是多少
if (value === "" || value === null || value === undefined) return true; // 空条件放行 → 没填就不算数
const v = item[field]; // 取出这条数据对应字段的值
switch (type) { // 看这条条件是哪种筛法
case "like": // 模糊搜索(包含)
return String(v).toLowerCase.includes(String(value).toLowerCase); // 都转成小写再比包含
case "equal": // 完全相等
return v === value; // 值必须一模一样
case "range": // 数值范围
return value[0] <= v && v <= value[1]; // 在最小值和最大值之间
case "dateRange": // 日期范围
return v >= value[0] && v <= value[1]; // 在起始日期和结束日期之间
case "in": // 在某几个选项里
return value.includes(v); // 值只要在这个候选列表里就行
default: // 不认识的筛法
return true; // 一律放行
}
});
});
}
// 页面只需要配置:筛哪个字段、用什么方式、值从哪来 → 不用重写逻辑,配一下就行
const userConfig = [
{ field: "name", type: "like", getValue: => $("#kw").val.trim }, // 姓名:模糊搜,取输入框
{ field: "role", type: "equal", getValue: => $("#roleSel").val } // 角色:完全相等,取下拉框
];
这就是"逻辑迁移":变的是字段,不变的是筛选逻辑。把筛选逻辑写成一个通用函数,新页面只要配一下"筛哪个字段、怎么筛"就行,不用重写一遍。工作里写多了你就会不自觉地这么抽。
① 可能在什么地方用:后台用户管理(按角色/状态/关键词筛)、商品列表(按分类/价格区间/销量排序)、订单列表(按状态/时间范围筛)——凡是"表格上带筛选框和排序表头"的页面都是这套。
② 常见的问题:越筛越少(在已筛结果上接着筛,没用原始数据);排序后原始数据被改乱(忘了 slice/拷贝 再 sort);中文排序乱掉(用了减法而不是 localeCompare);状态是数字 0/1 却拿字符串比("0" !== 0);条件填了空格没生效(忘了 trim)。
③ 解决思路:在 applyFilters 里把收集到的三个条件和最终 result 的长度打出来,看是不是从原始数据开始筛的;排序前 console.log 一下原数组,确认没被改;数字字段记得 parseInt 转一下再比;筛选没结果时,先单独测一个条件,看是哪个条件把数据全干掉了。