)
select下拉選項(xiàng)的selectedIndex屬性你用過(guò)嗎前陣子在某前端群里看到一個(gè)提問(wèn)明明給下拉框設(shè)置了value頁(yè)面也渲染出了對(duì)應(yīng)的option為什么提交表單時(shí)卻拿不到預(yù)期選項(xiàng)問(wèn)題最后定位到一行很老實(shí)的代碼——有人用selectedIndex做了初始定位又在另一個(gè)事件里改了value兩套邏輯互相覆蓋。評(píng)論區(qū)有人說(shuō)這個(gè)API都快被忘記了。我愣了一下仔細(xì)想想確實(shí)這些年框架用多了原生select上還有多少人在意selectedIndex這個(gè)屬性它到底是干什么的和value比優(yōu)勢(shì)在哪為什么動(dòng)態(tài)下拉里最常翻車的也是它這篇文章想把這家伙重新翻出來(lái)聊透順便把我在幾個(gè)項(xiàng)目里踩過(guò)的坑、寫(xiě)過(guò)的輪子一起交個(gè)底。1. selectedIndex與value到底差了點(diǎn)啥兩個(gè)API的實(shí)質(zhì)分工1.1 從DOM結(jié)構(gòu)理解selectedIndex的底層邏輯很多人第一眼看到selectedIndex覺(jué)得它不就是“選中了第幾項(xiàng)”嘛讀一下返回個(gè)數(shù)字寫(xiě)一下選中對(duì)應(yīng)項(xiàng)。這么說(shuō)沒(méi)錯(cuò)但它底層的工作方式比這更有意思。select元素在DOM里不是孤立存在的它下面掛著一個(gè)options集合也就是HTMLCollection。selectedIndex本質(zhì)上是“當(dāng)前選中項(xiàng)在這個(gè)集合里的整數(shù)下標(biāo)”。選中第一項(xiàng)就是0第二項(xiàng)就是1沒(méi)有選中任何一項(xiàng)就是-1。這種設(shè)計(jì)是瀏覽器最早實(shí)現(xiàn)下拉框交互時(shí)就定下來(lái)的一切操作圍繞集合的下標(biāo)展開(kāi)而不是圍繞選項(xiàng)的value展開(kāi)。這里有個(gè)容易混淆的細(xì)節(jié)value屬性也是select的一部分它返回的是“選中項(xiàng)option的value值”。但select本身沒(méi)有直接的value存儲(chǔ)它所有狀態(tài)都是通過(guò)“哪一項(xiàng)被選中”反映出來(lái)的。你讀select.value瀏覽器底層查的還是selectedIndex你寫(xiě)select.value瀏覽器做的也是遍歷options找到對(duì)應(yīng)value的那個(gè)option然后把它選中。所以在寫(xiě)代碼時(shí)value是“語(yǔ)義化”的selectedIndex是“結(jié)構(gòu)性”的。1.2 為什么說(shuō)selectedIndex在有些場(chǎng)景更可靠既然value這么語(yǔ)義化為什么還要用selectedIndex我的體會(huì)是value在兩種情況下容易“失真”。第一種是選項(xiàng)的value有重復(fù)。HTML規(guī)范里其實(shí)沒(méi)禁止多個(gè)option擁有相同的value值。一旦兩個(gè)選項(xiàng)value一樣select.value就分不清你到底選中了哪個(gè)。這時(shí)候只有selectedIndex能精確表達(dá)位置。第二種是選項(xiàng)根本沒(méi)有value。比如你直接寫(xiě)了option北京/option沒(méi)給value屬性。這種情況option.value返回的是文本內(nèi)容即“北京”。看著沒(méi)問(wèn)題但如果文本里有特殊字符、空格、或者后續(xù)顯示文案變了value跟著變后端存儲(chǔ)就可能受污染。而selectedIndex是不關(guān)心文本的它只管位置位置穩(wěn)定它就穩(wěn)定。我在做模擬項(xiàng)目X的表單頁(yè)時(shí)遇到過(guò)一個(gè)需求用戶在下拉里選學(xué)歷選項(xiàng)有“初中”“高中”“大?!薄氨究啤薄按T士”“博士”。產(chǎn)品要求只允許選“大?!奔耙陨系陀凇按髮!睍r(shí)彈提示并回退到默認(rèn)“大專”。如果我用value判斷還要維護(hù)一個(gè)等級(jí)對(duì)照表用selectedIndex直接比較下標(biāo)就一行if。這種“位置即業(yè)務(wù)等級(jí)”的場(chǎng)景selectedIndex反而比value更直接。1.3 讀寫(xiě)規(guī)則和瀏覽器差異讀selectedIndex很簡(jiǎn)單直接拿屬性值。寫(xiě)的規(guī)則也直接賦值整數(shù)即可。這里要注意幾點(diǎn)賦一個(gè)超過(guò)options長(zhǎng)度的值瀏覽器靜默忽略不會(huì)報(bào)錯(cuò)選中態(tài)保持原樣。賦負(fù)數(shù)在絕大多數(shù)瀏覽器里會(huì)被當(dāng)成取消選擇selectedIndex變成-1。但舊版本瀏覽器表現(xiàn)不一有的會(huì)把-1當(dāng)成不合法值直接忽略。若select有multiple屬性selectedIndex只返回第一個(gè)被選中的索引不能列舉全部。多選場(chǎng)景要用selectedOptions或遍歷options逐個(gè)看selected。這些邊界問(wèn)題平時(shí)不碰沒(méi)啥一旦碰上就會(huì)感受到“結(jié)構(gòu)API”的擰巴。我自己的習(xí)慣是單選下拉優(yōu)先用selectedIndex處理邏輯需要拿給后端的數(shù)據(jù)時(shí)再?gòu)倪x中項(xiàng)身上取value。分開(kāi)用反而兩邊都清爽。2. 讀selectedIndex的高頻業(yè)務(wù)場(chǎng)景聯(lián)動(dòng)、向?qū)?、列表選中態(tài)2.1 聯(lián)動(dòng)下拉為什么用索引比value省心聯(lián)動(dòng)下拉是selectedIndex出現(xiàn)最多的場(chǎng)景。比如選省份聯(lián)動(dòng)城市選一級(jí)分類聯(lián)動(dòng)二級(jí)分類。傳統(tǒng)寫(xiě)法是監(jiān)聽(tīng)change事件拿到當(dāng)前選中的省份再去請(qǐng)求或從本地?cái)?shù)據(jù)里篩城市。用value寫(xiě)的話代碼長(zhǎng)這樣const provinceSelect document.querySelector(#province); const citySelect document.querySelector(#city); provinceSelect.addEventListener(change, () { const provinceValue provinceSelect.value; citySelect.innerHTML ; const cities cityMap[provinceValue] || []; cities.forEach(city { const opt document.createElement(option); opt.value city.code; opt.textContent city.name; citySelect.appendChild(opt); }); });用selectedIndex寫(xiě)則可以完全不依賴value里的業(yè)務(wù)編碼provinceSelect.addEventListener(change, () { const idx provinceSelect.selectedIndex; citySelect.innerHTML ; const cities cityList[idx] || []; // 后續(xù)渲染邏輯一致 });兩種方式都能跑。差別在哪在數(shù)據(jù)結(jié)構(gòu)的穩(wěn)定性上。如果后端接口給的城市數(shù)據(jù)是按省份數(shù)組的索引對(duì)齊的用value反而要做一次indexOf查找。更關(guān)鍵的是當(dāng)value的編碼規(guī)則調(diào)整比如從數(shù)字編碼改成省份拼音如果你只在展示層用value去當(dāng)下標(biāo)后續(xù)維護(hù)成本一下就上來(lái)了。直接用selectedIndex對(duì)應(yīng)數(shù)組下標(biāo)結(jié)構(gòu)和數(shù)據(jù)源對(duì)齊改數(shù)據(jù)時(shí)幾乎不用動(dòng)邏輯。2.2 步驟向?qū)Ш捅韱畏植狡饔涀∮脩糇叩侥囊徊阶鲞^(guò)多步表單的人應(yīng)該都碰過(guò)“當(dāng)前步驟指示器”。有些產(chǎn)品用下拉框來(lái)切換步驟尤其是批量配置場(chǎng)景選第幾步就展示對(duì)應(yīng)步驟的區(qū)塊。這種地方用selectedIndex記錄步驟位置再合適不過(guò)。我做過(guò)一個(gè)某跨平臺(tái)系統(tǒng)的配置向?qū)ё髠?cè)是一個(gè)select右側(cè)是步驟面板。用戶切到第3步右側(cè)顯示第3步內(nèi)容還要記住他是否訪問(wèn)過(guò)第4步。這時(shí)候我用selectedIndex作為“步驟指針”配合一個(gè)數(shù)組去記錄每個(gè)步驟的完成狀態(tài)const stepSelect document.querySelector(#stepSelector); const stepStatus [false, false, false, false, false]; stepSelect.addEventListener(change, () { const current stepSelect.selectedIndex; stepStatus[current] true; renderStepPanel(current); updateVisitedFlag(stepStatus); });如果用value來(lái)記錄就得定義step-1、step-2這類約定還得解析字符串。selectedIndex天然就是數(shù)字跟數(shù)組下標(biāo)、步驟編號(hào)天然同構(gòu)少掉了整個(gè)parse環(huán)節(jié)。這個(gè)場(chǎng)景里它就是最順手的那把刀。2.3 表格行選中與下拉選項(xiàng)的同步還有一個(gè)我近期才摸索出來(lái)的妙用下拉列表和表格行選中做雙向同步。比如頁(yè)面上有一個(gè)表格每行數(shù)據(jù)對(duì)應(yīng)下拉里的一個(gè)選項(xiàng)用戶既可以直接在下拉里選也可以點(diǎn)擊表格行切狀態(tài)。下拉被程序化切換到某行時(shí)我需要高亮對(duì)應(yīng)表格行。這時(shí)selectedIndex的價(jià)值在于它本身就是“行號(hào)”。我只需要tableRowClickHandler(rowIndex) { mySelect.selectedIndex rowIndex; highlightTableRow(rowIndex); } mySelect.addEventListener(change, () { const row mySelect.selectedIndex; highlightTableRow(row); });不需要維護(hù)“選中的業(yè)務(wù)主鍵 - 行號(hào)”的映射關(guān)系。第一版我用的是valueid映射后來(lái)發(fā)覺(jué)純屬多余因?yàn)橄吕樞蚝捅砀耥樞蚴峭粋€(gè)數(shù)據(jù)源渲染出來(lái)的索引天然一一對(duì)應(yīng)。這個(gè)場(chǎng)景讓我對(duì)selectedIndex徹底改觀它不是老掉牙的舊API而是恰到好處的輕量工具。3. 動(dòng)態(tài)增刪option后selectedIndex為什么最容易翻車3.1 一次實(shí)打?qū)嵉牟瓤酉雀膐ptions再賦值下標(biāo)對(duì)不上了有一回做一個(gè)多級(jí)篩選器邏輯是第一個(gè)下拉選分類第二個(gè)下拉的選項(xiàng)根據(jù)分類動(dòng)態(tài)生成。為了用戶體驗(yàn)我保留了用戶之前選中過(guò)的二級(jí)選項(xiàng)切換一級(jí)分類時(shí)先備份當(dāng)前二級(jí)索引重新生成選項(xiàng)后嘗試恢復(fù)。代碼如下const backupIndex secondSelect.selectedIndex; buildOptions(secondSelect, newList); secondSelect.selectedIndex backupIndex;看上去沒(méi)問(wèn)題但實(shí)測(cè)中發(fā)現(xiàn)一個(gè)詭異現(xiàn)象有時(shí)候恢復(fù)的選項(xiàng)和用戶之前看到的不是同一個(gè)。排查半天發(fā)現(xiàn)問(wèn)題不在恢復(fù)那一步而在備份那一步——我在重新生成選項(xiàng)之前備份的是舊列表的索引可舊列表和新列表的內(nèi)容排序完全不一樣。比如舊列表第3項(xiàng)是“廣東”新列表按拼音重排后第3項(xiàng)變成了“北京”。索引位置一樣但含義完全不同。這就是典型的下標(biāo)體系被打破的案例。selectedIndex是位置敏感的一旦options數(shù)組結(jié)構(gòu)變化舊索引就失效了。解決方案不是硬恢復(fù)索引而是備份業(yè)務(wù)主鍵重建后先按主鍵找新位置再賦值const backupValue secondSelect.value; buildOptions(secondSelect, newList); restoreSelectByValue(secondSelect, backupValue);專門(mén)寫(xiě)了一個(gè)restore函數(shù)里面遍歷options找匹配value。這函數(shù)前面說(shuō)過(guò)value失效場(chǎng)景很少所以在動(dòng)態(tài)列表里value更可靠。這件事之后我的紅線是靜態(tài)列表用selectedIndex順手動(dòng)態(tài)列表優(yōu)先用value。3.2 HTMLCollection實(shí)時(shí)刷新帶來(lái)的“隱式位移”另一個(gè)更隱蔽的坑options是個(gè)實(shí)時(shí)集合你對(duì)它做增刪操作索引會(huì)立刻重新編號(hào)。比如當(dāng)前選中第5項(xiàng)下標(biāo)4此時(shí)刪掉第1項(xiàng)你選中的項(xiàng)自動(dòng)變成第4項(xiàng)下標(biāo)3selectedIndex跟著變。這個(gè)行為本身符合直覺(jué)但如果你沒(méi)有意識(shí)到“索引是實(shí)時(shí)變化的”就容易寫(xiě)錯(cuò)邏輯。比如const delIndex select.selectedIndex; select.remove(delIndex); // 此時(shí)如果刪的是前面項(xiàng)原選中項(xiàng)下標(biāo)已經(jīng)變了如果在刪除后立刻用舊的delIndex去處理原選中項(xiàng)拿到的一定是另一項(xiàng)或undefined。我的做法是凡是涉及刪除后要拿到原選中項(xiàng)先保存元素引用const selectedOption select.options[select.selectedIndex]; select.remove(select.selectedIndex); // 后續(xù)處理用selectedOption而不是索引類似的問(wèn)題也會(huì)出現(xiàn)在appendChild插入選項(xiàng)時(shí)。插入位置靠前后面所有選項(xiàng)索引都往后挪此時(shí)selectedIndex如果沒(méi)重設(shè)雖然指向的option沒(méi)變因?yàn)镈OM引用沒(méi)變但與用戶視覺(jué)上的“當(dāng)前選中順序”就出現(xiàn)了不一致。交互層觸發(fā)的事件和選中態(tài)的對(duì)齊需要手動(dòng)處理。3.3 空列表和重置時(shí)機(jī)的配合問(wèn)題動(dòng)態(tài)下拉還有個(gè)常見(jiàn)要求當(dāng)上級(jí)選項(xiàng)切換后下級(jí)列表清空并處于“請(qǐng)選擇”狀態(tài)。清空操作一般是innerHTML此時(shí)selectedIndex自動(dòng)變成-1。這操作沒(méi)問(wèn)題問(wèn)題在某些人喜歡在后面手動(dòng)補(bǔ)一個(gè)“請(qǐng)選擇”選項(xiàng)然后選中它select.innerHTML option value請(qǐng)選擇/option; select.selectedIndex 0;這么寫(xiě)沒(méi)問(wèn)題。但如果是先appendChild加了一個(gè)“請(qǐng)選擇”再立刻調(diào)用select.value嘗試選中它在某些瀏覽器里會(huì)因?yàn)関alue是空字符串且option的value屬性也是空字符串而命中但在另一些瀏覽器里select.value賦值空字符串可能不觸發(fā)選中邏輯。我后來(lái)統(tǒng)一用selectedIndex0來(lái)完成這個(gè)“重置到占位項(xiàng)”的動(dòng)作完全不依賴value跨瀏覽器穩(wěn)定得多。這類問(wèn)題不細(xì)測(cè)很難發(fā)現(xiàn)尤其當(dāng)你在一個(gè)復(fù)雜的表單里同時(shí)用了多種重置方式出bug時(shí)根本不知道該懷疑誰(shuí)。建議團(tuán)隊(duì)里約定一種固定的重置寫(xiě)法少讓成員自由發(fā)揮。4. selectedIndex的邊界情況和多選陷阱4.1 selectedIndex -1是真空態(tài)還是假空態(tài)selectedIndex為-1表示沒(méi)有任何選項(xiàng)被選中。這個(gè)值在業(yè)務(wù)里常用于表達(dá)“未選擇”狀態(tài)比如沒(méi)填完的表單項(xiàng)校驗(yàn)時(shí)判斷selectedIndex是否等于-1就知道用戶有沒(méi)有選過(guò)。但這里有一個(gè)讓人迷惑的點(diǎn)明明我設(shè)置了selectedIndex-1頁(yè)面上卻還能看到第一項(xiàng)是“選中”的高亮樣式。原因在于大部分瀏覽器的默認(rèn)行為如果select沒(méi)有選項(xiàng)或沒(méi)有選中項(xiàng)且沒(méi)有寫(xiě)required瀏覽器在用戶不操作時(shí)會(huì)把第一個(gè)可選項(xiàng)當(dāng)成默認(rèn)顯示項(xiàng)。這是瀏覽器“視覺(jué)回退”不是DOM狀態(tài)真的改了。我在某表單驗(yàn)證邏輯里就踩過(guò)下拉框是允許不選的我設(shè)置了selectedIndex-1然后校驗(yàn)通過(guò)結(jié)果用戶沒(méi)做任何操作提交時(shí)select.value取出來(lái)的卻是第一項(xiàng)的value后端也收到了導(dǎo)致一條不完整的記錄入庫(kù)。當(dāng)時(shí)的修復(fù)很簡(jiǎn)單校驗(yàn)時(shí)不只看selectedIndex還要看用戶有沒(méi)有真實(shí)交互過(guò)let userInteracted false; select.addEventListener(change, () { userInteracted true; }); // 提交時(shí) if (!userInteracted || select.selectedIndex -1) { // 當(dāng)作未選擇 }很多同學(xué)喜歡用“視覺(jué)上是否默認(rèn)選中第一項(xiàng)”來(lái)判斷有沒(méi)有選這個(gè)思路在原生select里是危險(xiǎn)的。視覺(jué)高亮不代表selectedIndex有值。如果必須讓用戶強(qiáng)制選擇建議options里顯式加一個(gè)disabled的“請(qǐng)選擇”占位項(xiàng)并且不要給它value。這樣能規(guī)避很多莫名其妙的默認(rèn)選中問(wèn)題。4.2 disabled選項(xiàng)會(huì)讓索引讀起來(lái)疑似“跳位”下拉里經(jīng)常有不可選項(xiàng)比如“該地區(qū)已停運(yùn)”。disabled的option依然占據(jù)一個(gè)索引位置但用戶永遠(yuǎn)無(wú)法選中它。selectedIndex讀出來(lái)的位置是“當(dāng)前選中的那個(gè)合法項(xiàng)的索引”。如果你以為“第一項(xiàng)就是下標(biāo)0”但第一項(xiàng)其實(shí)是disabled的占位項(xiàng)讀出來(lái)還會(huì)是0嗎實(shí)測(cè)如果第一項(xiàng)disabled且沒(méi)選中任何項(xiàng)selectedIndex在大部分瀏覽器里是-1但在用戶已經(jīng)選中第二項(xiàng)之后再通過(guò)程序把第一項(xiàng)改為disabledselectedIndex依然是第二項(xiàng)的索引比如1頁(yè)面視覺(jué)也保持第二項(xiàng)選中。這里不會(huì)出現(xiàn)位移只是你要注意渲染綁定索引時(shí)不要假設(shè)“options[i]一定可選”做高亮等邏輯前要主動(dòng)檢查option.disabled。另外有一種業(yè)務(wù)偽需求“用戶選中了A項(xiàng)后B項(xiàng)自動(dòng)不可選”。很多人會(huì)去改option的disabled屬性。這沒(méi)問(wèn)題但是disabled改動(dòng)不會(huì)觸發(fā)change事件用戶已經(jīng)選中的C項(xiàng)也不受影響。如果你希望通過(guò)狀態(tài)聯(lián)動(dòng)自動(dòng)切換選中項(xiàng)還是得手動(dòng)改selectedIndex。我在一個(gè)聯(lián)動(dòng)配置里就是先改disabled再判斷當(dāng)前選中項(xiàng)是否變成disabled如果是再selectedIndex往下一個(gè)可選項(xiàng)目移動(dòng)邏輯寫(xiě)起來(lái)比預(yù)想的多幾行但這些細(xì)節(jié)躲不過(guò)。4.3 多選select里讀selectedIndex的“誤導(dǎo)性”多選模式下selectedIndex返回的是第一個(gè)被選中項(xiàng)的下標(biāo)。聽(tīng)起來(lái)像是個(gè)便利功能實(shí)際上最容易被誤用。比如你寫(xiě)了const first mySelect.selectedIndex;下意識(shí)以為拿到了第一個(gè)選中項(xiàng)。其實(shí)如果用戶選的順序是第3項(xiàng)、第1項(xiàng)selectedIndex返回的是1因?yàn)橄聵?biāo)小的先被選中而不是3。你要是按“第一個(gè)被點(diǎn)選”的語(yǔ)義去理解就錯(cuò)了。多選場(chǎng)景我建議直接繞開(kāi)selectedIndex用Array.from(select.selectedOptions)拿所有選中項(xiàng)或者遍歷options過(guò)濾selected。這兩個(gè)API在多選里語(yǔ)義明確不會(huì)產(chǎn)生歧義。selectedIndex這個(gè)屬性天生是單選模型的產(chǎn)物放到多選里就只是個(gè)兼容性殘留別拿它當(dāng)業(yè)務(wù)邏輯的主干。5. 框架時(shí)代selectedIndex還剩下哪些用武之地5.1 React受控組件里為什么基本不用它React的受控select用的是value配合onChange事件。你寫(xiě)select value{city} onChange{...}React內(nèi)部會(huì)遍歷options去匹配value對(duì)應(yīng)的項(xiàng)并設(shè)置selected。這時(shí)候你如果自己再去動(dòng)selectedIndex麻煩就來(lái)了。受控組件要求“狀態(tài)唯一來(lái)源”。value存的是業(yè)務(wù)值selectedIndex只是渲染結(jié)果。如果你在事件里寫(xiě)了類似selectEl.selectedIndex 1React下次渲染時(shí)發(fā)現(xiàn)value還是原來(lái)的值會(huì)再用value重新設(shè)置選中態(tài)你手動(dòng)指定的索引就被覆蓋了。這就是標(biāo)題開(kāi)頭那個(gè)群里提問(wèn)的人遇到的問(wèn)題——他的value邏輯和selectedIndex邏輯互相打架因?yàn)镽eact的渲染循環(huán)會(huì)不斷用value“糾正”DOM。在React里如果需要用索引邏輯正確姿勢(shì)是把索引存進(jìn)state再通過(guò)value{options[idx]?.value}映射到受控屬性上。這樣索引只是業(yè)務(wù)計(jì)算的臨時(shí)量最終落到的還是value這個(gè)唯一狀態(tài)。硬要在React里用selectedIndex等于和框架的渲染模型對(duì)著干。5.2 Vue的v-model和原生selectedIndex的兼容經(jīng)驗(yàn)Vue這邊情況類似v-model對(duì)select的處理方式與React大同小異也是基于value做匹配。但在Vue的非受控組件或需要直接操作DOM的指令場(chǎng)景里selectedIndex還有空間。比如我寫(xiě)過(guò)一個(gè)自定義指令作用是“讓下拉支持通過(guò)程序按索引快速切換”指令內(nèi)部就直接操作selectedIndex不依賴框架狀態(tài)const directive { mounted(el, binding) { el.selectedIndex binding.value; }, updated(el, binding) { if (el.selectedIndex ! binding.value) { el.selectedIndex binding.value; } } };這種指令適用于“下拉項(xiàng)完全由靜態(tài)模板決定、value不承擔(dān)業(yè)務(wù)含義”的場(chǎng)合。比如一個(gè)純展示用的序號(hào)選擇器選項(xiàng)是“第1頁(yè)”“第2頁(yè)”這種用value反而要跟著頁(yè)面文案變。此時(shí)selectedIndex當(dāng)“頁(yè)碼指針”用非常順手。但要注意Vue更新DOM是異步的updated鉤子里拿到的el可能還在舊狀態(tài)。實(shí)測(cè)經(jīng)驗(yàn)多數(shù)情況下updated鉤子里直接賦值selectedIndex不會(huì)出錯(cuò)但如果選項(xiàng)數(shù)量在數(shù)據(jù)變化時(shí)同步增減了索引位置可能和應(yīng)用層期望不一致。穩(wěn)妥做法是在nextTick之后再做索引修正。這個(gè)坑不大但碰到一次就夠你記一輩子。5.3 無(wú)框架環(huán)境里selectedIndex還是最優(yōu)解之一如果項(xiàng)目還在用原生JS或者你只是在一個(gè)獨(dú)立的小組件里實(shí)現(xiàn)一個(gè)簡(jiǎn)單交互selectedIndex依然是最輕量的API。沒(méi)有框架的對(duì)比沒(méi)有響應(yīng)式的狀態(tài)流轉(zhuǎn)你要做的不過(guò)就是“選中第幾個(gè)”這件事一行代碼解決。比如我寫(xiě)過(guò)一個(gè)批量導(dǎo)入的預(yù)覽組件文件解析后得到的字段順序映射到下拉選項(xiàng)用戶調(diào)換字段時(shí)選中某個(gè)字段就對(duì)應(yīng)數(shù)據(jù)源列號(hào)。這個(gè)場(chǎng)景value完全無(wú)意義selectedIndex直接就是列號(hào)fieldSelect.addEventListener(change, () { const colIndex fieldSelect.selectedIndex; previewTable.dataset.columnMap colIndex; });代碼量少得不像話。你非要用value也不是不行但你得維護(hù)columnMap映射表純純給自己加戲。5.4 封裝一個(gè)安全的索引操作小工具為了兼顧各種場(chǎng)景我后來(lái)把自己常用的一套邏輯封裝成了一個(gè)極簡(jiǎn)函數(shù)專門(mén)處理“按索引選中”的安全操作function safeSetSelectedIndex(selectEl, index, fallbackIndex 0) { if (!selectEl) return -1; if (index 0 index selectEl.options.length) { selectEl.selectedIndex index; return index; } if (fallbackIndex 0 fallbackIndex selectEl.options.length) { selectEl.selectedIndex fallbackIndex; return fallbackIndex; } selectEl.selectedIndex -1; return -1; }它只做一件事檢查范圍賦索引失敗回退。進(jìn)入這個(gè)工具函數(shù)前業(yè)務(wù)邏輯先想清楚是備份索引還是備份value。如果是動(dòng)態(tài)列表我建議先備份value重建后再用value匹配索引最后調(diào)用safeSetSelectedIndex完成定位。這套組合拳寫(xiě)下來(lái)基本覆蓋了我遇到過(guò)的全部場(chǎng)景靜態(tài)索引、動(dòng)態(tài)變化、越界保護(hù)、回退兜底。自己團(tuán)隊(duì)里用它替換了大量繁瑣的if判斷出錯(cuò)率比之前手動(dòng)寫(xiě)低了不少。6. 幾個(gè)實(shí)戰(zhàn)命令和代碼片段可以直接抄6.1 輪播式切換下一項(xiàng)有一種需求是點(diǎn)擊按鈕切換到下拉的下一項(xiàng)循環(huán)滾動(dòng)。用selectedIndex寫(xiě)起來(lái)很順手document.querySelector(#nextBtn).addEventListener(click, () { const select document.querySelector(#demoSelect); const next (select.selectedIndex 1) % select.options.length; select.selectedIndex next; select.dispatchEvent(new Event(change)); });注意最后dispatchEvent那行程序化修改selectedIndex本身不觸發(fā)change事件。如果你后續(xù)邏輯依賴change記得手動(dòng)派發(fā)。這個(gè)細(xì)節(jié)我第一次也漏了導(dǎo)致點(diǎn)擊按鈕后UI變了但聯(lián)動(dòng)沒(méi)反應(yīng)。6.2 根據(jù)選中索引設(shè)置占位提示做表單時(shí)經(jīng)常遇到下拉為空需要顯示灰色提示選中后提示消失。selectedIndexCSS類可以這樣配合const select document.querySelector(#reqSelect); function updatePlaceholder() { select.classList.toggle(is-empty, select.selectedIndex -1); } select.addEventListener(change, updatePlaceholder); updatePlaceholder();CSS類單獨(dú)控制顏色和邊框不需要在每次渲染時(shí)寫(xiě)innerHTML。干凈利落。6.3 selectedIndex與鍵盤(pán)上移下移的配合原生select在聚焦?fàn)顟B(tài)下按上/下方向鍵就會(huì)自動(dòng)切換選中項(xiàng)而且會(huì)觸發(fā)change事件。在某些不支持原生樣式的自定義下拉組件里我們常常要模擬這個(gè)行為。簡(jiǎn)化版可以這樣寫(xiě)customSelect.addEventListener(keydown, (e) { if (e.key ! ArrowDown e.key ! ArrowUp) return; e.preventDefault(); const dir e.key ArrowDown ? 1 : -1; const nextIndex Math.min( Math.max(realSelect.selectedIndex dir, 0), realSelect.options.length - 1 ); realSelect.selectedIndex nextIndex; syncCustomUI(); });把真實(shí)但隱藏的select當(dāng)作狀態(tài)容器selectedIndex負(fù)責(zé)“當(dāng)前在哪一項(xiàng)”UI層負(fù)責(zé)畫(huà)出來(lái)數(shù)據(jù)層從options里取value。這個(gè)架構(gòu)在無(wú)框架環(huán)境下特別穩(wěn)我做了好幾個(gè)自定義下拉都沿用這個(gè)思路。7. 我用了這么多年最后留下三條經(jīng)驗(yàn)第一靜態(tài)結(jié)構(gòu)里大膽用selectedIndex動(dòng)態(tài)結(jié)構(gòu)里三思而后行。選項(xiàng)一旦可能增刪改排序索引就失去了穩(wěn)定性轉(zhuǎn)而以value備份為佳。第二程序改selectedIndex不會(huì)觸發(fā)change事件需要手動(dòng)派發(fā)或顯式調(diào)用業(yè)務(wù)函數(shù)。這個(gè)坑在原生環(huán)境極其高頻每次寫(xiě)完索引賦值先問(wèn)自己一句后續(xù)邏輯依靠事件嗎依靠就補(bǔ)上dispatch不依靠就確認(rèn)調(diào)用鏈。第三別在受控組件里跟框架搶方向盤(pán)。React或Vue都已經(jīng)把value作為狀態(tài)的唯一來(lái)源你在外面手動(dòng)動(dòng)selectedIndex大概率會(huì)被下一次渲染覆蓋。要玩索引就在state層玩映射成value再進(jìn)組件。如果你之前從沒(méi)認(rèn)真用過(guò)selectedIndex翻翻手頭的舊代碼把它從“曾經(jīng)用過(guò)但忘了”的收藏夾里撈出來(lái)弄懂它和value的邊界。這種老API不是沒(méi)用了它們只是被框架遮住了太久。真正需要的人會(huì)在某個(gè)深夜的聯(lián)動(dòng)下拉bug現(xiàn)場(chǎng)跟它重逢然后感嘆一句原來(lái)你一直在等我。