2

如何让 JS 代码不可断点 - EtherDream

 1 year ago
source link: https://www.cnblogs.com/index-html/p/js-anti-breakpoint.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

如何让 JS 代码不可断点

调试 JS 代码时,单步执行(F11)可跟踪所有操作。例如这段代码,每次调用 alert 时都会被断住:

debugger
alert(11)
alert(22)
alert(33)
alert(44)

有没有什么办法能让单步执行失效,一次执行多个操作?

事实上有一些巧妙的办法。例如通过数组回调执行这些 alert 函数:

debugger
[11, 22, 33, 44].forEach(alert)

这样只有 forEach 之前和之后会被断住,中间所有 alert 调用都不会被断住。

由此可见,通过 内置回调 执行 原生函数,调试器是无法断住的!

利用这个特性,我们可将一些重要的操作隐藏起来,从而能在调试者眼皮下悄悄执行。

主流浏览器的调试器允许拦截特定事件,例如触发 mousemove 时断点;

addEventListener('mousemove', e => {
  console.log(e)
})
273626-20220804170654064-771715643.png

因此调试者很容易找到事件回调函数,从而分析相应的处理逻辑。

如何防止事件回调被断点?这就需要前面讲解的黑科技了。我们对上述代码稍微修改,将自己的回调函数改成原生函数:

addEventListener('mousemove', console.log)
273626-20220804170654080-2144463030.png

这时,每次触发 mousemove 事件都不会被断住!

然而现实中的回调逻辑远比 console.log 复杂,又该如何应用?

事实上我们可以做一些调整,将事件的回调逻辑变得足够简单,简单到只需一个操作 —— 保存结果:

const Q = []
addEventListener('mousemove', Q.push.bind(Q))

由于调用函数 bind 方法后返回的新函数,其实是原生的:

function A() {}
A.bind(window) + ''   // "function () { [native code] }"

而 Q.push 本身也是原生函数,因此它们两都是原生函数。

同时 addEventListener 执行回调也属于内置行为,因此整个操作都是原生函数在执行,没有任何自己的代码可供调试器断点!

现在触发 mousemove 事件不仅不会被断住,而且还能将结果追加到数组 Q 中。

273626-20220804170654114-1993988067.png

至于读取则有很多办法,例如渲染事件、空闲事件、定期轮询等。

setInterval(() => {
  for (const v of Q) {
    console.log(v)
  }
  Q.length = 0
}, 20)
273626-20220804170654136-339609847.png

如果 JS 只是采集信息而没有交互,可用更低的读取频率。

前面的案例都是函数调用,例如 alert 函数、数组 push 函数。但属性读写又该如何实现?例如:

window.onclick = function() {
  document.title = 'hello'
}

其实也不难。属性读写本质上是 getter 和 setter 函数的调用。例如:

const setter = Object.getOwnPropertyDescriptor(Document.prototype, 'title').set
setter.call(document, 'hello')

当然这样会立即执行,而不是在 onclick 事件时执行。

因此我们可以给 setter 柯里化,创建一个已绑定参数的新函数,作为事件回调:

const setter = Object.getOwnPropertyDescriptor(Document.prototype, 'title').set
window.onclick = setter.bind(document, 'hello')

这样只有在点击时才会执行。并且调试器的 click 事件断点不会触发。

除了原型上的属性,普通对象的属性又该如何访问?例如:

const obj = {}
window.onclick = function() {
  obj.name = 'jack'
}

事实上 JS 基本操作都可通过 Reflect API 实现。例如:

const obj = {}
Reflect.set(obj, 'name', 'jack')

不过需注意的是,Reflect.set 的参数必须是 3 个,多一个也不行。例如:

const obj = {}
Reflect.set(obj, 'age', 20, {})
obj.age   // undefined

这样将其柯里化成事件回调函数是有问题的,因为事件回调还会加上一个 event 参数。

不过 Reflect.apply 方法倒没有这个限制,往后再加几个参数也不影响执行:

Reflect.apply(alert, null, ['hello'],   /* 无用的参数 */ 100, 200, 300)

因此我们可通过 Reflect.apply 执行 Reflect.set,从而过滤多余的参数:

const obj = {}
Reflect.apply(Reflect.set, null, [obj, 'age', 20])
obj.age   // 20

然后将其柯里化成事件回调函数:

const obj = {}
window.onclick = Reflect.apply.bind(null, Reflect.set, null, [obj, 'age', 20])

这样即可通过原生函数执行 obj.age = 20,并且 click 事件断点依然不会触发。

前面讲解的都是单个操作,是否可以一次执行多个操作?例如:

console.log('hello')
console.log('world')
alert(123)

最容易想到的办法,就是将每个操作放入数组,然后通过 forEach 回调 Reflect.apply 执行每个操作:

[
  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 的回调函数和 Reflect.apply 函数都是 3 个参数,并且第 3 个都是数组类型:

forEach_callback(element, index, array)

Reflect.apply(target, thisArgument, argumentsList)

这样通过 forEach 回调 Reflect.apply 是完全没问题的。于是可以一次执行多个操作,并且都无法断住!

除了上述提到的,其实还有更多玩法,大家可发挥想象~

(2021/11/01)


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK