如何将数字限制在一个区间内,最优雅的方法是什么?

201

假设有三个数字xab。我需要将x限制在区间[a, b]内。

换句话说,我需要一个夹紧函数

clamp(x) = max( a, min(x, b) )

有没有人可以想出一个更易读的版本?

10个回答

248

您可以使用标准方法来完成。您可以定义一个clamp实用函数:

/**
 * Returns a number whose value is limited to the given range.
 *
 * Example: limit the output of this computation to between 0 and 255
 * (x * 255).clamp(0, 255)
 *
 * @param {Number} min The lower boundary of the output range
 * @param {Number} max The upper boundary of the output range
 * @returns A number in the range [min, max]
 * @type Number
 */
Number.prototype.clamp = function(min, max) {
  return Math.min(Math.max(this, min), max);
};

(虽然扩展语言内置功能通常不被看好)


61
出于多种原因,扩展本地原型(在这种情况下是Number.prototype)是不可取的。请参见“不良实践:扩展本机原型” https://developer.mozilla.org/en-US/docs/Web/JavaScript/Inheritance_and_the_prototype_chain - broofa
10
JavaScript本身应该包含许多这些函数。开发者不应因弥补语言的不足而受到责备。 - default123
8
开发人员并不因为“弥补语言的缺陷”而受到不满,而是因为以错误或粗心的方式进行。 扩展原型是错误的,并且应该出于许多好的理由而避免,特别是当正确方法像使用函数一样容易时。 const clamp =(num,min,max)=> Math.min(Math.max(num,min),max) - Aurelio
12
应该扩展Math而不是Number - vsync

92

这是一个不那么“数学”取向的方法,但应该也可以工作。这种方法暴露了 < / > 的测试(可能比最小/最大值更易懂),但真正是否可读取,这取决于您所指的含义。

function clamp(num, min, max) {
  return num <= min 
    ? min 
    : num >= max 
      ? max 
      : num
}

5
最好用“<=”和“>=”来替换这个习语中的“<”和“>”,这样可能会少做一次比较。有了这个更正,这绝对是我首选的答案:最小值的最大值令人困惑且容易出错。 - Don Hatch
8
不更快。Math.min/max方法是最快的解决方案。 https://jsperf.com/math-clamp/9 - Manuel Di Iorio
1
@ManuelDiIorio 嗯?在当前版本的Chrome下,简单三元运算符的速度是Math.min/max的两倍 - 如果你去掉更昂贵的Math.random()调用的开销,这种简单方法会快10倍。 - mindplay.dk
6
使用 Math.min/max 函数的解决方案是最快的,可以消除 Math.random() 调用的额外开销。这个三元运算符的解决方案要慢80%。 - idbrii
哇,Safari在这些统计数据中发生了什么事情?奇怪的是,在Safari中三元解决方案要快得多。 - bummzack
显示剩余3条评论

75

有一个提案,即向内置的Math对象添加附加内容来执行此操作:

Math.clamp(x, lower, upper)

但请注意,截至今日,它是一个第一阶段的提案。 在它被广泛支持之前(这并不保证),您可以使用一个polyfill


4
如果您想追踪这个提案和其他提案,请参考:github.com/tc39/proposals - gfullam
2
对于那些寻找的人,该提案在第1阶段提案下列出为“数学扩展”。实际提案在这里:https://github.com/rwaldron/proposal-math-extensions - WickyNilliams
24
2020年冬季浏览器仍不可用! - gabry
@gabry - 如果你的项目使用Babel,你可以使用Babel Math polyfill - vsync

59

一个简单的方法是使用

Math.max(min, Math.min(number, max));

你可以明显地定义一个包装这个功能的函数:

function clamp(number, min, max) {
  return Math.max(min, Math.min(number, max));
}

起初,这个答案还将上面的函数添加到全局的 Math 对象中,但那是过去时代的遗物,因此已被移除(感谢 @Aurelio 的建议)。


2
这个解决方案实际上要快得多,尽管当使用函数时,我认为扩展prototype非常优雅。 - MaxArt
1
这是2012年的答案,因此可以理解实现扩展原型,但必须注意它现在被广泛认为是一种不良做法。Math.max(min, Math.min(number, max))(相关部分)仍然有效并值得点赞,因此@CAFxX是否考虑编辑答案并避免原型突变?我认为一些你会得到的赞被转移到了这个答案。 - Aurelio
就个人而言,我更喜欢这样的方式,让数值按顺序排列: Math.min(Math.max(MIN, VALUE), MAX),这样更容易记住:min+max 函数和 min value max 参数。 - Alex Yaroshevich

18

