在同一頁面多次使用同一套城市級聯選單元件時,若 HTML 結構中存在重複的 id 屬性,會導致 JavaScript 無法正確綁定事件、更新選項,進而使第二組選單(如「目的地」)失效。本文透過實際代碼分析問題根因,並提供完整的修復方案與最佳實踐。
原始表單代碼片段:
<!-- 出發地 -->
<label id="q_s1">
<select name="s" id="s1"><option>請選擇</option></select>
</label>
<label id="q_s2">
<select id="s2"><option>請選擇</option></select>
</label>
<!-- 目的地 -->
<label id="m_s1">
<select name="s2" id="s1"><option>請選擇</option></select><!-- 重複 id="s1" -->
</label>
<label id="m_s2">
<select id="s2"><option>請選擇</option></select><!-- 重複 id="s2" -->
</label>
核心問題:兩組選單的 select 元素使用了相同的 id="s1"、id="s2"。JavaScript 透過 document.getElementById('s1') 獲取元素時,只會返回文檔中第一個匹配的元素(出發地的選單),導致目的地選單無法被正確操作。
<!-- 出發地 -->
<select id="depart-province" name="depart_province">...</select>
<select id="depart-city" name="depart_city">...</select>
<!-- 目的地 -->
<select id="dest-province" name="dest_province">...</select>
<select id="dest-city" name="dest_city">...</select>
class CitySelector {
constructor(containerId, options = {}) {
this.container = document.getElementById(containerId);
this.provinceSelect = this.container.querySelector('[data-role="province"]');
this.citySelect = this.container.querySelector('[data-role="city"]');
this.init();
}
init() {
// 綁定省份變更事件,動態載入城市
this.provinceSelect.addEventListener('change', () => this.loadCities());
// 初始化省份選項
this.loadProvinces();
}
loadProvinces() { /* 從 city.js 獲取省份數據並填充 */ }
loadCities() { /* 根據選中省份獲取城市並填充 */ }
}
// 使用:兩個獨立容器,互不干擾
new CitySelector('depart-region');
new CitySelector('dest-region');
對應 HTML:
<div id="depart-region">
<select data-role="province"></select>
<select data-role="city"></select>
</div>
<div id="dest-region">
<select data-role="province"></select>
<select data-role="city"></select>
</div>
data-* 屬性而非 ID 選取元素:降低耦合,支援多實例。city.js 改為導出數據結構(如 ES Module export const cityData = {...}),而非直接操作 DOM,提高復用性。<label for="...">、ARIA 屬性,提升可用性。HTML id 屬性在文檔中必須唯一,這是 Web 標準的基本要求。多實例 UI 元件開發時,應採用「容器作用域 + 語義化選擇器」或「元件類封裝」模式,從根源上避免 ID 衝突。重構後的代碼不僅修復了 Bug,更大幅提升了可維護性與擴展性。