在Web表单开发中,下拉框联动指的是页面中后一个选择框的内容依赖于前一个选择框的当前值。比如选择了省份之后,城市下拉框只展示该省下属的城市。这种交互用原生JavaScript就能完成,核心在于监听前一个下拉框的变更事件,并动态重写后一个下拉框的option列表。

一、联动的基本原理
HTML中的<select>元素代表一个下拉框,其中的每一个选项用<option>标签定义。当用户改变下拉框的选中项时,浏览器会触发change事件。我们可以在JS中通过addEventListener给第一个下拉框绑定change事件处理函数,在函数内部读取其value,再决定第二个下拉框要显示哪些option。
实现联动通常有两种思路。第一种是直接操作DOM,每次变更时先清空后一个select的子节点,然后循环创建新的option并追加进去。第二种是把数据抽象成JS对象或数组,变更时根据前值过滤数据,再统一渲染。对于简单场景,直接操作option足够清晰;数据量大或层级多时,数据驱动方式更好维护。
1.1 事件监听与取值
要拿到用户选中的值,可以使用select元素的value属性,它返回当前选中option的value。如果option没有显式写value,则返回其文本内容。在事件回调里,this一般指向被绑定的select本身,也可以用event.target获取。
注意要给第一个下拉框设置默认的提示选项,比如“请选择”,其value设为空字符串。这样在页面初始化时,后一个下拉框可以处于禁用或空白状态,避免显示出不属于任何前值的默认数据。
二、两级联动的代码实现
下面用一个省和市的例子演示原生JS联动。数据用对象嵌套表示,键是省份名,值是城市数组。当省份变化,就清空城市框并填入对应数组。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>下拉框联动示例</title>
</head>
<body>
<label>省份:
<select id="province">
<option value="">请选择</option>
<option value="gd">广东</option>
<option value="zj">浙江</option>
</select>
</label>
<label>城市:
<select id="city">
<option value="">请先选择省份</option>
</select>
</label>
<script>
// 数据源:省份代码对应城市列表
var cityData = {
gd: ['广州', '深圳', '珠海'],
zj: ['杭州', '宁波', '温州']
};
var provinceSelect = document.getElementById('province');
var citySelect = document.getElementById('city');
provinceSelect.addEventListener('change', function () {
var pVal = this.value;
// 清空城市下拉框
citySelect.innerHTML = '';
if (!pVal) {
var tip = document.createElement('option');
tip.value = '';
tip.textContent = '请先选择省份';
citySelect.appendChild(tip);
return;
}
var cities = cityData[pVal];
cities.forEach(function (c) {
var opt = document.createElement('option');
opt.value = c;
opt.textContent = c;
citySelect.appendChild(opt);
});
});
</script>
</body>
</html>
上面的代码在change事件里先用innerHTML清空citySelect,然后根据province的value从cityData中取数组,用createElement生成option。这种方式兼容性好,逻辑直观,适合不需要框架的小型页面。
如果担心频繁操作DOM带来性能问题,也可以先把城市数据拼成字符串再用innerHTML一次性写入。不过对于几个到几十个选项而言,差异可以忽略。重点是保证每次变更都完整重置后一个下拉框,而不是在原有option后面追加,否则会出现数据堆叠的错误。
2.1 初始状态与回显
在表单编辑页,可能需要根据已有数据回显。做法是页面加载后,先设置provinceSelect.value为目标省份,然后手动调用一次change处理逻辑,或者直接调用封装好的renderCity函数,再设置citySelect.value为目标城市。
为了避免重复代码,建议把填充城市的逻辑提取成独立函数,例如renderCity(provinceCode),change事件只负责取当前值并调用它。这样初始化回显和交互更新走同一套逻辑,降低出错概率。
三、多级联动与异步数据
当联动层级超过两级,比如省、市、区,可以采用链式调用:每一级变更都触发下一级的渲染,并把更下级的框重置。如果数据来自后端接口,则在change事件里用fetch或XMLHttpRequest请求,拿到结果后再渲染,同时要注意处理加载中的禁用状态。
// 假设根据省份代码请求城市
function loadCity(pid) {
citySelect.disabled = true;
citySelect.innerHTML = '<option>加载中...</option>';
fetch('https://ipipp.com/api/cities?pid=' + pid)
.then(function (res) { return res.json(); })
.then(function (list) {
citySelect.innerHTML = '';
list.forEach(function (item) {
var o = document.createElement('option');
o.value = item.code;
o.textContent = item.name;
citySelect.appendChild(o);
});
citySelect.disabled = false;
})
.catch(function () {
citySelect.innerHTML = '<option>加载失败</option>';
citySelect.disabled = false;
});
}
异步联动时要给用户明确反馈,比如禁用下拉框或显示“加载中”。另外,如果用户快速切换省份,可能产生竞态:先发的请求后返回,覆盖了新选择。可以在请求前记录一个序号,回调里判断序号是否最新,或者利用AbortController取消旧请求。
从架构上看,联动本质是前驱状态驱动后续视图。原生JS适合轻量需求;若项目已用Vue或React,则用数据绑定更自然,但理解底层change加DOM操作有助于排查框架下的渲染异常。无论哪种方式,保证数据源单一、更新路径清晰,是避免联动混乱的关键。
四、常见错误与排查
第一类错误是忘记清空旧option,导致选项累加。第二类是把option的文本内容当value使用,却在前端用中文去匹配数据键,一旦文本有空格就失效。第三类是在循环里用闭包引用循环变量,造成事件绑定错乱,不过用forEach或let声明可规避。
排查时可在change回调首行打印this.value确认事件已触发,再打印过滤后的数据长度。若界面没变,检查是否选错了select引用,或CSS把下拉框隐藏。对于动态插入的option,也要确认没有在外部用原生HTML写死了一份静态列表,导致JS清空后又被缓存或其他脚本填充。