如何统计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
相关产品推荐
相关产品推荐

