
学JS第五天回头看第三天绝对是第一个真正的分水岭。前面两天跟变量、数据类型、运算符这些名词解释较劲顶多算是认字到了函数才算真正开始学造句。当时我在笔记里写了一句如果你觉得某个功能要在好几个地方用到别复制粘贴写个函数。这句话现在看来平平无奇但对零基础的人来说能主动从抄代码切换到封装代码本质上是一次编程思维的升级。这篇笔记记录了我在Day03从零啃函数的完整经过包括我一开始完全看不懂的三种写法、参数传值时的各种翻车现场、以及第一次接触作用域和闭包时的懵圈状态。内容按我当天的学习顺序整理每一步都有代码和踩坑记录。适合跟我一样从零开始、正在学JS基础或者学了一段时间但函数这块总觉得没吃透的朋友参考。1. 函数的本质把一段代码装进盒子随用随取1.1 为什么非要用函数第三天刚开头我看了一个购物的例子计算一件商品打折后的价格。不讲代码先说要做什么。如果不写函数我得在页面上每个地方都写一遍原价乘以折扣再四舍五入。如果折扣规则变了要把每个地方都找到、都改一遍漏了一个就出bug。而函数的思路非常朴素把打折计算这段逻辑放进一个盒子盒子开一个口参数告诉它原价多少、折扣多少盒子再开一个口返回值把它算好的结果递出来以后想在哪儿算就把盒子调过来用一次代码角度函数的字面意思就是封装一段可复用的逻辑接收输入经过处理返回输出。它是JS里最基础的抽象工具也是后续学数组方法、事件处理、异步回调、框架组件的基础。我可以很负责任地说一句函数没搞懂后面的DOM操作、Vue或React全都寸步难行。这也是我为什么把Day03定位成分水岭的原因。1.2 我的第一个函数从三次复制粘贴到一次封装当天练习题目很简单输入两个数返回较大的那个。先看不用函数的做法let a 3; let b 5; if (a b) { console.log(较大的是 a); } else { console.log(较大的是 b); }这段代码写一次、两次还没感觉但当我需要比较五组数字时就得把这段if else复制五遍变量名还得各起各的。这时我就开始意识到重复代码不仅丑更危险。最后我把它封装成了函数function getMax(a, b) { if (a b) { return a; } else { return b; } } console.log(getMax(3, 5)); // 5 console.log(getMax(99, 88)); // 99 console.log(getMax(-1, -2)); // -1这个过程给我的启发是写函数不是为了显得高级而是为了消除重复。判断什么时候该封装的标准也很简单——同一段逻辑出现了两次以上就值得考虑提成函数。这也是我后来写代码的基本习惯先能跑再提炼。1.3 函数声明与提升机制那天下午我发现了第一个看不懂的现象我把函数调用写在了函数定义的前面代码居然也能正常运行。sayHello(小明); function sayHello(name) { console.log(你好 name); }这就涉及一个知识点函数声明会提升。JS引擎在正式执行前会先把函数声明整体搬到当前作用域的最顶部所以你在哪一行调用都行。这个机制叫函数提升。说白了就是JS帮我们做了预整理但初学者很容易把能跑误解成顺序无所谓。实际上只有函数声明这种写法才有这个待遇换成函数表达式就完全不同这个我在下一章详细说。注意早期我为了图省事习惯把所有函数调用都放在代码前面甚至一个JS文件里随意穿插。后来项目文件变大发现这种写法非常难维护。建议从初学阶段起就保持先定义、后调用的书写习惯哪怕JS允许你反过来。2. 写函数的三种姿势声明、表达式、箭头函数2.1 函数表达式函数也只是一个值刚学完函数声明教程紧接着抛出一个概念函数在JS里本身就是一个值。我不太理解什么叫值后来明白了JS里数字是值、字符串是值、数组是值函数同样也是一种可以赋给变量的值。const getMax function(a, b) { if (a b) { return a; } else { return b; } };这就是函数表达式。它和函数声明最大的区别在于函数声明有名字能被提示提升函数表达式是给一个匿名函数起变量名它不会被提升。如果在定义之前调用控制台会报错TypeError: getMax is not a function。因为此时变量还没被赋值。这里有个坑我当时踩得特别实在。我把函数表达式赋值给const变量后想重新给变量赋另一个函数结果报错了。原因是const声明的变量不能重新赋值。如果想替换函数得用letlet getMax function(a, b) { return a b ? a : b; };这样一个细节带给我的认知是函数表达式归根结底是用变量装着函数值遵循的是变量的规则作用域、可赋值性而函数声明遵循的是语句的规则提升、独占一行。理解了这层区别后面看很多源码都不容易迷糊。2.2 箭头函数简洁到像变了个语言箭头函数是ES6里我最喜欢、也最容易写错的写法。它还叫胖箭头因为用的是 符号而不是单纯的 -。先看对照// 函数表达式 const add function(a, b) { return a b; }; // 箭头函数 const add (a, b) { return a b; }; // 还能更简短有返回值且只有一行可以把{}和return一起省略 const add (a, b) a b;第一次看到这种写法时我的反应是这写的什么中间的function去哪了但适应之后我反而觉得箭头函数更接近输入-输出的本质左边是参数列表右边是返回结果。但我同时记了一条重要提醒箭头函数不是简单的简写它和普通函数还有一些本质差异。比如它没有自己的this没有arguments也不能当作构造函数用。我第一天只求会用等学到this的变化规律再回到这一章对比理解效果才真正出来。建议初学阶段先老老实实把函数声明和函数表达式练熟箭头函数可以看、可以抄但不要第一时间全换成箭头函数。等理解了this之后再主动切换否则出bug时根本没法排查。2.3 三种写法怎么选我把当天的感受总结成了一张表不是标准答案但新手参考够用了。写法是否提升this的表现适用场景函数声明是由调用方式决定全局工具函数、回调函数直观函数表达式否由调用方式决定按条件动态选择函数时箭头函数否继承外层this回调、数组方法、不需要自己的this时在实际项目里我现在的习惯是独立功能用函数声明值传递、回调函数用箭头函数需要动态指派的用函数表达式。但Day03当天我只做了一件事不限写法把每个功能都写成函数。先把能把代码写成函数练成本能再谈风格。3. 参数与返回值函数与外界的数据通道3.1 实参、形参与缺失参数学函数绕不开两个词形参和实参。我当时记的理解方式是形参相当于函数内部定义的占位符是函数声明时用来接收数据的参数名实参是调用时真正传入的实际数据。// a、b是形参 function add(a, b) { return a b; } // 3和5是实参 let result add(3, 5);这个还算好理解但紧接着我试了一个少传参数的情况add(3)。结果返回值是NaN因为b的值为undefinedundefined加数字自然就是NaN。JS不像有些语言强制校验参数个数少传的情况不会直接报错但运算结果会变得很诡异。这让我养成一个习惯调试代码时如果发现输出是NaN、undefined拼在字符串里之类的情况第一个要怀疑的就是某个函数没接收到正确的参数。后来我还会用一个技巧在函数开头打印一下所有参数确认它们到底是什么再往下查。3.2 默认参数与剩余参数为了处理可能少传参数的问题ES6给函数增加了默认参数能力function greet(name 朋友) { console.log(你好 name); } greet(); // 你好朋友 greet(小明); // 你好小明这个语法非常直观也很好用。唯一的坑是只要实参不是undefined默认值就不会生效。比如greet(null)会输出你好null哪怕你在语义上并不想接收null。所以如果需要把null当作没有传参还得自己在函数体内加判断。剩余参数的场景我也记了一个例子写一个函数接收任意多个数字返回它们的总和。如果不知道剩余参数会想到用arguments但arguments是个长得像数组但不是数组的类数组对象不能直接用数组的reduce方法。剩余参数就把这个事变得很顺手function sum(...numbers) { return numbers.reduce((total, num) total num, 0); } console.log(sum(1, 2, 3)); // 6 console.log(sum(1, 2, 3, 4, 5)); // 15这里面的numbers是一个真正的数组可以直接用数组方法。这个体验恰好帮我理解了rest参数把散装的实参收集成一个数组的含义。3.3 返回值忘写return的翻车现场Day03我遇到的第一起诡异bug就出在返回值上。我写了一个函数里面console.log能正常输出但用变量接收返回值时得到的是undefined。function double(num) { let result num * 2; } console.log(double(4)); // undefined原因简单到让人脸红函数没有return自然没有任何东西返回。但我觉得这个教训值得反复讲因为它太容易犯了。尤其在写了箭头函数、还习惯省略{}和return的时候稍微多写一行语句整段返回逻辑就消失了。后来我养成了一个检查习惯写函数时先明确这个函数到底要不要向外输出值。如果只需要执行操作比如打印、修改页面那可以不用return如果需要拿返回值继续使用return千万别漏。顺序上return之后的所有代码都不会执行这一点也很重要。4. 作用域、闭包与执行上下文第一次被绕晕4.1 全局作用域与局部作用域变量去哪找Day03后半段开始上难度了我从会写函数进入理解函数背后的机制。第一个概念是作用域它决定了一个变量在哪些区域能被访问到。我用一句话记住的作用域就是变量能生效的范围。JS中每个函数会创建自己的局部作用域函数外面是全局作用域。内部可以访问外部的变量但外部不能访问内部的变量。let globalName 全局; function show() { let localName 局部; console.log(globalName); // 能访问外部变量全局 } show(); console.log(localName); // ReferenceError: localName is not defined这段代码第一次正常打印、第二次报错让我一下就理解了外部拿不到内部变量。基于这个规则我总结出两条经验定义变量尽量靠近使用它的地方不要全堆到全局否则变量容易被无意覆盖函数内部如果给变量赋值但没有用let声明JS会默认把它当成全局变量来用这是很低级的坑4.2 闭包函数悄悄记住了自己出生的地方第二天我试着理解这个让无数新手头皮发麻的定义闭包就是函数和它外部作用域的组合。后来我用另一个方式想通了函数在出生的时候会记住自己所在的环境包括外部变量哪怕它后来被搬到别的地方调用它还能访问那些变量。看一个最经典的计数例子function createCounter() { let count 0; return function() { count; return count; }; } let counter createCounter(); console.log(counter()); // 1 console.log(counter()); // 2 console.log(counter()); // 3普通情况下createCounter调用结束count应该被清理掉。但因为它内部返回的函数还在引用countcount就没被回收每次调用都能基于上一次的值继续增加。这就是闭包的核心效果延长了变量的存储范围。第一次理解到这里我最大的困惑不是语法而是不知道这有什么用。后来遇到防抖、节流、模块化、组件封装等场景回头才明白闭包无处不在。Day03我建议的新手任务是能复现上面这个计数器的运行结果并且能解释count为什么能持续累加先不追求应用。4.3 闭包循环陷阱经典到几乎所有教程都讲当我试图用闭包写依次输出0到4时不熟悉var和let区别的我翻了大跟头for (var i 0; i 5; i) { setTimeout(function() { console.log(i); }, 100); }期望输出0、1、2、3、4实际连续输出5个5。这个现象在我第一次看到时半天想不通。后来明白过程是这样的var声明的i是函数级作用域整个for循环就只有一个i变量setTimeout里的回调函数到100毫秒后才执行那时循环早结束i已经变成5五个回调访问的是同一个i所以全是5。改成let能解决因为let是块级作用域每轮循环都会创建独立的i绑定for (let i 0; i 5; i) { setTimeout(function() { console.log(i); }, 100); }这个例子让我真正理解了变量捕获这件事。它属于函数中比较深入的机制新手遇到时很容易怀疑自己是不是不适合学编程——我也有过那样的心情但请相信这只是规律没讲透时的正常现象。把上面两步反复跑几次亲手把5个5改成0到4比看十遍文章都有用。5. 回调函数、IIFE与this函数最有魅力的套路5.1 回调函数把函数当作参数传递回调函数的概念一句话就能讲清楚把一个函数作为参数传给另一个函数在合适的时候被调用。我在Day03就已经接触到它了因为数组的forEach就是典型例子。let numbers [1, 2, 3]; numbers.forEach(function(item) { console.log(item); });这里传给forEach的这个函数就是一个回调函数。它不马上执行而是等待forEach在遍历每个元素时逐个调用。这跟我之前写的自己写、自己调的函数完全不同回调函数是写好规则交给别人来调。理解这个思维转变很重要。因为JS里大量异步操作事件监听、网络请求、定时器本质上都是等某个时机到了再执行回调。我Day03还没到异步先靠forEach和setTimeout记住了这个范式把一个函数传给另一个函数执行时机由后者说了算。5.2 IIFE定义完立刻执行并且不留变量污染全称是Immediately Invoked Function Expression立即执行函数表达式。写法如下(function() { let secret 外部拿不到我; console.log(立即执行); })();它的特征是函数定义完马上调用并且函数内部变量不会泄漏到全局。在没有模块化的年代这个写法大量用于封装插件。现在写法变多了但它帮我理解了一个关键点函数作用域是天然隔离变量的容器。后来做项目我需要临时处理一段逻辑又不想污染全局变量时第一时间就会想到用IIFE包一层。5.3 this我在Day03只记住规则不试图精通this绝对是我往后好几天都在挣扎的内容。Day03当天我的态度很务实先记住最常见的几种规则不要求一次全懂。在普通函数中this指向调用它的对象例如obj.method()里的this指向obj在严格模式下普通函数独立调用时this是undefined而不是全局对象箭头函数没有自己的this它继承外层作用域let person { name: 小明, sayName: function() { console.log(this.name); } }; person.sayName(); // 小明这里为什么能输出小明因为sayName被person这个对象调用了所以this指向person。如果把这个函数拿出来单独调用this可能就不再是person了。关于this我给自己的建议是遇到不确定就在代码里打印一下this马上就能验证不要靠猜。6. 我给Day03做的练习清单与避坑清单6.1 六个让我真正掌握函数的练习我的经验是看再多教程都不如亲手敲一遍。当天我给自己布置了几个小任务从易到难写一个函数接收一个数字n返回1到n的和把求最大值的逻辑封装成函数并分别用函数声明、函数表达式、箭头函数实现写一个判断字符串是否包含某个字符的函数大小写不敏感练一下参数和返回值用回调风格给数组做一遍筛选写一个自己的myFilter用闭包写一个薪水计数器每次调用加100外部无法直接修改底薪修复5.3里forsetTimeout输出全是5的问题前两个练语法第三个练参数处理第四个练函数作为参数的思维第五个练作用域与闭包第六个练排查问题。做完这一套函数的基础就算站住了。我自己做这些题的时候习惯是每写完一个小函数就在控制台先跑出结果再故意改出一两个错误比如漏return、拼错参数名观察报错信息长什么样。这个方法很笨但对培养报错敏感度特别有效。6.2 踩坑大合集记下来就不会再犯下面这些坑是我Day03当天真实踩过的每一个都让我浪费了至少十分钟漏写return导致函数返回undefined排查半天发现根本没返回值箭头函数加花括号但忘记写return输出直接变undefinedconst声明的函数表达式试图二次赋值报Assignment to constant variable试图在函数外部访问函数内部用let声明的变量报ReferenceErrorsetTimeout配合var循环输出全是最后一个数闭包陷阱函数参数名与全局变量重名导致函数内部无法访问外部的同名变量如果你也在学Day03这个阶段我建议你也这样建一份自己的踩坑清单。因为报错信息虽然看起来吓人但圈定问题、定位范围之后无非就是那几类变量作用域、返回值、this指向。6.3 给Day04的我接下来要补的东西写这篇笔记时我已经往后多学了一些内容所以我在这里给当时的自己列了几个衔接点函数作用域、块级作用域、变量提升再往深挖一层配合var和let逐一验证数组的map、filter、reduce都离不开回调函数它们值得单独花一天练this的指向规则要从调用方式去理解而不是从函数定义的位置去理解学完闭包后建议做一个小模块用一个闭包保存私有状态模拟封装我当时的体会是Day03的内容量看起来不大天花板却很高。函数能往深挖的东西足够再学一个月。但只要把封装逻辑、传递参数、返回值、作用域、闭包、回调、this这一条主线给捋顺了后面的路会越走越踏实。最后分享一个小经验学函数千万不要只看不写更不要急着用特别花哨的写法。从最朴素的function关键字开始一步步优化成箭头函数整个过程本身就是对函数式思维的一次完整练习。我现在写项目时很多核心逻辑都还是靠这些基础函数撑起来的。把这天啃下来你就算正式迈进了JS的世界。