Skip to content

5. 魔法函数(函数与模块化)

知识点:函数与模块化

项目:把散落各处的重复代码,装进"魔法盒子"


故事开场:三份程序,一样的烦恼

这几天你写了三个程序:

  • 点名器:用 Math.floor(Math.random() * names.length) 随机选一个人
  • 密信:用循环把一句话倒过来(encrypt 函数)
  • 画星星:用循环往一条线上加星星

今天老师让你把它们合在一起,做一个"班级工具盒":一运行, 先随机点一个名,再输出一句用密信写的鼓励语,最后画一排星星。

你正要把代码拼在一起,忽然发现了问题:

ts
// 点名器里的随机
const index = Math.floor(Math.random() * names.length)
const picked = names[index]

// 密信里的加密
let reversed = ""
for (let i = text.length - 1; i >= 0; i--) {
  reversed = reversed + text[i]
}

咦,这两段代码,好像都很眼熟?而且你想给"班级工具盒"加一个新功能: 随机给同学分配号码牌——号码要 1 到 50 之间随机。

于是你又写了:

ts
const number = Math.floor(Math.random() * 50) + 1

三段代码,都在做"随机"这件事,却长得不一样。更麻烦的是——

老师突然说:"随机点名不够刺激,我们要用班牌号点名,不是用名字!"

你要把点名器里的随机逻辑改掉。可是……"随机"这件事散落在三个程序里, 你记得清到底要改哪几处吗?


笨办法先行:复制粘贴的代价

复制粘贴看起来省事,但它有一个隐藏的陷阱——代码会"分家"

  • 你在点名器里把随机范围从 50 改成 51(新同学来了)
  • 但号码牌程序里的 50 忘了改
  • 结果:点名器说"51 号同学",号码牌却永远抽不到 51 号

程序一大,你根本记不清"同样的逻辑"散落在哪里。改一处,漏一处,就是 bug 的开始。

这时候你需要一个东西:把"随机选一个"这件事,写成一段只写一次、 哪里都能调用的代码。在 TypeScript 里,它叫函数(function)

等等——你在第 3 章已经写过函数了(encrypt)!函数一直都在, 只是我们还没有认真认识它。这一章,我们把它变成你的"魔法盒子"。


引出知识点:函数就是魔法盒子

一个函数,就像一只魔法盒子:

  • 从左边丢进去:参数(arguments)——盒子需要的原料
  • 盒子里面:函数体——魔法发生的地方(一段代码)
  • 从右边吐出来:返回值(return value)——处理好的结果
ts
function 盒子名字(原料) {
  // 魔法代码
  return 结果
}

你已经在用函数了:Math.floor(...)Math.random()console.log(...) 都是函数——只是它们是别人写好的。现在,你来写自己的。

函数最重要的好处:写一次,用无数次;改一次,处处生效。 "随机选一个同学"和"随机抽一个号码",其实是同一只盒子 (随机选下标)配上不同的原料(数组)而已。


动手实现:班级工具盒

先把"随机"这只盒子做出来——名字要取得像"咒语"一样清楚:

ts
// 魔法盒子一号:从数组里随机挑一个
function randomPick(list: string[]): string {
  const index = Math.floor(Math.random() * list.length)
  return list[index]
}

// 魔法盒子二号:随机 1~n 的整数
function randomNumber(max: number): number {
  return Math.floor(Math.random() * max) + 1
}

注意看 randomPick 的签名:list: string[] 是说"我要一个字符串数组", ): string 是说"我吐出一个字符串"。类型标注就像盒子的说明书—— 别人一看就知道怎么用。

现在把以前的代码统统改造成"调用盒子":

ts
const names = ["小明", "小红", "小刚", "小雨", "小丽", "小华"]

// 点名:喊出盒子
console.log("今天的幸运同学是:" + randomPick(names))

// 号码牌:另一个盒子
console.log("你的号码牌是:" + randomNumber(50))

输出(每次运行不一样):

今天的幸运同学是:小丽
你的号码牌是:17

新同学来了?只要把名字加进 names 数组,点名的代码一行都不用改—— 因为"随机挑一个"这件事被装进了盒子,它自己会数 list.length


跑起来:升级魔法盒子

现在,把第 3 章、第 4 章的魔法也收进盒子,组合成完整的"班级工具盒":

