如何 “正确” hook JS方法

大家好,好几天没有更新文章了,其实是去了趟外地参加了同学的婚礼,正好赶上这几天工作比较忙没有抽出时间来,所以停了几天哈哈。不过今天我回来了,今天分享一下在 JS 中如何正确 hook 我们想要 hook 的方法。

hook是什么

先说 hook,hook 不是 JS 中的特例,hook 其实是一种方法,本质是在不修改源码的前提下,通过拦截函数调用来实现监控、修改或扩展程序行为的技术。这种方法很多语言中普遍存在,在 Python 叫装饰器,在 Java 中叫切面,大同小异。

在 JS 中,根据方法的类型可分为两类 hook 场景:

静态方法hook

静态方法是直接挂载在构造函数或类上的方法(如 Math.random()Object.keys()),hook 时可以直接覆盖原方法,例如 hook Mathrandom 方法:

1
2
3
4
5
6
7
8
9
// 保存原始方法
const originalRandom = Math.random;

// 覆盖静态方法
Math.random = function() {
const result = originalRandom();
console.log(`Random value generated: ${result}`);
return result;
};

image-20250513211156743

hook 静态方法时可以直接覆盖原始方法,但是别忘了保存原始方法,不然在调用函数的时候就会进入死循环一直调用自己,导致的后果就是栈溢出,见下图:

image-20250513211716310

其他静态方法

静态方法还有很多,很多内置对象都有自己的静态方法:

image-20250513212036087

image-20250513212136206

image-20250513212232333

image-20250513212249643

可能有人会问上面的内容在哪里可以找到,这里推荐一个网站:MDN,可以把它当做学习 JS 的官方网站来用,想要知道某个对象的某个方法如何使用,都可以来这个网站上来查询,里面的内容相当丰富,随时打开网页就能看。

如何判断是否是静态方法

那如何判断一方法是否是静态方法呢?很简单,在浏览器中输入这个方法,看一下浏览器自动提示出来的方法中有没有想要的方法就知道了。例如:

image-20250513213151722

image-20250513213308156

会发现 JSON 有四个静态方法,Date 有三个静态方法,注意哦,namelength 是属性,不是静态方法。属性和方法如何区分这个就不用我说了吧,教大家一个小技巧。

image-20250513213438705

image-20250513213501175

在输入对应的方法或者属性名的时候,注意哦,我没有敲回车,方法下面会自动提示一个 f,意思就是当前输入的是一个方法,如果是一个属性,会自动显示属性的值。可以使用这种方法来判断当前输入的是一个属性还是方法。

实例方法 hook

对于实例方法,就不能直接覆盖原始方法了,在上面讲静态方法的时候也能看到,如果不实例化的话,根本看不到实例方法,重写也就无从谈起。

实例方法存在于对象原型链中(如 Array.prototype.push),需要通过修改原型实现:

1
2
3
4
5
6
7
8
// 保存原型方法
const originalPush = Array.prototype.push;

// 重写原型方法
Array.prototype.push = function(...items) {
console.log(`Pushing ${items.length} elements`);
return originalPush.apply(this, items);
};

如果要 hook 实例方法,那么就要通过 prototype 重写原型方法,因为 JS 的实例方法默认存储在原型对象(prototype)中,所有实例通过原型链共享这些方法,直接修改原型会影响所有已存在和未来的实例。

image-20250513214459923

image-20250513215420526

可能有小伙伴注意到了,除了打印日志之外,return 并没有直接调用原型方法,而是使用了原型方法的 apply 方法(其实这里使用 call 也是一样的,区别不大,先忽略),这是为什么呢?

apply 方法的作用

apply 方法是定义在了函数的原型上的,也就是说所有的函数都可以使用这个方法。applycall 方法几乎相同,可以互相替换,不过要注意参数的传递方式有所区别。

image-20250513222902113

image-20250513220839724

image-20250513223306968

它的作用就是以给定的 this 值来调用该函数,说白了就是改变 this 的指向。那 this 的指向是什么意思呢?来看一个示例:

image-20250513223201503

先定义一个对象,它有一个 aa 方法和一个 pro 属性,在调用 aa 方法的时候输出 this 指向的 pro 属性,发现输出为 undefined,但是如果我传入 ob2 对象,它有 pro 属性,则可以正确的获取 pro 属性的值,值为 ob2 对象中的 pro 属性的值。

通过以上代码应该可以很好的理解 this 指向的意思了,如果在调用 apply 的时候不提供第一个参数,则会被替换为 undefined,如果提供了则 this 会指向提供的对象。

在实际 hook 实例方法的时候,因为不同的实例调用的都是相同的实例方法,这个时候如何针对不同的实例进行操作呢?这里就要用到 apply 方法的第一个参数 this ,那么问题又来了,为什么使用不同的实例调用同一个 push 方法,this 会默认指向不同的实例呢?

1
2
3
4
5
6
7
8
9
// 伪代码解释执行过程
const arr = [];
arr.push(1);

// 实际执行步骤:
// 1. 在 arr 实例上查找 push 方法
// 2. 发现 arr 自身没有 push,沿原型链找到 Array.prototype.push
// 3. 调用 push 方法时,this 被绑定为 arr 实例
Array.prototype.push.call(arr, 1); // 这是真实底层行为

因为 JS 帮我们进行了自动绑定,在调用 push 方法的时候底层会自动将当前的对象绑定到 this 中。

总结下来,为了可以正确的 hook 实例方法,有以下三种机制在起作用:

  • 动态绑定:JS 中方法的 this 始终指向调用该方法的对象实例。
  • 原型共享:所有数组实例共享 Array.prototype 上的方法,但每次调用时 this 会绑定到当前实例。
  • apply 的作用:通过 originalPush.apply(this, ...) 确保原始方法操作正确的实例数据。

以上三种机制可以保证重写原型方法的时候透明的作用于所有的实例,而不用为每个实例维护单独的副本。

注:以上内容根据我当前所了解的知识整理而成,如有疏漏或错误请大家指出,谢谢大家。

总结

以上就是关于如何正确 hook JS 方法的所有内容了,可能有些内容大家都没咋见过,不过高级一点的混淆代码中大量运用了这些日常开发中不怎么会用到的知识点,这些 JS 的“底层逻辑”还是建议大家掌握一下,对于以后扣代码或者搞懂混淆代码很有好处。

本文章首发于个人博客 LLLibra146’s blog

本文作者:LLLibra146

更多文章请关注公众号 (LLLibra146):LLLibra146

版权声明:本博客所有文章除特别声明外,均采用 © BY-NC-ND 许可协议。非商用转载请注明出处!严禁商业转载!

本文链接
https://blog.d77.xyz/archives/4f48c38.html