news 2026/9/25 4:57:46

深入理解 Sinon 的 `spyCall.firstArg`:读取单次调用首个参数的正确姿势

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
深入理解 Sinon 的 `spyCall.firstArg`:读取单次调用首个参数的正确姿势
  • 测试
  • 开发工具

【免费下载链接】sinon

Test spies, stubs and mocks for JavaScript.

项目地址:https://gitcode.com/gh_mirrors/si/sinon
点击查看免费下载

spyCall.firstArg是 Sinon 中 spy call 对象的一个核心只读属性,用于获取某一次函数调用传入的第一个参数值。它适用于 fake、spy、stub 和 mock 方法等所有被 Sinon 代理过的函数,是细粒度验证调用行为、提取回调参数时最常用的入口之一。读完本文,你将掌握firstArg的语义边界(含无参数调用时的表现)、获取 spyCall 的多种途径,以及它在源码中的实现原理。

spyCall.firstArg是什么

在 Sinon 的概念体系中,spy call 是对一次“被监视函数调用”的对象化表示。当 fake、spy、stub 或 mock 方法 被调用时,Sinon 会为每一次调用生成一个独立的 spyCall 对象,其中就包含本次调用的全部细节:

  • args:本次调用的参数数组
  • firstArg:本次调用的第一个参数(本文主题)
  • lastArg:本次调用的最后一个参数
  • callback:本次调用中作为回调的函数参数
  • returnValue:本次调用的返回值
  • exception:本次调用抛出的异常
  • thisValue:本次调用执行时的this上下文

firstArg的官方定义非常简洁——它保存了对该次调用第一个参数的引用(参见 first-arg.md)。这里的“引用”一词值得注意:它直接指向原始参数值本身,而非拷贝,因此对复杂对象(如对象、数组、函数)使用时,比较的是引用与原始值。

获取 spyCall 对象的五种途径

要读取firstArg,首先需要拿到某个 spyCall 对象。Sinon 提供了以下常用途径:

1.getCall(n):按索引取第 n 次调用

fake.getCall(n)返回第n次调用的 spyCall(索引从 0 开始)。在 proxy.js 的实现中,getCall还支持负数索引——-1表示从最后一次调用往前数:

const f = sinon.fake(); f("a"); f("b"); f("c"); f.getCall(0).firstArg; // "a" f.getCall(1).firstArg; // "b" f.getCall(-1).firstArg; // "c"(倒数第一次调用)

2.firstCall/lastCall:首尾调用快捷属性

proxy-call-util.js 中的createCallProperties会在每次调用后更新代理上的firstCall、secondCall、thirdCall与lastCall属性:

const f = sinon.fake(); f("apple pie", "banana pie"); f.lastCall.firstArg; // "apple pie" f.firstCall.firstArg; // "apple pie"

3.getCalls():批量获取全部调用

当需要遍历所有调用逐个检查firstArg时,getCalls()返回所有 spyCall 的数组。

4.secondCall/thirdCall:特定序号调用

与firstCall类似,secondCall与thirdCall分别指向第二、第三次调用(不存在时为null)。

5.this.args[0]:从代理级参数数组取首参

代理对象本身也有args(所有调用的参数数组的数组),f.args[0][0]等价于f.getCall(0).firstArg。但 spyCall 的firstArg语义更直观,且不受args二维结构影响。

基本行为:有参调用与无参调用

firstArg的取值规则由创建 spyCall 时的参数个数决定。参照 first-arg.test.js 中的官方测试:

import t from "tap"; import sinon from "sinon"; const f = sinon.fake(); // 有参数调用:firstArg 为第一个参数 f("apple pie", "banana pie"); t.equal(f.lastCall.firstArg, "apple pie"); // 通过 // 无参数调用:firstArg 为 undefined f(); t.equal(f.lastCall.firstArg, undefined); // 通过

两个关键结论:

  1. 有参数时,firstArg精确等于args[0];
  2. 无参数调用时,firstArg为undefined,不会抛出异常或返回null。

这一点在 fakes 基础用法测试 中也有印证:未传参数的 fake 调用后,fake.firstArg为undefined。因此,断言firstArg是否为undefined可以用来判断某次调用是否携带了参数。

结合其他 spyCall 属性完成组合断言

firstArg常常需要与args、lastArg等属性配合使用,才能准确描述一次调用:

const spy = sinon.spy(); spy("query", { page: 2 }, callback); const call = spy.lastCall; call.firstArg; // "query",首个参数 call.args; // ["query", { page: 2 }, callback],完整参数数组 call.args[0]; // 等价于 call.firstArg call.lastArg; // callback(最后一个参数)

特别地,lastArg与callback有关联:在 proxy-call.js 的创建逻辑中,若最后一个参数是函数,它同时会成为lastArg和callback。结合 callback 属性文档 可进一步提取回调。

在 fake、spy、stub 与 mock 上的统一表现

由于 fake、spy、stub 与 mock 方法最终都经由同一套 proxy 机制创建调用记录(proxy.js、proxy-invoke.js),firstArg的行为在四种对象上完全一致:

