在函数中使用兄弟参数作为默认值

82 阅读3分钟

原文链接:macarthur.me/posts/sibli…

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);

    // 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);

看起来很奇怪。从我自己的经验来看,用处不大,但还是很有趣。