You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何统计JavaScript中函数参数的被使用次数?

如何统计JavaScript中函数参数的被使用次数?

这个需求我之前也琢磨过,刚好有两个可行的方案,给你唠唠~

方案一:运行时包装拦截(适合大部分日常场景)

核心思路就是把传入的参数包成一个“带计数器的壳”,当这个参数被实际用起来(比如参与运算、转成字符串、作为返回值)的时候,计数器就自动加一。为啥能行?因为JS里对象在需要转成原始值的时候,会自动调用valueOf()或者toString()方法,我们就把计数逻辑埋在这俩方法里:

function argCounter(fn) {
  return (arg) => {
    let argCalls = 0;

    // 给原始类型参数(比如数字、字符串)做包装
    function wrapPrimitive(value) {
      const wrapper = {
        valueOf() {
          argCalls++;
          return value;
        },
        toString() {
          argCalls++;
          return String(value);
        }
      };
      // 让包装出来的对象尽量跟原始值“长得像”,避免被黑盒函数识破
      Object.defineProperty(wrapper, 'constructor', { value: value.constructor });
      return wrapper;
    }

    // 给对象类型参数做包装(用Proxy补全场景)
    function wrapObject(obj) {
      return new Proxy(obj, {
        get(target, prop) {
          // 要是碰到隐式转换的钩子函数,就计数
          if (prop === 'valueOf' || prop === 'toString') {
            return () => {
              argCalls++;
              return Reflect.get(target, prop).call(target);
            };
          }
          // 注意:访问对象的属性不算“使用参数本身”,所以这里不计数
          return Reflect.get(target, prop);
        },
        // 要是参数是个函数,被调用的时候也计数
        apply(target, thisArg, args) {
          argCalls++;
          return Reflect.apply(target, thisArg, args);
        }
      });
    }

    // 根据参数类型选对应的包装方式
    const wrappedArg = (typeof arg !== 'object' || arg === null)
      ? wrapPrimitive(arg)
      : wrapObject(arg);

    // 该调用黑盒函数还是得调用,不能影响原功能
    fn(wrappedArg);

    return argCalls;
  };
}

这个方案的好处是纯运行时,不用额外装库,日常用足够了;但也有个明显的坑:单独写一行obj;这种只引用不干活的语句,它统计不到。因为这种场景JS引擎不会触发任何隐式转换,Proxy也没对应的拦截钩子,所以你例子里的fn2()用这个方案只能统计到return那1次,出不来4次的结果。

方案二:AST静态分析(覆盖所有场景,得能拿到函数代码)

要是你必须统计所有场景——包括obj;这种“摸鱼式引用”,那静态分析是唯一的出路。简单说就是把函数转成字符串,拆成抽象语法树(AST),然后在树里找参数名出现的次数,有点像用代码“搜素”参数引用:

这里我们用acorn这个常用的AST解析库(浏览器可以引CDN,Node.js直接npm install acorn就行),实现代码如下:

// 先确保引入了acorn,比如Node.js里用import,浏览器里加script标签
import acorn from 'acorn';

function argCounter(fn) {
  return (arg) => {
    let argCalls = 0;
    const fnCode = fn.toString();

    // 第一步:先把函数的参数名抠出来
    // 兼容箭头函数带括号和不带括号的情况,比如`(obj) => ...`或者`obj => ...`
    const paramRegex = /^\s*(?:\(([^)=]+)\)|([^)=]+))\s*=>/;
    const match = fnCode.match(paramRegex);
    if (!match) {
      throw new Error('搞不定这个函数的参数,得是标准箭头函数才行');
    }
    const paramName = (match[1] || match[2]).trim();

    // 第二步:把函数代码拆成AST
    const ast = acorn.parse(fnCode, { ecmaVersion: 'latest' });
    // 箭头函数的主体可能是单个表达式,也可能是大括号包的代码块,都能拿到
    const bodyNode = ast.body[0].expression.body;

    // 第三步:遍历AST,碰到参数名就计数
    function traverseAST(node) {
      if (node.type === 'Identifier' && node.name === paramName) {
        argCalls++;
      }
      // 递归遍历所有子节点,别漏了
      for (const key in node) {
        if (node[key] && typeof node[key] === 'object') {
          traverseAST(node[key]);
        }
      }
    }
    traverseAST(bodyNode);

    // 别忘了原函数该咋跑咋跑,不能影响功能
    fn(arg);

    return argCalls;
  };
}

这个方案能完美解决你例子里的问题——fn2()里的三次obj;加return的那一次,总共4次,全能统计到。但它也有几个小限制:

  • 要是函数是浏览器内置的、或者用Function构造函数动态生成的,toString()会返回[native code],那就没法解析了;
  • 要是函数内部又定义了和参数同名的变量,会把这些也统计进去,有点误伤;
  • 得依赖AST解析库,用正则的话太容易翻车,比如碰到字符串里刚好有参数名的情况。

对了,你提到的Proxy确实只能管对象的属性、方法调用,没法直接拦截对象本身的引用,所以单独用Proxy搞不定全部场景,得结合包装或者静态分析来补。

备注:内容来源于stack exchange,提问作者makxca

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.16 02:54:51