前言
FizzBuzz 大概是最广为人知的程序设计题目之一,它的题目是这样的:
- 打印出
1
到100
的数字- 假如数字是
3
的倍数,则打印Fizz
- 假如数字是
5
的倍数,则打印Buzz
- 假如数字是
3
和5
的公倍数,则打印FizzBuzz
- 假如数字是
解题
基本解
最直接的解法即是制作一个函数会打印 1 ~ 100
的数字,并依照规则判断是否要打印 Fizz
、Buzz
或 FizzBuzz
。但明显这样的做法会将所有数值都写死在程序当中,未来要扩充条件时就需要修改代码,尚不够灵活。
function fizzBuzz(max) { for (let i = 1; i <= max; i++) { if (i % 3 === 0 && i % 5 === 0) { console.log('FizzBuzz'); } else if (i % 3 === 0) { console.log('Fizz'); } else if (i % 5 === 0) { console.log('Buzz'); } else { console.log(i); } }}
fizzBuzz(100);
分离数据与逻辑
既然可以预期规则都是 某数字
要打印 某结果
那么或许可以使用对象来记录这些 Key-Value 结构的数据:
function fizzBuzz(max) { const map = { 3: 'Fizz', 5: 'Buzz', 15: 'FizzBuzz', };
for (let i = 1; i <= max; i++) { if (i % Object.keys(map)[0] === 0 && i % Object.keys(map)[1] === 0) { console.log(Object.values(map)[2]); } else if (i % Object.keys(map)[0] === 0) { console.log(Object.values(map)[0]); } else if (i % Object.keys(map)[1] === 0) { console.log(Object.values(map)[1]); } else { console.log(i); } }}
数据是抽离出来了,但逻辑还是依赖指定 map
对象当中的特定内容,让我们再写个循环自动的将 map
对象的内容取出来,让数据来驱动逻辑:
function fizzBuzz(max) { const map = { 3: 'Fizz', 5: 'Buzz', };
for (let i = 1; i <= max; i++) { let output = ''; for (const key in map) { if (i % key === 0) { output += map[key]; } } console.log(output || i); }}
保持数值不变(Immutable)
以上解法可以观察到目前定义了 i
以及 output
两个变量并且于程序中持续的变动其内容,在某些程序开发风格当中会被视为应当避免的习惯,我们可以尝试看看将覆盖变量的部分改为纯粹函数。
numberReplacer(100, { 3: 'Fizz', 5: 'Buzz',}).forEach((value) => console.log(value));
function numberReplacer(max, replacementMap) { return Array.from({ length: max }, (_, index) => { const currentNumber = index + 1; const replacement = Object.entries(replacementMap).reduce( (acc, [divisor, replaceWord]) => (currentNumber % divisor === 0 ? acc + replaceWord : acc), '', ); return replacement || currentNumber; });}
补齐文档与检查边界案例
JavaScript 是动态类型语言,因此其他人在使用这个函数时可能不清楚具体参数的需求,这里通过 JSDoc 来补齐文档,也可以考虑使用 TypeScript 来检测类型或于 Runtime 进行类型检测:
/** * 生成一数组,将 1 到 max 之间的数字取代为指定字符串 * * @param {number} max - 最大数值 * @param {Object<number, string>} replacementMap - key 为除数,value 为取代字符串 * @returns {Array<string | number>} - 取代后的数值 * @throws {TypeError} 如 max 不是正整数 * @throws {TypeError} 如 replacementMap 不是非空对象 * @throws {TypeError} 如 replacementMap 内的 key 不是正整数 * @example * numberReplacer(15, { 3: 'Fizz', 5: 'Buzz' }); * // Returns [1, 2, 'Fizz', 4, 'Buzz', 'Fizz', 7, 8, 'Fizz', 'Buzz', 11, 'Fizz', 13, 14, 'FizzBuzz'] */function numberReplacer(max, replacementMap) { if (!Number.isInteger(max) || max <= 0) { throw new TypeError('max 必须是正整数'); }
if (typeof replacementMap !== 'object' || replacementMap === null) { throw new TypeError('replacementMap 必须是非空对象'); }
return Array.from({ length: max }, (_, index) => { const currentNumber = index + 1; const replacement = Object.entries(replacementMap).reduce((acc, [divisor, replaceWord]) => { const numDivisor = Number(divisor); if (!Number.isInteger(numDivisor) || numDivisor <= 0) { throw new TypeError('replacementMap 内的 key 必须是正整数'); } return currentNumber % numDivisor === 0 ? acc + replaceWord : acc; }, ''); return replacement || currentNumber; });}
try { numberReplacer(100, { 3: 'Fizz', 5: 'Buzz', }).forEach((value) => console.log(value));} catch (error) { console.error(error.message);}
通过局部应用来简化 numberReplacer
你可能会想说:老天!只是要印个 FizzBuzz
为什么每次都要通过 numberReplacer
来计算?有没有办法单纯创个函数只传入最大值就好?就像一开始一样?让我们用局部应用函数来通过创造抽象达成简化:
const createFizzBuzz = (max) => { return numberReplacer(max, { 3: 'Fizz', 5: 'Buzz', });};const fizzBuzz100 = createFizzBuzz(100);fizzBuzz100.forEach((value) => console.log(value));
const fizzBuzz15 = createFizzBuzz(15);fizzBuzz15.forEach((value) => console.log(value));
在这次解题过程中我贯彻 DRY 原则,并且通过不断的重构来提升代码的可读性与可维护性,但也可以思考真的有必要制造更多抽象吗?过早的优化是万恶之源,或许 YAGNI。