从零开始学前端 | 第十四章:函数与代码复用 本章定位前面几章里你已经开始真正写“有逻辑”的 JavaScript 代码了。你已经学习了JavaScript 是什么代码在哪里运行。变量、数据类型与运算。条件判断与循环。到这里你已经能写出一些会存值判断计算重复执行的代码。但这时候通常会出现一个新的问题同一段逻辑我好像要写很多次。例如计算多个商品的总价多次输出欢迎语多次格式化同一类文本多个地方都需要做相似判断如果每次都重新从头写一遍代码很快就会变得重复很多不容易修改容易写乱这时候函数就登场了。这一章要解决的核心问题是如何把一段逻辑封装起来给它起个名字然后在需要的时候反复使用。这也是“代码复用”最基础、最重要的一步。本章学习目标学完这一章后你应该能做到理解函数是什么以及为什么程序需要函数。掌握函数的定义和调用。理解参数的作用知道函数如何接收不同输入。理解返回值的作用知道函数如何把结果交出来。知道为什么要把重复代码提炼成函数。理解作用域的基础概念知道函数内外的变量可见范围不同。能写出“计算总价”“格式化姓名”这类简单可复用函数。一、为什么程序需要函数函数存在不是为了把代码写得“更高级”而是为了帮你解决一个非常实际的问题重复逻辑不要每次都重写。1. 没有函数时重复代码会怎样假设你要计算三个商品的总价constproductAPrice99;constproductAQuantity2;constproductATotalproductAPrice*productAQuantity;console.log(productATotal);constproductBPrice59;constproductBQuantity3;constproductBTotalproductBPrice*productBQuantity;console.log(productBTotal);constproductCPrice199;constproductCQuantity1;constproductCTotalproductCPrice*productCQuantity;console.log(productCTotal);这段代码当然可以运行但你会发现几个问题逻辑几乎一模一样。代码越来越长。如果计算规则变了要改很多地方。这就是函数最适合出场的时候。2. 函数像是“带名字的小工具”你可以先把函数理解成一段被单独封装起来、可以重复使用的代码。它很像你给一套固定操作做了一个小工具。例如计算总价打印欢迎语格式化姓名只要这套操作未来还会再用就值得考虑封装成函数。3. 函数在做两件非常重要的事1. 复用代码写一次多次调用。2. 让逻辑更容易读当你看到calculateTotalPrice()往往会比直接看到一大段重复计算更容易理解。二、函数到底是什么函数可以先用一句最直白的话记住函数就是把一段逻辑包起来然后在需要时执行它。它通常会包含下面几部分函数名参数可选函数体返回值可选你现在不用一下子把这四个概念都吃透后面我们会一个个展开。1. 本章先统一使用function写法JavaScript 里创建函数有不止一种写法。但对纯小白来说这一章我们先统一使用最基础、最直观的一种function函数名(){// 代码}这样会更容易把“函数名、参数、返回值、作用域”这些基础概念看清楚。后面如果需要再接触其他写法会更稳。三、最基础的函数定义与调用先学函数时最容易混淆的两件事是定义函数调用函数这两个不是一回事。1. 什么叫“定义函数”定义函数就是先把这段逻辑准备好并给它起一个名字。例如functionsayHello(){console.log(你好欢迎学习函数);}这里你只是把函数准备好了。2. 什么叫“调用函数”调用函数才是真正让这段逻辑执行。例如sayHello();这一行才会让函数里的代码跑起来。3. 一个完整的最小例子functionsayHello(){console.log(你好欢迎学习函数);}sayHello();这段代码会输出你好欢迎学习函数4. 为什么“定义了函数但没调用”是常见问题很多初学者第一次写函数时会写出functionsayHello(){console.log(你好);}然后发现控制台什么都没有。原因不是函数坏了而是你只是定义了函数还没有调用它。所以一定要先把这层区别分清定义把函数准备好调用让函数执行四、为什么说函数能复用代码这是本章的核心价值之一。1. 一个重复代码的例子假设你要连续输出三次欢迎语console.log(欢迎来到前端学习世界);console.log(欢迎来到前端学习世界);console.log(欢迎来到前端学习世界);这种写法虽然简单但已经出现重复了。2. 提炼成函数之后你可以改成functionprintWelcomeMessage(){console.log(欢迎来到前端学习世界);}printWelcomeMessage();printWelcomeMessage();printWelcomeMessage();这样做有两个明显好处欢迎语只写了一次。以后如果文案要改只需要改函数内部。3. 复用不是“偷懒”而是更好的组织方式初学阶段有时会觉得这几行代码也不长为什么还要包成函数因为函数不只是为了减少敲字更是为了明确逻辑边界降低重复让代码更容易维护这就是“代码复用”的本质价值。五、参数是什么到这里你可能会发现一个问题刚才的函数虽然可以重复执行但它每次做的都是完全一样的事。那如果我想让函数根据不同输入产生不同结果怎么办这就需要参数。你可以先把参数理解成函数接收外部输入的入口。1. 一个最简单的带参数函数functionsayHello(userName){console.log(你好${userName});}这里的userName就是参数。调用时你可以传入不同值sayHello(小白);sayHello(小明);输出结果分别会是你好小白 你好小明这说明同一个函数因为输入不同结果也可以不同。2. 参数像是函数的“输入口”如果把函数想成一个小机器那参数就像输入口。你给它不同输入它就按同样规则处理这些输入。例如输入不同姓名输出不同欢迎语3. 多个参数怎么写函数当然不只可以接收一个参数。例如functionintroduceUser(userName,cityName){console.log(你好我是${userName}我来自${cityName});}调用时introduceUser(小白,北京);introduceUser(小林,上海);4. 参数顺序为什么重要这一点很值得初学者尽早注意。例如functionintroduceUser(userName,cityName){console.log(你好我是${userName}我来自${cityName});}如果你调用时写成introduceUser(北京,小白);那结果就会变成你好我是北京我来自小白也就是说参数位置通常是有意义的调用时顺序不能乱。5. 先记住两个术语就够了你后面可能会看到形参实参现在你可以先做一个非常基础的对应1. 写在函数定义里的是“参数名字”例如functionsayHello(userName){console.log(userName);}这里的userName是定义时写好的参数位置。2. 调用函数时传进去的是真正的值例如sayHello(小白);这里的小白才是这次真正传进去的内容。初学阶段你不用被术语绊住先把“参数位置”和“传入内容”区分开就够了。六、返回值是什么到目前为止你学的很多函数都只是“做一件事然后在内部打印出来”。但函数还有一种非常重要的能力把处理结果交回函数外面。这就叫返回值。1.return的作用是什么return可以先理解成把函数处理出来的结果带出去。例如functionaddNumbers(numberA,numberB){returnnumberAnumberB;}这个函数不是直接把结果打印死在里面而是把结果返回出去。2. 调用后怎么拿到返回值例如functionaddNumbers(numberA,numberB){returnnumberAnumberB;}constresultaddNumbers(10,20);console.log(result);这里的执行过程是调用addNumbers(10, 20)函数内部计算10 20用return把结果30带出来把这个结果交给result3.console.log和return不是一回事这是初学者特别容易混淆的一点。例如下面这个函数functionaddNumbers(numberA,numberB){console.log(numberAnumberB);}它确实会在控制台里显示结果但它并没有把结果“交出来”给外部继续使用。而下面这种写法functionaddNumbers(numberA,numberB){returnnumberAnumberB;}才是真正把结果返回出去。你可以先记一句非常重要的话console.log是打印出来给你看return是把结果交出来继续用。4. 为什么返回值很重要因为很多时候我们不是只想“看一眼结果”而是还想拿这个结果继续做别的事。例如保存到变量里再参与下一步计算再传给另一个函数这时候返回值就非常关键。5.return后面的结果通常就作为函数结果例如functiongetWelcomeMessage(userName){return你好${userName};}constmessagegetWelcomeMessage(小白);console.log(message);这里函数最终交出来的就是你好小白七、函数要尽量只做一件事这是写函数时一个非常重要的习惯。尤其对初学者来说它能帮你少踩很多坑。你可以先记住一个函数尽量只负责一件明确的事。1. 为什么这样做更清楚如果一个函数里同时做太多事就容易变成一半在计算一半在判断一半在打印一半在改变量最后自己也很难看懂这个函数到底在负责什么。2. 一个“不太清楚”的例子例如functionhandleOrder(price,quantity){consttotalPriceprice*quantity;console.log(总价是${totalPrice});if(totalPrice200){console.log(超过 200 元);}else{console.log(未超过 200 元);}}这个函数能不能运行能。但它同时在做计算总价输出总价判断预算输出判断结果对于当前阶段来说这已经有点挤在一起了。3. 一个更清楚的拆法你完全可以先拆成职责更单纯的函数。例如functioncalculateTotalPrice(price,quantity){returnprice*quantity;}functionisOverBudget(totalPrice){returntotalPrice200;}这样会更容易理解一个函数负责算总价一个函数负责判断是否超预算这就是“函数尽量只做一件事”的价值。八、函数名应该怎么取函数名非常重要因为函数名本身就在告诉别人这段逻辑是做什么的。1. 函数名尽量表达动作和意图推荐这种风格calculateTotalPrice formatUserName printWelcomeMessage isOverBudget这些名字的共同特点是看名字就大概知道它在干什么通常像一个动作或一个判断2. 不推荐太随意的函数名例如doIt test aaa fn1这些名字虽然浏览器能识别但对读代码的人帮助很小。3. 为什么函数名特别值得认真取因为当项目变大后别人往往先看到的不是函数内部而是函数名。如果名字取得好很多代码会在还没展开细节前就已经更容易读懂。九、什么是作用域作用域是函数学习里一个必须接触的基础概念。你可以先把它理解成一个变量在什么范围内可以被访问。也就是说不是所有变量都能在所有地方使用。1. 全局位置定义的变量例如constsiteName从零开始学前端;functionprintSiteName(){console.log(siteName);}printSiteName();这里的siteName是定义在函数外面的。当前阶段你可以先把它理解成它在外层所以函数内部也能用到它。2. 函数内部定义的变量例如functionprintChapterInfo(){constchapterName函数与代码复用;console.log(chapterName);}printChapterInfo();这里的chapterName是定义在函数内部的变量。它可以在这个函数内部被使用。3. 为什么函数内部变量不能随便拿到外面用如果你写functionprintChapterInfo(){constchapterName函数与代码复用;console.log(chapterName);}printChapterInfo();console.log(chapterName);最后一行通常会报错。因为chapterName只在这个函数内部有效。这就是作用域在起作用。4. 为什么作用域很重要因为它能帮助程序把不同位置的数据边界分清楚。如果所有变量都能在所有地方随便改代码会很容易混乱。所以初学阶段你先记住一个非常重要的基础规则函数内部定义的变量通常只在函数内部使用。十、一个完整示例封装“计算总价”函数下面我们做一个非常贴近前面练习内容的完整示例。目标是把“总价计算”封装成可复用函数。1. 示例代码functioncalculateTotalPrice(unitPrice,quantity,shippingFee){// 先计算商品总价再加上运费最后把结果返回出去constproductTotalPriceunitPrice*quantity;returnproductTotalPriceshippingFee;}constorderATotalPricecalculateTotalPrice(99,2,10);constorderBTotalPricecalculateTotalPrice(59,3,12);console.log(订单 A 总价${orderATotalPrice});console.log(订单 B 总价${orderBTotalPrice});2. 这个函数解决了什么问题它把原本可能重复写很多次的逻辑单价乘数量再加上运费统一封装到了一个函数里。以后只要计算规则不变你就可以一直复用它。3. 怎么逐步读懂这段代码你可以按下面顺序读1. 先看函数接收什么输入单价unitPrice数量quantity运费shippingFee2. 再看函数内部怎么处理先算unitPrice*quantity得到商品总价。再加shippingFee得到最终总价。3. 最后看函数把什么交出来它通过return把最终总价返回了出去。所以外部可以这样接constorderATotalPricecalculateTotalPrice(99,2,10);十一、一个完整示例封装“格式化姓名”函数再来看一个更偏文本处理的例子。目标是把姓名组合逻辑提炼成函数。1. 示例代码functionformatFullName(lastName,firstName){return${lastName}${firstName};}constuserAFullNameformatFullName(王,小白);constuserBFullNameformatFullName(李,小林);console.log(userAFullName);console.log(userBFullName);2. 这个例子在练什么它虽然简单但已经同时练到了多个参数返回值模板字符串代码复用3. 为什么这种小函数很值得练因为函数学习初期最适合从这种“小而明确”的任务开始。例如组合姓名计算总价判断是否及格生成欢迎语这些任务边界清楚很适合帮助你建立对函数的直觉。十二、函数和前面学过的知识是怎么连起来的走到这里你会发现函数并不是一个完全新的孤立知识点。它其实是在把前面学过的内容组织起来。1. 函数里会继续使用变量例如参数本身就是变量位置函数内部也可以定义变量2. 函数里会继续处理数据类型例如数字做计算字符串做拼接布尔值做判断3. 函数里会继续用到条件判断和循环例如你后面完全可以写判断成绩等级的函数输出固定规律的函数所以函数不是要替代前面知识而是在帮你把前面知识封装起来、重复利用。十三、初学者最容易踩的坑这一章的坑也很典型而且很多人会反复遇到。1. 定义了函数但忘了调用只写functionsayHello(){console.log(你好);}是不会自动输出内容的。你还要真正调用sayHello();2. 以为console.log就等于返回值再强调一次console.log是打印return是返回结果如果你后面还要继续使用某个结果通常更应该考虑return。3. 参数名和值混在一起理解例如functionsayHello(userName){console.log(你好${userName});}这里的userName是参数位置不是固定只能等于某一个人。真正传进去的值是调用时写的内容sayHello(小白);4. 一个函数里塞太多事情这是初学者很容易出现的问题。刚学函数时很容易觉得既然都写成函数了那干脆把一堆事都塞进去。但这样会让函数越来越难懂。当前阶段更推荐你先做到一个函数尽量只负责一件明确的事。5. 在函数外面使用函数内部变量比如functionprintChapterInfo(){constchapterName函数;}console.log(chapterName);这通常会报错。因为这个变量只在函数内部有效。6. 函数名没有表达动作例如functiontest(){}functiondoIt(){}这种名字很难帮助人理解逻辑。更推荐functioncalculateTotalPrice(){}functionformatFullName(){}十四、本章实践练习这一章最适合通过“小函数练习”建立感觉。重点不是一次写很复杂而是先把函数的边界想清楚。1. 练习 1封装“计算总价”函数请你自己写一个函数至少接收单价数量运费然后返回最终总价。你可以先自己写再检查是否满足有函数名有参数有返回值2. 练习 2封装“格式化姓名”函数请写一个函数接收姓名然后返回完整姓名。例如formatFullName(王,小白);返回王小白3. 练习 3把重复代码提炼成函数请回头看看你前面写过的 JavaScript 小练习找一找有没有这种情况同一段输出写了很多次同一套计算逻辑写了很多次同一种格式化操作写了很多次然后尝试把其中一段提炼成函数。这个练习特别重要因为它最接近“真实代码重构”的第一步。4. 练习 4写几个不同输入、不同输出的小函数你可以尝试写下面这些非常小的函数1.getDoubleNumber(number)接收一个数字返回它的两倍。2.getWelcomeMessage(userName)接收用户名返回欢迎语。3.isPass(score)接收成绩返回它是否大于等于 60。这些函数都很短但非常适合练习参数返回值命名单一职责十五、学习重点提示这一章最重要的不是把函数语法硬背下来而是真正理解为什么一段逻辑值得被单独封装成函数。请重点记住函数是把一段逻辑封装起来在需要时重复使用。定义函数和调用函数不是一回事。参数是函数接收输入的入口。返回值是函数把处理结果交回外部的方式。console.log是打印结果return是返回结果。函数尽量只做一件事这样更容易理解和复用。函数名尽量表达动作和意图。函数内部变量通常只在函数内部有效这和作用域有关。如果你只记一句话请记住函数的价值不只是“少写几行代码”而是把逻辑组织得更清楚、让重复工作真正可以复用。十六、本章小结这一章你已经从“会写一段逻辑”走到了“会把一段逻辑封装起来反复使用”。你已经学习了函数是什么为什么要封装函数函数的定义与调用参数和返回值的作用函数命名与单一职责作用域的基本概念如何把重复代码提炼成可复用函数这一步非常关键。因为从这一章开始你的代码不再只是“能跑的一段段片段”而是开始慢慢具备更清楚的结构。后面当你继续学习数组、对象、DOM、事件和更复杂的交互逻辑时函数几乎会一直陪着你。可以说函数是 JavaScript 中最重要的组织工具之一。十七、课后思考题请你思考下面几个问题为什么函数特别适合解决“重复逻辑”问题为什么说定义函数和调用函数不是一回事参数和返回值分别在函数中扮演什么角色为什么console.log和return不能混为一谈为什么“一个函数尽量只做一件事”会让代码更清楚为什么函数内部变量不能随便拿到函数外面使用建议你用自己的话把答案写下来。只要你能把这些问题说清楚说明你已经真正开始掌握“函数思维”了。十八、下一章预告下一章我们会继续学习从零开始学前端 | 第十五章数组与对象到那时你会开始接触如何组织“多个值”和“带结构的数据”这会让你的 JavaScript 从处理单个变量进一步走向处理更真实的数据世界。

本月热点