詳解如何讓JavaScript代碼不可斷點
繞過斷點
調(diào)試 JS 代碼時,單步執(zhí)行(F11)可跟蹤所有操作。例如這段代碼,每次調(diào)用 alert 時都會被斷住:
debugger alert(11) alert(22) alert(33) alert(44)
有沒有什么辦法能讓單步執(zhí)行失效,一次執(zhí)行多個操作?
事實上有一些巧妙的辦法。例如通過數(shù)組回調(diào)執(zhí)行這些 alert 函數(shù):
debugger [11, 22, 33, 44].forEach(alert)
這樣只有 forEach 之前和之后會被斷住,中間所有 alert 調(diào)用都不會被斷住。
由此可見,通過 內(nèi)置回調(diào) 執(zhí)行 原生函數(shù),調(diào)試器是無法斷住的!
利用這個特性,我們可將一些重要的操作隱藏起來,從而能在調(diào)試者眼皮下悄悄執(zhí)行。
應(yīng)用案例
主流瀏覽器的調(diào)試器允許攔截特定事件,例如觸發(fā) mousemove 時斷點;
addEventListener('mousemove', e => { console.log(e) })
因此調(diào)試者很容易找到事件回調(diào)函數(shù),從而分析相應(yīng)的處理邏輯。
如何防止事件回調(diào)被斷點?這就需要前面講解的黑科技了。我們對上述代碼稍微修改,將自己的回調(diào)函數(shù)改成原生函數(shù):
addEventListener('mousemove', console.log)
這時,每次觸發(fā) mousemove 事件都不會被斷??!
然而現(xiàn)實中的回調(diào)邏輯遠比 console.log 復(fù)雜,又該如何應(yīng)用?
事實上我們可以做一些調(diào)整,將事件的回調(diào)邏輯變得足夠簡單,簡單到只需一個操作 —— 保存結(jié)果:
const Q = [] addEventListener('mousemove', Q.push.bind(Q))
由于調(diào)用函數(shù) bind 方法后返回的新函數(shù),其實是原生的:
function A() {} A.bind(window) + '' // "function () { [native code] }"
而 Q.push 本身也是原生函數(shù),因此它們兩都是原生函數(shù)。
同時 addEventListener 執(zhí)行回調(diào)也屬于內(nèi)置行為,因此整個操作都是原生函數(shù)在執(zhí)行,沒有任何自己的代碼可供調(diào)試器斷點!
現(xiàn)在觸發(fā) mousemove 事件不僅不會被斷住,而且還能將結(jié)果追加到數(shù)組 Q 中。
至于讀取則有很多辦法,例如渲染事件、空閑事件、定期輪詢等。
setInterval(() => { for (const v of Q) { console.log(v) } Q.length = 0 }, 20)
如果 JS 只是采集信息而沒有交互,可用更低的讀取頻率。
屬性訪問
前面的案例都是函數(shù)調(diào)用,例如 alert 函數(shù)、數(shù)組 push 函數(shù)。但屬性讀寫又該如何實現(xiàn)?例如:
window.onclick = function() { document.title = 'hello' }
其實也不難。屬性讀寫本質(zhì)上是 getter 和 setter 函數(shù)的調(diào)用。例如:
const setter = Object.getOwnPropertyDescriptor(Document.prototype, 'title').set setter.call(document, 'hello')
當然這樣會立即執(zhí)行,而不是在 onclick 事件時執(zhí)行。
因此我們可以給 setter 柯里化,創(chuàng)建一個已綁定參數(shù)的新函數(shù),作為事件回調(diào):
const setter = Object.getOwnPropertyDescriptor(Document.prototype, 'title').set window.onclick = setter.bind(document, 'hello')
這樣只有在點擊時才會執(zhí)行。并且調(diào)試器的 click 事件斷點不會觸發(fā)。
對象屬性
除了原型上的屬性,普通對象的屬性又該如何訪問?例如:
const obj = {} window.onclick = function() { obj.name = 'jack' }
事實上 JS 基本操作都可通過 Reflect API 實現(xiàn)。例如:
const obj = {} Reflect.set(obj, 'name', 'jack')
不過需注意的是,Reflect.set
的參數(shù)必須是 3 個,多一個也不行。例如:
const obj = {} Reflect.set(obj, 'age', 20, {}) obj.age // undefined
這樣將其柯里化成事件回調(diào)函數(shù)是有問題的,因為事件回調(diào)還會加上一個 event 參數(shù)。
不過 Reflect.apply
方法倒沒有這個限制,往后再加幾個參數(shù)也不影響執(zhí)行:
Reflect.apply(alert, null, ['hello'], /* 無用的參數(shù) */ 100, 200, 300)
因此我們可通過 Reflect.apply
執(zhí)行 Reflect.set
,從而過濾多余的參數(shù):
const obj = {} Reflect.apply(Reflect.set, null, [obj, 'age', 20]) obj.age // 20
然后將其柯里化成事件回調(diào)函數(shù):
const obj = {} window.onclick = Reflect.apply.bind(null, Reflect.set, null, [obj, 'age', 20])
這樣即可通過原生函數(shù)執(zhí)行 obj.age = 20,并且 click 事件斷點依然不會觸發(fā)。
多個操作
前面講解的都是單個操作,是否可以一次執(zhí)行多個操作?例如:
console.log('hello') console.log('world') alert(123)
最容易想到的辦法,就是將每個操作放入數(shù)組,然后通過 forEach
回調(diào) Reflect.apply
執(zhí)行每個操作:
[ Reflect.apply.bind(null, console.log, null, ['hello']), Reflect.apply.bind(null, console.log, null, ['world']), Reflect.apply.bind(null, alert, null, [123]), ].forEach(Reflect.apply)
幸運的是 forEach
的回調(diào)函數(shù)和 Reflect.apply
函數(shù)都是 3 個參數(shù),并且第 3 個都是數(shù)組類型:
forEach_callback(element, index, array) Reflect.apply(target, thisArgument, argumentsList)
這樣通過 forEach
回調(diào) Reflect.apply
是完全沒問題的。于是可以一次執(zhí)行多個操作,并且都無法斷住!
除了上述提到的,其實還有更多玩法,大家可發(fā)揮想象~
到此這篇關(guān)于詳解如何讓JavaScript代碼不可斷點的文章就介紹到這了,更多相關(guān)JavaScript代碼不斷點內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
通過復(fù)制Table生成word和excel的javascript代碼
通過復(fù)制Table生成word和excel,個人感覺這個功能還是比較實用的,下面有個不錯的示例,希望對大家有所幫助2014-01-01Easy.Ajax 部分源代碼 支持文件上傳功能, 兼容所有主流瀏覽器
下面是Easy.Ajax類的初稿,如須發(fā)表,在代碼上還要修改以達到最簡,但API是不會變了2011-02-02promise和co搭配生成器函數(shù)方式解決js代碼異步流程的比較
這篇文章主要介紹了promise和co搭配生成器函數(shù)方式解決js代碼異步流程的比較,在es6中引入的原生Promise為js的異步回調(diào)問題帶來了一個新的解決方式co模塊搭配Generator函數(shù)的同步寫法,更是將js的異步回調(diào)帶了更優(yōu)雅的寫法。感興趣的小伙伴們可以參考一下2018-05-05