前言
最近遇到一個應用場景,用於檢測頁面是否加載過帶有某個特徵的 URL。實現原本很簡單:
var entries = performance.getEntriesByType('resource')
var ret = entries.some(function(v) {
return /xxx/.test(v.name)
})
但由於某些原因,要求代碼中不能出現 function
關鍵字。(箭頭函數不用考慮,語法兼容性就不行。至於 eval
之類的動態執行那就更不用提了)
也就是說,即使要有回調,函數也只能通過 JS 或 DOM 內置的 API 創建。
下面開始挑戰。
簡單但低效的方案
最先想到的方案非常簡單,根本不用函數。直接將 entries
數組序列化成字符串,一步到位:
var str = JSON.stringify(entries)
var ret = /xxx/.test(str)
不過該方案存在性能問題。由於 Performance API 記錄了大量信息,導致序列化的開銷非常大。
例如淘寶首頁,打開後往下翻幾頁,URL 記錄多達數百條,序列化用時數毫秒,字符串長度超過十萬。
函數式編程
回到主題,我們嘗試用 JS 或 DOM 內置的 API 創建回調函數。
回顧本文開頭的代碼:
var entries = performance.getEntriesByType('resource')
var ret = entries.some(function(v) {
return /xxx/.test(v.name) // 如何用 JS 內置的方法實現這個邏輯?
})
回調函數中的邏輯看似簡單,但實際上做了兩件事:讀取 name
屬性、調用 test
方法。
爲了方便理解,我們將這兩件事進行拆分,每次只做一件。
1.讀取屬性
我們將 entries
數組轉換成 urls
字符串數組,類似如下邏輯:
var urls = entries.map(function(v) {
return v.name
})
2.調用方法
在 urls
數組中搜索關鍵字,類似如下邏輯:
var ret = urls.some(function(v) {
return /xxx/.test(v)
})
下面開始逐一突破。
屬性讀取函數化
若想通過函數調用的方式讀取屬性,顯然需要用到 讀訪問器
,即 getter
。
Performance API 記錄中的 name
屬性定義於 PerformanceEntry
類,因此可通過如下方式獲取該屬性的 getter
:
var nameGetter = Object.getOwnPropertyDescriptor(PerformanceEntry.prototype, 'name').get
現在讀取屬性,即可拋棄 entries[n].name
的形式,換成函數調用的形式:
nameGetter.call(entries[n]) // "https://..."
是不是有種倒裝句的感覺?
我們把謂語放在最前,主語放在最後。因爲我們強調的是讀屬性這個行爲,而不是強調讀誰的。這個「誰」,可以指代數組中任何一個元素。
現在,我們代碼變成了這樣:
// 臨時版
var urls = entries.map(function(v) {
return nameGetter.call(v)
})
顯然,如果能直接將 nameGetter.call
傳給 map
回調,那麼 function
就可以去掉了。
// 這樣可以嗎?好像缺了什麼。。。
var urls = entries.map(nameGetter.call)
因爲 nameGetter.call
只是 Function.prototype.call
的一個引用,是不帶上下文的。
nameGetter.call === Function.prototype.call // true
好在數組的 map
方法還有 第二個參數,用於設定回調函數的 this
上下文。
於是,我們可以把 nameGetter
作爲 map
的第二個參數:
// 大功告成
var urls = entries.map(nameGetter.call, nameGetter)
成功得到所有記錄的 URL 數組!
調用方法
事實上,數組的迭代方法都支持設置 this 上下文。
因此,我們使用同樣的思路,實現字符串數組的正則搜索。例如:
var reg = /google/
urls.find(reg.test, reg) // "https://www.google.com/..."
或者使用 some
方法,直接判斷是否存在。
完整實現
var nameGetter = Object.getOwnPropertyDescriptor(PerformanceEntry.prototype, 'name').get
var entries = performance.getEntriesByType('resource')
var urls = entries.map(nameGetter.call, nameGetter)
var reg = /xxx/
urls.some(reg.test, reg)
實現很簡單,性能也很高,並且沒有出現任何一個字面函數。