ts
// 魔法盒子三号:把一句话倒过来(来自密信章)
function reverseText(text: string): string {
  let reversed = ""
  for (let i = text.length - 1; i >= 0; i--) {
    reversed = reversed + text[i]
  }
  return reversed
}

// 魔法盒子四号:画一排星星(来自画星星章)
function drawStars(count: number): string {
  let line = ""
  for (let i = 0; i < count; i++) {
    line = line + "★"
  }
  return line
}

// 班级工具盒:组合所有盒子
const lucky = randomPick(names)
console.log("今天的幸运同学是:" + lucky)
console.log("给你的密信:" + reverseText("今天也要加油"))
console.log(drawStars(randomNumber(10)))

输出(每次运行不一样):

今天的幸运同学是:小刚
给你的密信:油加要也天今
★★★★★★★

注意到没有——drawStars(randomNumber(10)) 里,盒子的输出直接喂给了 另一只盒子!函数的好处就在这里:一只盒子的输出,可以当另一只盒子的输入。 你把四段小魔法拼成了一台大机器,而每一段魔法都只写了一次。


小挑战

  1. 最小值和最大值:写两个盒子 min(a, b)max(a, b), 返回两个数里较小/较大的那个(不许用 Math.min/Math.max)。 再用它们找出 372 三个数里最大的(提示:max(max(3, 7), 2))。
  2. 温度转换:写一个盒子 celsiusToFahrenheit(c), 把摄氏度转成华氏度(公式:c × 9 ÷ 5 + 32), 再写一个反向的盒子,两个盒子互相验证:转过去再转回来,数字不变。
  3. 积木塔:用第 4 章的三角形代码,封装成盒子 drawTriangle(rows), 然后调用它画 3 行、5 行、8 行的三个三角形——同一个盒子,不同的大小

课后练习

第 1 题(动手题):我的第一个盒子

写一个盒子 add(a, b),返回两个数相加的结果。 再写 subtract(a, b)multiply(a, b)divide(a, b), 用它们计算 (10 + 5) × 2 − 3 ÷ 3,输出结果。

参考答案(点开查看)
ts
function add(a: number, b: number): number {
  return a + b
}
function subtract(a: number, b: number): number {
  return a - b
}
function multiply(a: number, b: number): number {
  return a * b
}
function divide(a: number, b: number): number {
  return a / b
}

const result = subtract(multiply(add(10, 5), 2), divide(3, 3))
console.log(result)  // 29

盒子套盒子:add(10, 5) 是 15,multiply(15, 2) 是 30, divide(3, 3) 是 1,subtract(30, 1) 是 29。

第 2 题(思考题):盒子里的"隐身人"

看这段代码,猜猜输出是什么,再运行验证:

ts
function changeNumber(x: number): number {
  x = x + 100
  return x
}

let age = 10
const result = changeNumber(age)
console.log("盒子外面:" + age)
console.log("盒子里面:" + result)
参考答案(点开查看)

输出:

盒子外面:10
盒子里面:110

age 是 10,丢进盒子后盒子把它改成了 110,但盒子外面的 age 还是 10

因为盒子拿到的是"10 的复印件",不是"age 本人"——函数里的 x 是一个 新变量,它在盒子里怎么折腾,都不会影响外面的 age。这叫作 按值传递(pass by value)。这个特性很安全:你的盒子不会 "顺手改掉"外面的变量。

第 3 题(思考题):盒子的说明书

randomPick 的签名写的是 list: string[]。如果传给它一个 数字数组 [1, 2, 3],会怎么样?运行试试。

参考答案(点开查看)

TypeScript 会直接报错:string[]number[] 不是同一种类型, 在程序运行之前编译器就会拦住你。

这就是类型标注的价值:错误在写代码时就被发现,而不是在运行时才爆炸。 想让它支持任何数组?以后你会学到"泛型"(<T>),现在先记住: 类型就是盒子的说明书,说明书对不上,盒子就拒绝工作。


预告:基础篇到这里就结束了!你已经会数步数(复杂度)、会用数组和字符串、 会循环、会函数——你已经是一个"入门小算法家"了。 下一卷《数据结构篇》,我们要做更酷的事:给程序的"撤销"按钮、叫号机、 还有那个 1 秒钟就能找到人的"闪电查找"。