如何 “正确” hook JS方法
大家好,好几天没有更新文章了,其实是去了趟外地参加了同学的婚礼,正好赶上这几天工作比较忙没有抽出时间来,所以停了几天哈哈。不过今天我回来了,今天分享一下在 JS 中如何正确 hook 我们想要 hook 的方法。
hook是什么
先说 hook,hook 不是 JS 中的特例,hook 其实是一种方法,本质是在不修改源码的前提下,通过拦截函数调用来实现监控、修改或扩展程序行为的技术。这种方法很多语言中普遍存在,在 Python 叫装饰器,在 Java 中叫切面,大同小异。
在 JS 中,根据方法的类型可分为两类 hook 场景:
静态方法hook
静态方法是直接挂载在构造函数或类上的方法(如 Math.random()
、Object.keys()
),hook 时可以直接覆盖原方法,例如 hook Math
的 random
方法:
1 | // 保存原始方法 |
hook 静态方法时可以直接覆盖原始方法,但是别忘了保存原始方法,不然在调用函数的时候就会进入死循环一直调用自己,导致的后果就是栈溢出,见下图:
其他静态方法
静态方法还有很多,很多内置对象都有自己的静态方法:
可能有人会问上面的内容在哪里可以找到,这里推荐一个网站:MDN,可以把它当做学习 JS 的官方网站来用,想要知道某个对象的某个方法如何使用,都可以来这个网站上来查询,里面的内容相当丰富,随时打开网页就能看。
如何判断是否是静态方法
那如何判断一方法是否是静态方法呢?很简单,在浏览器中输入这个方法,看一下浏览器自动提示出来的方法中有没有想要的方法就知道了。例如:
会发现 JSON
有四个静态方法,Date
有三个静态方法,注意哦,name
和 length
是属性,不是静态方法。属性和方法如何区分这个就不用我说了吧,教大家一个小技巧。
在输入对应的方法或者属性名的时候,注意哦,我没有敲回车,方法下面会自动提示一个 f
,意思就是当前输入的是一个方法,如果是一个属性,会自动显示属性的值。可以使用这种方法来判断当前输入的是一个属性还是方法。
实例方法 hook
对于实例方法,就不能直接覆盖原始方法了,在上面讲静态方法的时候也能看到,如果不实例化的话,根本看不到实例方法,重写也就无从谈起。
实例方法存在于对象原型链中(如 Array.prototype.push
),需要通过修改原型实现:
1 | // 保存原型方法 |
如果要 hook 实例方法,那么就要通过 prototype
重写原型方法,因为 JS 的实例方法默认存储在原型对象(prototype
)中,所有实例通过原型链共享这些方法,直接修改原型会影响所有已存在和未来的实例。
可能有小伙伴注意到了,除了打印日志之外,return
并没有直接调用原型方法,而是使用了原型方法的 apply
方法(其实这里使用 call
也是一样的,区别不大,先忽略),这是为什么呢?
apply 方法的作用
apply
方法是定义在了函数的原型上的,也就是说所有的函数都可以使用这个方法。apply
和 call
方法几乎相同,可以互相替换,不过要注意参数的传递方式有所区别。
它的作用就是以给定的 this
值来调用该函数,说白了就是改变 this
的指向。那 this
的指向是什么意思呢?来看一个示例:
先定义一个对象,它有一个 aa
方法和一个 pro
属性,在调用 aa
方法的时候输出 this
指向的 pro
属性,发现输出为 undefined
,但是如果我传入 ob2
对象,它有 pro
属性,则可以正确的获取 pro
属性的值,值为 ob2
对象中的 pro
属性的值。
通过以上代码应该可以很好的理解 this
指向的意思了,如果在调用 apply
的时候不提供第一个参数,则会被替换为 undefined
,如果提供了则 this
会指向提供的对象。
在实际 hook 实例方法的时候,因为不同的实例调用的都是相同的实例方法,这个时候如何针对不同的实例进行操作呢?这里就要用到 apply
方法的第一个参数 this
,那么问题又来了,为什么使用不同的实例调用同一个 push
方法,this
会默认指向不同的实例呢?
1 | // 伪代码解释执行过程 |
因为 JS 帮我们进行了自动绑定,在调用 push
方法的时候底层会自动将当前的对象绑定到 this
中。
总结下来,为了可以正确的 hook 实例方法,有以下三种机制在起作用:
- 动态绑定:JS 中方法的
this
始终指向调用该方法的对象实例。 - 原型共享:所有数组实例共享
Array.prototype
上的方法,但每次调用时this
会绑定到当前实例。 apply
的作用:通过originalPush.apply(this, ...)
确保原始方法操作正确的实例数据。
以上三种机制可以保证重写原型方法的时候透明的作用于所有的实例,而不用为每个实例维护单独的副本。
注:以上内容根据我当前所了解的知识整理而成,如有疏漏或错误请大家指出,谢谢大家。
总结
以上就是关于如何正确 hook JS 方法的所有内容了,可能有些内容大家都没咋见过,不过高级一点的混淆代码中大量运用了这些日常开发中不怎么会用到的知识点,这些 JS 的“底层逻辑”还是建议大家掌握一下,对于以后扣代码或者搞懂混淆代码很有好处。
本文章首发于个人博客 LLLibra146’s blog
本文作者:LLLibra146
更多文章请关注公众号 (LLLibra146):
版权声明:本博客所有文章除特别声明外,均采用 © BY-NC-ND 许可协议。非商用转载请注明出处!严禁商业转载!