写埋点、扒 SDK、改框架:JS 函数复写 10 连招实战手册

markdown 复制代码
# JS 中"复写函数"的 10 种姿势(附完整可运行 Demo)

在前端工程里,"复写一个函数"(Hook / Patch / Override)是非常常见的需求,尤其是在:

- 调试黑盒 SDK
- 做监控埋点
- 改第三方库行为但又不能直接改源码

本文把常见的 10 种手法整理成一个"武器清单",**每种都给出可直接跑的 Demo**,方便你复制到控制台 / Node 里试。

---

## 🟦 一、直接覆写:最简单粗暴

**思路**:如果函数本来就是某个对象上的方法,直接重新赋值即可。

### 示例场景

已有一个业务对象:

```js
const userService = {
  getUserName(id) {
    console.log('[orig] getUserName', id);
    return 'Alice';
  }
};

// 原始调用
console.log('--- 原始调用 ---');
console.log(userService.getUserName(1));

现在想完全替换getUserName 的实现:

js 复制代码
// 直接覆写
userService.getUserName = function (id) {
  console.log('[new] getUserName', id);
  return 'Bob';
};

console.log('--- 覆写后 ---');
console.log(userService.getUserName(1));

特点

  • 简单直接
  • 不保留原逻辑(除非你自己先存一份引用)

🟩 二、Monkey Patch:保留原逻辑再"加一层壳"(最常用)

思路:保存原函数引用 → 用同名函数包装一层。

示例:给 SDK 方法加埋点

js 复制代码
// 模拟一个 SDK
const sdk = {
  track(eventName, payload) {
    console.log('[orig track]', eventName, payload);
    return { ok: true };
  }
};

// 原始使用
console.log('--- 原始调用 ---');
sdk.track('page_view', { path: '/home' });

// 开始 Monkey Patch
const origTrack = sdk.track;

sdk.track = function (...args) {
  console.log('[before track]', ...args);

  const result = origTrack.apply(this, args);

  console.log('[after track]', result);
  return result;
};

console.log('--- Patch 之后 ---');
sdk.track('page_view', { path: '/detail' });

适用

  • 想在原逻辑前后"插入"自己的逻辑
  • Hook 黑盒 SDK、监控、埋点等

🟧 三、Proxy:拦截整个对象(高级黑盒分析)

思路 :用 Proxy 代理整个对象,在 get 里统一拦截函数调用。

示例:拦截所有方法调用做日志

js 复制代码
// 一个"黑盒"对象,方法名可能会变
const api = {
  login(user, pwd) {
    console.log('login...', user);
    return { token: 'xxx' };
  },
  logout() {
    console.log('logout...');
  }
};

// 用 Proxy 包一层
const apiWithLog = new Proxy(api, {
  get(target, prop, receiver) {
    const value = Reflect.get(target, prop, receiver);

    if (typeof value === 'function') {
      return function (...args) {
        console.log(`[Proxy] call ${String(prop)} with`, args);
        const result = value.apply(this, args);
        console.log(`[Proxy] ${String(prop)} result`, result);
        return result;
      };
    }

    return value;
  }
});

// 使用方式跟原来一样
console.log('--- 使用 Proxy 后 ---');
apiWithLog.login('alice', '123456');
apiWithLog.logout();

适用

  • 不知道具体方法名(SDK 里一堆方法)
  • 想统一拦截所有方法调用(sniff)

🟨 四、Object.defineProperty:强行改 getter / setter / 不可枚举属性

思路 :当属性不可写、或是 getter / setter,需要用 defineProperty 操作描述符。

示例 1:把只读属性改成可写函数

js 复制代码
const obj = {};

// 初始定义:只读属性
Object.defineProperty(obj, 'foo', {
  value: 'readonly',
  writable: false,
  configurable: true,
  enumerable: true
});

console.log('原始 foo =', obj.foo);

// 直接赋值会失败(严格模式下报错,非严格模式下无效)
obj.foo = 'new';
console.log('直接赋值后 foo =', obj.foo); // 还是 readonly

// 用 defineProperty 强行改成函数
Object.defineProperty(obj, 'foo', {
  configurable: true,
  writable: true,
  value: function () {
    console.log('I am new foo()');
  }
});

console.log('--- 改成函数之后 ---');
obj.foo();

示例 2:用 getter 做懒加载 Hook

js 复制代码
const service = {};

Object.defineProperty(service, 'expensiveApi', {
  configurable: true,
  get() {
    console.log('[getter] 初始化 expensiveApi');
    // 真正创建对象
    const realApi = {
      call() {
        console.log('real api called');
      }
    };
    // 用普通属性替换掉 getter(只初始化一次)
    Object.defineProperty(service, 'expensiveApi', {
      value: realApi,
      writable: false,
      configurable: false
    });
    return realApi;
  }
});

// 第一次访问会触发 getter
console.log('--- 第一次访问 ---');
service.expensiveApi.call();

// 第二次访问直接用缓存
console.log('--- 第二次访问 ---');
service.expensiveApi.call();

🟥 五、Hook 全局内置方法:网络、事件、路由等(非常强)

示例 1:拦截 fetch(浏览器)

js 复制代码
// 保存原始 fetch
const origFetch = window.fetch;

// 覆写
window.fetch = async (...args) => {
  console.log('[Hook fetch] 请求参数:', args);

  const res = await origFetch(...args);

  // 复制一份 Response 查看内容(注意:这里只是示意,实际需考虑流只能读一次)
  const clone = res.clone();
  clone.text().then(text => {
    console.log('[Hook fetch] 响应文本截断:', text.slice(0, 100));
  });

  return res;
};

// 使用:之后所有 fetch 都会经过 Hook
// fetch('https://jsonplaceholder.typicode.com/todos/1');

示例 2:拦截所有 DOM 事件监听

js 复制代码
const origAdd = EventTarget.prototype.addEventListener;

EventTarget.prototype.addEventListener = function (type, listener, options) {
  console.log('[Hook addEventListener]', type, 'on', this);

  // 也可以在这里包一层 listener,实现事件埋点
  return origAdd.call(this, type, listener, options);
};

// Demo:添加一个 click 事件
document.body.addEventListener('click', () => {
  console.log('body clicked');
});

还能 Hook

  • XMLHttpRequest.prototype.send
  • WebSocket.prototype.send
  • history.pushState / replaceState(前端路由监控)
  • console.log(调试场景)

🟪 六、代理模块加载:require Hook / 构建期 alias

6.1 Node:Hook require(CommonJS)

js 复制代码
// hook-require.js
const Module = require('module');
const originalRequire = Module.prototype.require;

Module.prototype.require = function (path) {
  console.log('[require]', path);
  const exports = originalRequire.apply(this, arguments);

  // 针对指定模块做 Monkey Patch
  if (path === './sdk') {
    const origDo = exports.doSomething;
    exports.doSomething = function (...args) {
      console.log('[patched sdk.doSomething]', args);
      return origDo.apply(this, args);
    };
  }

  return exports;
};

// 主入口
require('./hook-require');
const sdk = require('./sdk');

sdk.doSomething('hello');
js 复制代码
// sdk.js
exports.doSomething = function (msg) {
  console.log('[orig sdk.doSomething]', msg);
};

运行 node main.js 即可看到 require 日志 + patch 效果。


6.2 前端:通过 alias 替换整个模块

以 Vite 为例(Webpack 思路类似)。

步骤 1:配置 alias

js 复制代码
// vite.config.js
import { defineConfig } from 'vite';

export default defineConfig({
  resolve: {
    alias: {
      'some-sdk': '/src/sdk-wrapped.js' // 用你自己的 wrapper 替换原 SDK
    }
  }
});

步骤 2:在 wrapper 里再 Monkey Patch

js 复制代码
// src/sdk-wrapped.js
import * as realSdk from 'some-sdk-original'; // 假设这是原 SDK 包名

const sdk = { ...realSdk };

const origInit = sdk.init;
sdk.init = function (...args) {
  console.log('[wrapped sdk.init]', args);
  return origInit.apply(this, args);
};

export default sdk;
export * from 'some-sdk-original'; // 其余原样导出

业务代码:

js 复制代码
// import sdk from 'some-sdk'; // 实际拿到的是 sdk-wrapped.js
// sdk.init(...);

🟫 七、ES6 class 继承:在可控框架里"正统扩展"

思路 :当你能控制实例创建时,直接用 extends 继承原类,覆写方法并按需 super.foo()

示例:扩展一个 UI 组件类

js 复制代码
class Button {
  click() {
    console.log('[Button] clicked');
  }
}

// 原始使用
const btn = new Button();
btn.click();

// 新类:在原 click 前后加逻辑
class LogButton extends Button {
  click() {
    console.log('[LogButton] before click');
    const result = super.click();
    console.log('[LogButton] after click');
    return result;
  }
}

const logBtn = new LogButton();
logBtn.click();

适合

  • 你能自己 new 子类,而不是框架内部帮你 new
  • 比如内部业务类、自己封装的组件体系

🟩 八、Proxy + construct:拦截 new 的构造行为

思路 :用 Proxy 代理"类本身",在 construct 里拦截所有实例创建。

示例:给每个实例自动打 Patch

js 复制代码
class Service {
  foo() {
    console.log('[Service.foo]');
  }
}

const PatchedService = new Proxy(Service, {
  construct(target, args, newTarget) {
    console.log('[Proxy construct] 创建 Service 实例,参数:', args);

    // 创建原始实例
    const instance = Reflect.construct(target, args, newTarget);

    // 给实例打补丁
    const origFoo = instance.foo;
    instance.foo = function (...fooArgs) {
      console.log('[patched foo] before', fooArgs);
      const result = origFoo.apply(this, fooArgs);
      console.log('[patched foo] after', result);
      return result;
    };

    return instance;
  }
});

// 之后统一用 PatchedService,而不是 Service
const s = new PatchedService();
s.foo();

适合

  • 第三方库暴露的是"类",你可以控制 new 的地方
  • 想让所有实例都应用同一套 Patch

🟦 九、Hook 原型:对所有实例生效

思路 :直接改 Class.prototype.xxx 或内置类型原型,例如 Array.prototype.push

示例 1:改自定义类的原型方法

js 复制代码
function Person(name) {
  this.name = name;
}

Person.prototype.sayHi = function () {
  console.log('Hi, I am', this.name);
};

const p1 = new Person('Alice');
p1.sayHi();

// Patch 原型
const origSayHi = Person.prototype.sayHi;

Person.prototype.sayHi = function () {
  console.log('[patched] before sayHi');
  origSayHi.call(this);
  console.log('[patched] after sayHi');
};

const p2 = new Person('Bob');

console.log('--- Patch 之后 ---');
p1.sayHi(); // 旧实例也会受影响
p2.sayHi();

示例 2:给所有数组的 push 打日志(谨慎使用)

js 复制代码
const origPush = Array.prototype.push;

Array.prototype.push = function (...items) {
  console.log('[Array.push patched] this =', this, 'items =', items);
  return origPush.apply(this, items);
};

const arr = [1, 2];
arr.push(3);  // 会打印日志

注意:改内置原型影响范围非常大,要在可控环境下使用。


🟣 十、动态注入 <script>:在不能改源码时复写全局函数

思路:在浏览器通过脚本注入的方式覆盖某个全局函数(油猴脚本、Chrome 插件常用)。

示例:复写页面里已有的 window.someFn

假设目标页面有:

js 复制代码
window.someFn = function () {
  console.log('[page] someFn');
};

你在自己的脚本里注入一段:

js 复制代码
(function inject() {
  const script = document.createElement('script');

  script.innerHTML = `
    (function () {
      const orig = window.someFn;
      window.someFn = function (...args) {
        console.log('[injected] before someFn', args);
        const result = orig && orig.apply(this, args);
        console.log('[injected] after someFn', result);
        return result;
      };
    })();
  `;

  document.documentElement.appendChild(script);
  script.remove();
})();

执行后,页面中所有对 someFn 的调用都会走你注入的版本。

适用

  • 你不能改页面源码,但能注入 JS(油猴脚本 / 浏览器扩展 / WebView 注入等)

🔥 总结:复写函数的"武器清单"

方法 能力/复杂度 典型场景
直接覆写 最简单 你能直接访问对象和方法名
Monkey Patch ⭐最常用 Hook SDK、埋点、调试
Proxy(拦截对象) ⭐高级黑盒分析 不知道具体方法名,想全量 sniff
defineProperty 强力 只读属性、getter/setter 代理
Hook 全局 API ⭐非常强 网络、事件、路由、日志监控
require Hook / alias 构建期 替换整个 SDK 模块
class 继承 正统扩展 自己可控的类/组件
Proxy + construct 高级 拦截所有实例的创建过程
原型 Hook 全局影响 对所有实例生效(谨慎)
script 注入 浏览器插件/油猴场景 不能改源码、只能运行时注入

--

相关推荐
感谢地心引力34 分钟前
【HTML Living Standard 01】HTML基础概述
前端·html
૮・ﻌ・36 分钟前
Vue2(三):自定义指令、插槽、路由
前端·javascript·vue.js
快落的小海疼44 分钟前
全局重复接口取消&重复提示
前端·vue.js
快落的小海疼1 小时前
前端导出页面内容为PDF
前端
周某人姓周1 小时前
XSS(一)概述
前端·安全·xss
半梅芒果干1 小时前
vue3 网站访问页面缓存优化
前端·javascript·缓存
lichong9511 小时前
android 使用 java 编写网络连通性检查
android·java·前端
孟祥_成都1 小时前
公司 React 应用感觉很慢,我把没必要的重复渲染砍掉了 40%!
前端
王大宇_1 小时前
word对比工具从入门到出门
前端·javascript