更多好文,欢迎关注公众号
Geek技术前线
默认参数值在JavaScript中已经存在一段时间了。但其实可以将前面的相邻参数作为默认值本身。
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);// 返回之前优化过的图像。if (cached) return cached;const optimizedImage = await this.optimizeService.optimize(this.imageUrl);// 将优化后的图像缓存以备下次使用。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
。但想象一下,如果它们需要在自己的构造函数中传入这个参数,我们可能会想把实例化过程放入 OptimizedImage
的构造函数中
class OptimizedImage {constructor(imageUrl: string) {this.imageUrl = imageUrl;this.cacheService = new CacheService(imageUrl);this.optimizeService = new OptimizeService(imageUrl);}}
这样是可行的,但现在 OptimizedImage
完全负责服务的实例化,测试也变得更加麻烦。传入服务实例的模拟对象并不容易。
我们可以通过传入模拟类定义来解决这个问题,但这就需要为这些类创建带有自己构造函数的模拟版本,使得测试变得更加繁琐。幸运的是,还有另一种选择:在其余参数列表中使用 imageUrl
参数。
共享相邻参数
它的实现方式如下:
export class OptimizedImage {constructor(imageUrl: string,// 在两个依赖中使用相同的 `imageUrl`。cacheService = new CacheService(imageUrl),optimizeService = new OptimizeService(imageUrl)) {this.cacheService = cacheService;this.optimizeService = optimizeService;}async get() {const cached = this.cacheService.get();// 返回之前优化过的图像。if (cached) return cached;const optimizedImage = await this.optimizeService.optimize();// 将优化后的图像缓存以备下次使用。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);
参考 https://macarthur.me/posts/sibling-parameters/