JavaScript 自 ES2015 起就支持默认参数值。你知道这一点。我也知道。我不知道的是,你可以使用之前的同级参数作为默认值。(或者叫 "相邻位置参数"?)
function
myFunc(arg1, arg2 = arg1) {
console.log(arg1, arg2);
}
myFunc("arg1!");
// "arg1!" "arg1!"
MDN 甚至指出了这一点(我是在发表这篇文章后才发现的),并演示了该功能如何帮助处理一些不寻常的函数签名。
场景
继续以图像优化为主题,假设你有一个 OptimizedImage 类。向它的构造函数提供一个图片 URL,你就可以获取图片的最新优化缓冲区或缓存版本。
class
OptimizedImage
{
constructor(
imageUrl: string,
cacheService = new CacheService(),
optimizeService = new OptimizeService()
) {
this.imageUrl = imageUrl;
this.cacheService = cacheService;
this.optimizeService = optimizeService;
}
async
get() {
const cached = this.cacheService.get(this.imageUrl);
// Return the previously optimized image.
if (cached) return cached;
const optimizedImage = await
this.optimizeService
.optimize(this.imageUrl);
// 堆代码 duidaima.com
// Cache the optimized image for next time.
return
this.cacheService.put(this.imageUrl, optimizedImage);
}
}
const instance = new OptimizedImage('<https://macarthur.me/me.jpg>');
const imgBuffer = await instance.get();
生产中使用的构造函数参数只有 imageUrl ,但注入 CacheService 和 OptimizeService 可以更方便地使用模拟进行单元测试:
import { it, expect, vi } from
'vitest';
import { OptimizedImage } from
'./main';
it('returns freshly optimized image', async
function () {
const fakeImageBuffer = new
ArrayBuffer('image!');
const mockCacheService = {
get: (url) =>
null,
put: vi.fn().mockResolvedValue(fakeImageBuffer),
};
const mockOptimizeService = {
optimize: (url) => fakeImageBuffer,
};
const optimizedImage = new OptimizedImage(
'<https://test.jpg>',
mockCacheService,
mockOptimizeService
);
const result = await optimizedImage.get();
expect(result).toEqual(fakeImageBuffer);
expect(mockCacheService.put).toHaveBeenCalledWith(
'<https://test.jpg>',
'optimized image'
);
});
使问题更加复杂
在该示例中,这两个服务类只有在调用特定方法时才会使用 imageUrl 。但试想一下,如果它们要求将 imageUrl 传递到自己的构造函数中,那又会怎样呢?你可能会倾向于将实例化引入 OptimizedImage 的构造函数(我就是这样做的):
class
OptimizedImage
{
constructor(
imageUrl: string
) {
this.imageUrl = imageUrl;
this.cacheService = new CacheService(imageUrl);
this.optimizeService = new OptimizeService(imageUrl);
}
这也行得通,但现在 OptimizedImage 完全负责服务实例化,测试也变得更加麻烦。为服务实例传递模拟数据也不是那么容易。你可以通过传递模拟类定义来解决这个问题,但这样一来,你就需要为这些类创建具有自己构造函数的 mock 版本,从而使测试变得更加繁琐。幸运的是,还有另一种选择:在参数列表的其余部分使用 imageUrl 参数。
共享兄弟参数
直到不久前,我才意识到这是可能的。这就是它的样子:
export
class
OptimizedImage
{
constructor(
imageUrl: string,
// Use the same `imageUrl` in both dependencies.
cacheService = new CacheService(imageUrl),
optimizeService = new OptimizeService(imageUrl)
) {
this.cacheService = cacheService;
this.optimizeService = optimizeService;
}
async
get() {
const cached = this.cacheService.get();
// Return the previously optimized image.
if (cached) return cached;
const optimizedImage = await
this.optimizeService.optimize();
// Cache the optimized image for next time.
return
this.cacheService.put(optimizedImage);
}
}
有了这个设置,你就可以像以前一样轻松地模拟这些实例,而类的其他部分甚至不需要持有 imageUrl 的实例。当然,实例化仍然很简单:
const instance = new OptimizedImage('<https://macarthur.me/me.jpg>');
const img = await instance.get();
测试方法也保持不变:
import { it, expect, vi } from
'vitest';
import { OptimizedImage } from
'./main';
it('returns freshly optimized image', async
function () {
const mockCacheService = {
get: () =>
null,
put: vi.fn().mockResolvedValue('optimized image'),
};
const mockOptimizeService = {
optimize: () =>
'optimized image',
};
const optimizedImage = new OptimizedImage(
'<https://test.jpg>',
mockCacheService,
mockOptimizeService
);
const result = await optimizedImage.get();
expect(result).toEqual('optimized image');
expect(mockCacheService.put).toHaveBeenCalledWith('optimized image');
});
这里没有什么革命性的东西--只是一种使特定问题的解决更符合人体工程学的功能。
其他案例
除了这种情况,我还没有遇到过这种功能特别方便的用例。不过,也许会有一些机会来加强你的工作保障,并树立你不败的聪明才智。
考虑用 .reduce() 计算一个数组的和:
const numbers = [1, 2, 3, 4, 5];
const total = numbers.reduce((acc, value) => {
return acc + value;
});
console.log(total); // 15
这个过程的 "业务" 存在于函数体中。不过,你也可以把它作为默认参数塞进函数签名中,让函数体只返回结果。
const total = numbers.reduce(
(acc, value, _index, _array, result = acc + value) => {
return result;
}
);
而且,它还可以使用隐式 return 来显得更加引人注目:
const total = numbers.reduce((acc, value, _index, _array, result = acc + value) => result);
看起来很奇怪。从我自己的经验来看,用处不大,但还是很有趣。