我不想给你一个“只用库”的答案,但是如果你使用的是Lodash,你可以使用.clamp函数:

_.clamp(yourInput, lowerBound, upperBound);

因此:

_.clamp(22, -10, 10); // => 10

以下是它的实现,取自 Lodash 源代码

/**
 * The base implementation of `_.clamp` which doesn't coerce arguments.
 *
 * @private
 * @param {number} number The number to clamp.
 * @param {number} [lower] The lower bound.
 * @param {number} upper The upper bound.
 * @returns {number} Returns the clamped number.
 */
function baseClamp(number, lower, upper) {
  if (number === number) {
    if (upper !== undefined) {
      number = number <= upper ? number : upper;
    }
    if (lower !== undefined) {
      number = number >= lower ? number : lower;
    }
  }
  return number;
}

此外,值得注意的是Lodash将单个方法作为独立模块提供,因此如果您只需要这个方法,可以安装它而无需安装整个库:
npm i --save lodash.clamp

8
比较 number === number 的目的是什么?它总是评估为 true 吗? - Jivan Pal
7
在 JavaScript 中,当且仅当值为 NaN 时,表达式 number === number 的结果为 false。在处理数字逻辑时,通常需要处理这种情况。而 Object.is(number, number) 始终返回 true。 - Rafi
2
@Rafi,谢谢 - JavaScript 与其他语言相比,其独特性总是让我感到惊奇。 - Jivan Pal
2
为什么不直接使用 isNaN 而非使用 === 呢?哈哈 - Justin
1
值与值的比较可以在多种编程语言中检测NaN,而且不会有任何性能损失。 - Alexander Shostak
显示剩余2条评论

10

如果您能使用ES6箭头函数,也可以使用部分应用程序方法:

const clamp = (min, max) => (value) =>
    value < min ? min : value > max ? max : value;

clamp(2, 9)(8); // 8
clamp(2, 9)(1); // 2
clamp(2, 9)(10); // 9

or

const clamp2to9 = clamp(2, 9);
clamp2to9(8); // 8
clamp2to9(1); // 2
clamp2to9(10); // 9

2
每次想夹住一个数字就创建一个函数看起来非常低效。 - George
2
可能取决于您的用例。这只是一个工厂函数,用于创建具有设置范围的夹子。您可以创建一次并在整个应用程序中重复使用。不必多次调用。此外,在您不需要锁定设置范围的所有用例中,它也可能不是适当的工具。 - bingles
1
@George 如果您不需要保留范围,只需删除内部箭头函数并将值参数移动到外部箭头函数即可。 - Alynva

7

如果您不想定义任何函数,那么像Math.min(Math.max(x, a), b)这样写也不错。


6
用户@CAFxX(上面)在你之前6年回答了相同的问题。 - vsync

5

将三元操作扩展为if/else,缩小后与三元操作等效但不牺牲可读性。

const clamp = (value, min, max) => {
  if (value < min) return min;
  if (value > max) return max;
  return value;
}

最小化后的文件大小为35字节(如果使用function,则为43字节):

const clamp=(c,a,l)=>c<a?a:c>l?l:c;

同时,根据你使用的性能工具或浏览器不同,无论是基于数学实现还是三元实现,都会得到不同的结果。如果两者大致相同,则应选择易读性更好的实现。


1
在箭头函数的精神下,您可以使用剩余参数创建微型夹子/约束/门等功能。
var clamp = (...v) => v.sort((a,b) => a-b)[1];

然后只需传入三个值。
clamp(100,-3,someVar);

那就是说,如果你说“性感”,那意思是“短”吗?

3
如果你的“夹子”函数需要使用复杂逻辑(即使它很“性感”),你不应该使用数组和排序,因为这会严重影响性能。 - Andrew McOlash

-8

我最喜欢的:

[min,x,max].sort()[1]

21
因为它不起作用所以被踩了。[1,5,19].sort()[1] 返回的是 19。可以通过这种方式修复:[min,x,max].sort(function(a, b) { return a - b; })[1],但这样就不那么简洁明了了。此外,当频繁使用时,创建一个新数组来比较三个数字可能会影响性能。 - kayahr
3
在我看来,这种方式比其他任何选项都更易读,特别是使用箭头函数:[min, x, max].sort((a,b) => a-b)[1] - zaius
7
这种方法不总是有效的原因是排序方法不能正确比较数字值(它会将它们视为字符串)。 - John Leuenhagen
@JohnLeuenhagen 现在是什么?!? - Sreenikethan I
1
是的,不幸的是,JavaScript 有相当多的令人讨厌的“坑”,像这样的问题。JavaScript 程序员必须知道它们并绕过它们。 - John Leuenhagen
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接