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