做前端开发时,你可能遇到过这样的需求:两个动画周期分别是8秒和12秒,想知道它们多久后会同时回到起点;或者计算分数加法时需要找公分母。这些问题的本质都是求最小公倍数。JavaScript的Math对象提供了很多数学函数,却没有内置LCM方法,所以我们需要自己实现。这篇文章带你从原理到代码,完整掌握最小公倍数的计算方法。

一、理解最小公倍数与最大公约数的关系
在写代码之前,先弄清楚数学原理。两个整数a和b的最小公倍数,是指能同时被a和b整除的最小正整数。比如4和6的公倍数有12、24、36等,其中最小的是12。
最小公倍数有一个非常重要的性质:两个数的乘积等于它们的最大公约数(GCD)与最小公倍数的乘积。用公式表达就是 a × b = GCD(a, b) × LCM(a, b)。由此可以推导出LCM的计算公式:
// LCM(a, b) = (a * b) / GCD(a, b)
function lcm(a, b) {
return (a * b) / gcd(a, b);
}这样一来,求LCM的问题就转化成了求GCD的问题。而求GCD有一个经典的算法——辗转相除法(欧几里得算法),它基于这样一个事实:两个数的最大公约数等于其中较小的数与两数相除余数的最大公约数。不断用除数去除被除数取余,直到余数为0,此时的除数就是最大公约数。
举例说明:求48和18的GCD。48除以18余12,接着18除以12余6,再12除以6余0,所以GCD是6。整个算法只用了三次除法,效率远高于穷举法,即使数字很大也能快速得出结果。
二、用辗转相除法实现LCM函数
下面是完整的实现代码。GCD部分使用循环写法,避免递归带来的函数调用开销:
// 求最大公约数(辗转相除法)
function gcd(a, b) {
while (b !== 0) {
const temp = b;
b = a % b;
a = temp;
}
return a;
}
// 求最小公倍数
function lcm(a, b) {
return (a * b) / gcd(a, b);
}
// 测试
console.log(lcm(4, 6)); // 12
console.log(lcm(8, 12)); // 24
console.log(lcm(7, 13)); // 91(互质的两个数,LCM就是它们的乘积)这段代码逻辑清晰,但有一个隐藏的坑需要注意:当a和b都很大时,a * b的结果可能超出Number.MAX_SAFE_INTEGER(即2的53次方减1),导致精度丢失。比如计算两个很大的质数的LCM时,乘法运算的结果已经不是精确值了。
解决方法是调整运算顺序,先做除法再做乘法。由于GCD一定能整除a,先除后乘不会产生小数,同时把中间结果控制在较小范围内:
// 优化版:先除后乘,避免大数相乘溢出
function lcmSafe(a, b) {
return (a / gcd(a, b)) * b;
}
// 测试大数场景
console.log(lcmSafe(1000000007, 999999937)); // 结果依然精确另外,如果输入可能包含0或负数,还需要做边界处理。数学上规定任何数与0的LCM为0,负数则通常先取绝对值:
// 带边界处理的健壮版本
function lcmRobust(a, b) {
a = Math.abs(a);
b = Math.abs(b);
if (a === 0 || b === 0) return 0;
return (a / gcd(a, b)) * b;
}三、扩展:一次计算多个数的最小公倍数
实际业务中经常需要求多个数的LCM,比如找出3个、5个甚至更多个数的公共周期。数学上有个重要性质:LCM具有结合性,即 LCM(a, b, c) = LCM(LCM(a, b), c)。利用这个性质,可以用reduce不断累积计算:
// 计算多个数的最小公倍数
function lcmOfArray(arr) {
return arr.reduce((acc, cur) => lcmRobust(acc, cur));
}
// 示例:求动画周期的同步时间点
const periods = [8, 12, 20];
console.log(lcmOfArray(periods)); // 120,三个动画每120秒同步一次reduce的初始值是数组第一个元素,然后依次把当前累积结果与下一个数求LCM。由于每次都先除以GCD再相乘,中间结果始终保持在能整除的最小范围内,整个累积过程不会出现精度问题。
如果习惯函数式风格,也可以把GCD写成递归形式,配合箭头函数让代码更紧凑:
// 递归写法配合箭头函数 const gcd = (a, b) => b === 0 ? a : gcd(b, a % b); const lcm = (a, b) => a / gcd(a, b) * b; const lcmAll = (...nums) => nums.reduce((acc, n) => lcm(acc, n)); console.log(lcmAll(4, 6, 8, 14)); // 168
递归版GCD代码量最少,可读性也不错。不过当数字规模特别大时,递归深度理论上不会超过对数量级,性能上和循环版几乎没差别,选择哪种写法主要看团队的代码风格偏好。
四、常见问题与性能对比
有人可能想到用穷举法:从较大的数开始逐个尝试,找到第一个能同时整除两个数的值。这种写法虽然容易理解,但时间复杂度是O(a×b/GCD),当两个数互质且很大时,循环次数会呈线性增长,性能完全无法和辗转相除法的O(log min(a, b))相比。生产代码中应始终优先选择辗转相除法。
还有一种思路是分别对两个数做质因数分解,取每个质因数的最高次幂相乘。这个方法在数学上成立,但实现质因数分解本身就需要大量计算,代码复杂度也更高,除非题目明确要求展示分解过程,否则不推荐。
总结一下推荐做法:用循环版辗转相除法求GCD,采用先除后乘的顺序计算LCM,对输入做绝对值和零值处理,最后用reduce扩展到多数场景。这一套组合拳既保证了计算精度,又兼顾了性能和代码健壮性,可以直接复制到项目工具库中使用。
JavaScript最小公倍数LCM算法修改时间:2026-09-15 13:38:32