级联下拉需用 data-level 和 data-parent 标记依赖关系,change 时先清空后续 select 并禁用,再异步填充;地区数据宜用扁平数组 filter 查询,避免嵌套 JSON;iOS Safari 中 disabled 不可靠,须配合 pointer-events 或移除事件监听器。

级联下拉的 DOM 结构必须用 data-parent 或 data-level 标记依赖关系
浏览器原生 select 不自带级联逻辑,必须靠 JS 主动控制显隐和选项填充。如果只是把省、市、区三个 select 并排放,不加任何标记,JS 就无法知道“当前选中的是第几级”或“该清空后面哪几个”。常见错误是靠 DOM 顺序硬编码(比如“第二个 select 一定对应市”),一旦页面结构微调就全崩。
推荐做法是统一加 data-level 属性,并让后一级明确声明 data-parent:
<select id="province" data-level="1"></select> <select id="city" data-level="2" data-parent="province"></select> <select id="district" data-level="3" data-parent="city"></select>
这样 JS 可以通过 document.querySelectorAll('[data-level="2"]') 找到所有二级控件,再用 el.dataset.parent 知道它依赖谁,后续增删层级也只需改属性,不动逻辑。
change 事件里必须先清空后续所有下拉,再异步加载新选项
用户从“广东省”切到“浙江省”,如果不先清空 city 和 district,就会残留旧数据,甚至出现“浙江 → 深圳 → 南山区”这种错乱组合。更隐蔽的问题是:如果城市列表是异步请求的,而用户快速连点两次省份,第二次请求返回可能覆盖第一次的结果,导致城市选项和当前选中的省不匹配。
立即学习“前端免费学习笔记(深入)”;
关键操作顺序如下:
- 监听
province的change事件 - 立即执行
clearNextSelects('province')—— 这个函数根据data-parent找到所有后续select,并清空其innerHTML、重置value、设为disabled - 再发请求获取城市数据,成功后只填充
city,不碰district -
city填完后,自身再触发一次change(可选),但需加防抖或标记避免死循环
地区数据建议用扁平数组 + filter() 而非嵌套 JSON
很多人一上来就设计成 { "广东": { "深圳": ["南山", "福田"], "广州": [...] } } 这种嵌套结构,看似直观,但实际操作中极易出错:键名大小写、空格、中英文括号、历史遗留别名(如“内蒙” vs “内蒙古”)都会让 obj[provinceName] 返回 undefined。而且前端做搜索、模糊匹配、分页时,嵌套结构要反复 Object.keys() 和 for...in,性能差还难调试。
更稳妥的方式是用一个扁平数组:
const regions = [
{ code: '440300', name: '深圳', parentCode: '440000' },
{ code: '440100', name: '广州', parentCode: '440000' },
{ code: '440000', name: '广东省', level: 1 }
];
查某省下的所有市,直接 regions.filter(item => item.parentCode === selectedProvinceCode),查名字也支持 .some(item => item.name.includes(keyword)),兼容性好,也方便和服务端 API 对齐。
移动端 Safari 下 select 的 disabled 状态可能不生效
在 iOS Safari 中,给 select 设置 disabled 后,视觉上变灰了,但用户仍能点开并选择——这是 WebKit 的已知 bug。单纯靠 disabled 阻止交互不可靠。
必须配合以下任一方式:
- 用
pointer-events: none+opacity: 0.5CSS 强制禁用点击 - 在
change事件处理器开头加守卫:if (el.disabled) return; - 更彻底的做法:清空后续
select时,同时移除其onchange监听器,加载完新数据后再重新绑定
这个细节在桌面端几乎没影响,但只要上线就大概率被 iOS 用户反馈,且复现稳定。



















