如何用 JavaScript 生成 CSS Selector ?

realdennis
5 min readDec 28, 2018

--

這個題目算是有名的前端實作題,但第一次接觸時我是挺懵的,那時的我只知道用 class、id 可以選到元素,但從沒反過來用元素生成過 selector ,隨著後來接觸越深,自然也有類似的需求,諸如此類反查選擇器的函式庫也是存在的,但我想聊聊這個題目到底想考什麼。

如果有用過 recorder 記錄事件發生順序的擴充套件,肯定對生成唯一的選擇器不陌生,畫面如下。ex: puppeteer-recorder

此為 Puppeteer-recorder 的執行畫面。

其他類似的唯一選擇器函式庫有 unique-selectorsimmerjs

這個題目實在是很有意思,我當時只是搜尋完,然後照著做,會印出來,卻沒有自己思考過該怎麼做,這次就當做補交作業吧

重新命題:點擊元素後,將其選擇器印出

_cb = e => {
console.log(SOMETHING) // ??
}
document.querySelectorAll('*').forEach( dom => dom.onclick = _cb )

程式的一開始大概會長這個樣子,我們來思考一下常見的選擇器方法,元素標籤名、元素的 ID 、元素的 class,我們先來思考能不能針對這個元素拿出這些東西。

當然我們要先拿到觸發事件的元素,使用event.currentTarget

針對父元素,我們再用遞迴的方式把父元素傳到函數裡呼叫。(畢竟題目沒有要求時間複雜度,笑。)

針對標籤名可以用節點的屬性tagName ,而 ID 則一樣是透過id的屬性取出。可以看到目前程式的結構大概變成這個樣子:

_find = dom => {
//I can get tag name by dom.tagName
}
_cb = e => {
console.log(_find(e.currentTarget))
}
document.querySelectorAll('*').forEach( dom => dom.onclick = _cb )

那麼之後我們只針對 _find 函數作逐步介紹,就不在佔用程式碼的空間重複描述_cb跟事件註冊了。

有了標籤名 與 ID 接下來來解析一下class

class有幾點比較麻煩…

  1. class可以是多個的,如果一次把 className 內的字串取下,必須做一下字串處理,把空白 split 掉
  2. 有另一個 API 叫做 classList 做掉了這件事,但是這個 classList 沒有辦法使用陣列的 map 方法

我們選擇走第二條路,透過 Array.prototype.map 把 classList 給 call 進去。

Array.prototype.map.call(dom.classList, d=>'.'+d ).join('')
// .xxx.ooo

最後我們再判斷父元素是否存在,透過遞迴的方式拼接選擇器:父元素選擇器 > 子元素選擇器

完整程式碼如下:

寫成遞迴式子暴力又優美

終於可以來聊聊 這個題目到底想考些什麼了

有時候讀題目更多的是站在出題者的立場思考,對方想從答案裡面獲得些什麼。

事件的 currentTarget 與 target

可以注意到文章裡頭我們使用非常暴力的方式把每個元素都註冊事件,再取得 currentTarget 後,將事件停止傳遞。然而最終的版本我們直接把事件註冊在 document 上面,透過 target 去拿到實際觸發的位置。

currentTarget →需要額外付出遍尋的成本,增刪麻煩,需要額外控制事件停止傳遞。

target →未來移除監聽器成本較低

ID雖然是獨一無二 但還是會有人重複使用

這是一個蠻容易遇到的問題,如果原先專案重複使用,還是可以被選擇器拉出來。

實際上這真的有獨一無二嗎?

我們考慮下列的 HTML 結構

<div>
<p class="card">
<p class="card">

可以發現不管點擊哪個辨別出來的選擇器都會是 HTML>BODY>DIV>P.card,那麼我們該怎麼做才能確保選擇器獨一無二。

我們馬上可以發現,我們需要正確的 index 去判斷到底是第幾個 <p>,這時候有一個選擇器(其實上面的 puppeteer 已經暴雷了),正是:nth-child(index)

讓我們把正確的做法加上去吧!

快點按下 F12 貼上去看看點擊後會發生什麼事吧!

註:陣列的indexOf方法,由於陣列是從0開始,但是:nth-child 是從1開始,所以必須做+1這個 shift 的動作。(第八行)

又臭又長,讓人成就感滿滿

寫在最後

網路上查到的做法往往都是沒有加上:nth-child 的做法, 我在想會不會是這間出題的公司,故意流出來的錯誤答案呢?判斷寫題的人是不是抄襲之類的…。(陰謀論)

最後的最後,如果可以的話,還是把這個遞迴算法展開為一般的迴圈做法,這就作為看文者的作業吧!來寫一個時間複雜度低的算法。畢竟你交這份 code 出去,可能會讓主管偏北宋喔!

最後的最後的最後,再思考一件事,透過:nth-child 來定位的話,是否還需要 class 、 id 呢。

歡迎留言告訴我,需要不需要、不需要的理由、需要的理由。

--

--

No responses yet