// spy:包装现有函数 const spy = sinon.spy((name) => `hello ${name}`); spy("world"); spy.lastCall.firstArg; // "world" // stub:预定义行为 const stub = sinon.stub().returns(1); stub("x", "y"); stub.lastCall.firstArg; // "x" // fake:独立假函数 const fake = sinon.fake.returns(42); fake(1, 2); fake.lastCall.firstArg; // 1 // mock 方法 const obj = { greet: () => {} }; const mock = sinon.mock(obj); const expectation = mock.expects("greet"); obj.greet("hi"); expectation.lastCall.firstArg; // "hi"(expectation 本身也是代理)

源码级原理:firstArg从何而来

理解firstArg的底层实现,能帮你预判各种边界情况。核心逻辑位于 proxy-call.js 的createProxyCall工厂函数:

export default function createProxyCall( proxy, thisValue, args, returnValue, exception, id, errorWithCallStack, ) { let firstArg, lastArg; if (args.length > 0) { firstArg = args[0]; lastArg = args[args.length - 1]; } const proxyCall = Object.create(callProto); // ... proxyCall.args = args; proxyCall.firstArg = firstArg; proxyCall.lastArg = lastArg; // ... return proxyCall; }

由此可以明确实现事实:

  • firstArg并非 getter 而是普通实例属性,在 spyCall 创建时一次性写入;
  • 它直接取自调用时记录的args[0],因此是对原参数的引用;
  • 当args.length === 0时,firstArg保持undefined(局部变量未赋值),不会额外处理;
  • 同理,lastArg为args[args.length - 1],两者只在args非空时才有值。

调用链完整回放(可对照 proxy-invoke.js):

  1. 代理函数被调用,invoke(func, thisValue, args)执行;
  2. 调用incrementCallCount并把args、thisValue、callId推入对应数组;
  3. 调用createCallProperties刷新firstCall/lastCall等快捷属性(内部调用getCall);
  4. 执行原函数,记录返回值/异常;
  5. 再次调用createCallProperties,使 spyCall 携带完整的returnValue、exception等。

因此每次访问f.lastCall.firstArg,实际上都会经由getCall(this.callCount - 1)实时构建一个新的 spyCall 对象并读取其firstArg属性。

典型实战场景

场景一:断言函数以特定参数被调用

const sendEmail = sinon.spy(); sendEmail("user@example.com", "欢迎注册", body); sinon.assert.calledWith(sendEmail, "user@example.com"); // 更细粒度:直接检查最后一次调用的首个参数 t.equal(sendEmail.lastCall.firstArg, "user@example.com");

场景二:从调用记录中提取回调参数

const fake = sinon.fake(); fake(1, 2, "done"); fake.lastCall.firstArg; // 1 —— 但若首参不是回调,应改用 callback / lastArg

注意:firstArg只承诺“第一个参数”,不保证它是函数。若需要提取回调,应使用 callback 属性(它指向最后一个函数参数)或yield/yieldTo系列方法。

场景三:多参数调用中的位置敏感断言

const f = sinon.fake(); f("GET", "/api/users", { limit: 10 }); const call = f.getCall(0); call.firstArg; // "GET" call.args[1]; // "/api/users" call.args[2]; // { limit: 10 }

注意事项与边界情况

  1. 无参数调用返回undefined:判断“是否携带参数”时请用firstArg !== undefined或检查args.length,不要依赖== null的宽松判断(undefined == null为真,会与显式传null混淆)。
  2. 显式传入undefined与不传参数不同:f(undefined)的firstArg是undefined(有参数但值为 undefined),而f()的firstArg也是undefined(无参数)。两者在firstArg上无法区分,需要时用args.length区分。
  3. 与代理级firstArg的区别:代理对象上也有firstArg属性(初始为null,见 proxy.js),它记录的是最近一次调用的首参;而spyCall.firstArg是某个特定调用的首参,二者不要混用。多调用场景下请优先使用lastCall.firstArg或getCall(n).firstArg。
  4. 重置后失效:调用resetHistory或sinon.reset()后,调用记录清空,lastCall变为null,此时访问null.firstArg会抛错,应先判空(参考 get-call.test.js 中getCall(1) === null的断言)。

相关文档速查

  • spy-call 概念总览:spyCall 的定位与getCall说明
  • spyCall API 索引:全部方法与属性列表
  • lastArg:与firstArg对称的末参属性
  • args:完整参数数组
  • spies 入门:spy 的创建与使用
  • fakes 入门:fake 的创建与使用

源码与测试参考:proxy-call.js(spyCall 工厂)、proxy.js(getCall)、proxy-call-util.js(调用属性维护)、first-arg.test.js(官方行为测试)。

  • 测试
  • 开发工具

【免费下载链接】sinon

Test spies, stubs and mocks for JavaScript.

项目地址:https://gitcode.com/gh_mirrors/si/sinon
点击查看免费下载
上一篇:Promptify与智能搜索:提升搜索引擎的相关性
下一篇:Ever® Traduora标签系统使用指南:如何组织和分类翻译

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/25 4:56:52

四路CAN FD与LTE远程调试:汽车电子逆向工程实战利器

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/25 4:53:29

边缘AI芯片选型实战:从场景反推算力、功耗与内存带宽

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/25 4:53:21

Delphi 13.1 跨平台开发:TMS FNC UI Pack 源码版安装与多端实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/25 4:52:16

HG680-LC刷机教程:S905L3B升级安卓9,解锁全网通去广告

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华