这里有一个老模式,似乎正在卷土重来:
// Convert some numbers into human-readable strings:
import { toReadableNumber } from 'some-library';
const readableNumbers = someNumbers.map(toReadableNumber);
其中toReadableNumber 的实现是这样的:
export function toReadableNumber(num) {
// Return num as string in a human readable form.
// Eg 10000000 might become '10,000,000'
}
一切都很好,直到some-library 被更新,然后一切都坏了。但这不是some-library 的错--他们从未将toReadableNumber 设计成对array.map 的回调。
问题就在这里:
// We think of:
const readableNumbers = someNumbers.map(toReadableNumber);
// …as being like:
const readableNumbers = someNumbers.map((n) => toReadableNumber(n));
// …but it's more like:
const readableNumbers = someNumbers.map((item, index, arr) =>
toReadableNumber(item, index, arr),
);
我们也在向toReadableNumber 传递数组中项目的索引,以及数组本身。这在一开始很正常,因为toReadableNumber 只有一个参数,但在新版本中:
export function toReadableNumber(num, base = 10) {
// Return num as string in a human readable form.
// In base 10 by default, but this can be changed.
}
toReadableNumber 的开发者认为他们在做一个向后兼容的改变。他们增加了一个新的参数,并给它一个默认值。然而,他们没有想到,有些代码已经在调用有三个参数的函数了。
toReadableNumber 并不是设计成对 的回调,所以安全的做法是创建你自己的函数,设计array.map成与 。array.map
const readableNumbers = someNumbers.map((n) => toReadableNumber(n));
就这样了!toReadableNumber 的开发者现在可以在不破坏我们代码的情况下增加参数。
同样的问题,但在网络平台函数方面
这是我最近看到的一个例子:
// A promise for the next frame:
const nextFrame = () => new Promise(requestAnimationFrame);
但这相当于:
const nextFrame = () =>
new Promise((resolve, reject) => requestAnimationFrame(resolve, reject));
这在今天是可行的,因为requestAnimationFrame 只对第一个参数做一些事情,但这可能不会永远是真的。可能会增加一个额外的参数,而上述代码可能会在任何浏览器的更新requestAnimationFrame 。
这种模式出错的最好例子可能是:
const parsedInts = ['-10', '0', '10', '20', '30'].map(parseInt);
如果有人在技术面试中问你这个问题的结果,我建议你揉揉眼睛走出去。但无论如何,答案是[-10, NaN, 2, 6, 12] ,因为parseInt 有第二个参数。
选项对象也会有同样的问题
Chrome 90将允许你使用一个AbortSignal 来移除事件监听器,这意味着一个单一的AbortSignal 可以用来移除事件监听器,取消获取,以及其他任何支持信号的东西:
const controller = new AbortController();
const { signal } = controller;
el.addEventListener('mousemove', callback, { signal });
el.addEventListener('pointermove', callback, { signal });
el.addEventListener('touchmove', callback, { signal });
// Later, remove all three listeners:
controller.abort();
然而,我看到了一个例子,其中没有:
const controller = new AbortController();
const { signal } = controller;
el.addEventListener(name, callback, { signal });
...他们这样做:
const controller = new AbortController();
el.addEventListener(name, callback, controller);
和回调的例子一样,这在今天是可行的,但在将来可能会失效。
AbortController 并不是被设计成addEventListener 的一个选项对象。它现在是有效的,因为唯一的东西 AbortController和addEventListener 选项的共同点是signal 属性。
如果,比如说在将来,AbortController 得到一个controller.capture(otherController) 方法,你的监听器的行为就会改变,因为addEventListener 会在capture 属性中看到一个真实的值,而capture 是addEventListener 的一个有效选项。
和回调的例子一样,最好是创建一个旨在成为addEventListener 选项的对象:
const controller = new AbortController();
const options = { signal: controller.signal };
el.addEventListener(name, callback, options);
// Although I find this pattern easier when multiple things
// get the same signal:
const { signal } = controller;
el.addEventListener(name, callback, { signal });
就这样吧!注意函数被用作回调,对象被用作选项,除非它们是为这些目的而设计的。不幸的是,我不知道有什么提示规则能抓住它(编辑:看起来有一条规则能抓住一些情况,谢谢James Ross!)。
TypeScript并不能解决这个问题
编辑:当我第一次发布这篇文章时,它在结尾处有一个小注解,显示TypeScript并不能防止这个问题,但还是有朋友在Twitter上告诉我 "只要使用TypeScript",所以让我们更详细地看一下。
TypeScript不喜欢这样:
function oneArg(arg1: string) {
console.log(arg1);
}
oneArg('hello', 'world');
// ^^^^^^^
// Expected 1 arguments, but got 2.
但它对这个却很好:
function twoArgCallback(cb: (arg1: string, arg2: string) => void) {
cb('hello', 'world');
}
twoArgCallback(oneArg);
...尽管结果是一样的:
因此,TypeScript对这个没有意见。
function toReadableNumber(num): string {
// Return num as string in a human readable form.
// Eg 10000000 might become '10,000,000'
return '';
}
const readableNumbers = [1, 2, 3].map(toReadableNumber);
如果toReadableNumber ,增加了第二个字符串参数,TypeScript会抱怨,但这并不是例子中发生的事情。一个额外的数字参数被添加,而这符合类型约束。
在requestAnimationFrame 这个例子中,事情变得更糟,因为这是在浏览器的新版本部署后出的问题,而不是在你的项目的新版本部署后出的问题。此外,TypeScript的DOM类型往往落后于浏览器发货的时间。
我是TypeScript的粉丝,这个博客也是用TypeScript建立的,但是它并没有解决这个问题,也许也不应该解决。
大多数其他类型的语言在这里的行为与TypeScript不同,不允许以这种方式铸造回调,但TypeScript在这里的行为是故意的,否则它就会拒绝以下情况。
const numbers = [1, 2, 3];
const doubledNumbers = numbers.map((n) => n * 2);
...因为给map 的回调被投到一个有更多参数的回调。以上是JavaScript中极为常见的模式,而且完全安全,所以TypeScript为其破例是可以理解的。
问题是 "这个函数是否打算成为map 的回调?",而在JavaScript的世界里,这并不是真的可以用类型解决的。相反,我想知道新的JS和网络函数如果被调用时有太多的参数,是否应该抛出。这将 "保留 "额外的参数供将来使用。我们不能在现有的函数中这样做,因为这将破坏向后的兼容性,但我们可以在现有的函数中显示控制台警告,我们可能想在以后添加参数。我提出了这个想法,但人们似乎对它不太感兴趣😀。
当涉及到选项对象时,事情就有点棘手了:
interface Options {
reverse?: boolean;
}
function whatever({ reverse = false }: Options = {}) {
console.log(reverse);
}
你可以说,如果传递给whatever 的对象有除reverse 以外的其他属性,API应该警告/失败。但是在这个例子中:
whatever({ reverse: true });
......我们传递的对象有toString,constructor,valueOf,hasOwnProperty 等属性,因为上面的对象是Object 的一个实例。要求这些属性是 "自己的 "属性似乎太有限制性了(目前在运行时不是这样的),但也许我们可以为Object 自带的属性增加一些津贴。