尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

JavaScript函数全解:从基础定义到闭包高阶玩法

JavaScript函数全解:从基础定义到闭包高阶玩法 如果你正在学JavaScript函数是绝对绕不过去的一道坎。不管你是刚接触前端的初学者还是准备前端面试题2026的求职者都会发现函数几乎出现在每一道题目、每一段代码里。回调函数、箭头函数写法、函数声明、闭包、this指向……这些词你可能都听过但真到自己写代码的时候又容易卡壳。这节内容对应《JavaScript学习手册》系列的第七章就把“函数”这件事彻底聊透。函数是什么一句话它是一段可复用的代码块接收输入、执行处理、返回结果。听起来简单但JavaScript里的函数比这要复杂得多。它不仅能被定义和调用还能被当作参数传来传去、被当作返回值从函数里蹦出来、甚至能记住自己出生时的环境。可以说理解了函数你就理解了JavaScript这门口语的一大半。这篇文章我打算从零开始把函数的定义方式、底层机制、参数处理、作用域与闭包、高阶函数与回调、内置函数、常见报错、面试题实战全部串起来。我会尽量用大白话和实际代码来讲不讲空洞的理论每个点都给出能直接跑起来的例子。看完之后你可以回过头去重新审视自己写过的代码会发现很多原来“能跑但说不清为什么”的地方现在都有答案了。1. 先想明白函数在JavaScript里到底扮演什么角色1.1 函数就是一台“加工机器”你可以把函数想象成一台榨汁机。你往里面丢进苹果它给你苹果汁丢进橙子它给你橙汁。丢进去的东西叫参数出来的东西叫返回值里面的工作过程就是函数体。JavaScript里写函数本质上就是定义一台可复用的加工机器function makeJuice(fruit) { return fruit 汁; } makeJuice(苹果); // 苹果汁就这么简单。但为什么很多初学者觉得函数难因为JavaScript里的这台“机器”不是摆在固定位置的它可以被塞进变量里、被塞进另一个机器的进料口里、还可以被另一台机器当作成品生产出来。这种灵活性是JavaScript区别于Java、C等语言的核心特性之一也是后面要讲的回调、闭包、高阶函数的地基。1.2 函数是“一等公民”在JavaScript中函数和数字、字符串、对象一样属于“一等公民”。这意味着函数可以像普通值一样被赋值给变量、存入数组、作为对象属性、作为参数传入另一个函数甚至作为返回值从函数中返回。我举个例子。你把函数赋值给变量调用的时候就直接用变量名加括号const greet function (name) { return 你好 name; }; greet(小明); // 你好小明这个特性现在看着平平无奇但它引出了一个重要结论函数本质上是对象。既然是对象就可以有属性可以被传递也自然可以出现在任何普通值能出现的位置。前端开发里大量的传参、回调、事件处理都建立在这个“一等公民”的基础上。理解这一点后面讲高阶函数的时候你会觉得特别顺。1.3 函数有三种常见的写法JavaScript里定义一个函数有三大流派函数声明、函数表达式、箭头函数。虽然最终都是“函数”但细节差异很大。函数声明就是function关键字开头function sum(a, b) { return a b; }函数表达式把函数赋值给变量const sum function (a, b) { return a b; };箭头函数是ES6引入的写法最简洁const sum (a, b) a b;这几种写法看着只是语法不同实际在“提升机制”、“this指向”、“适用场景”上都有明显区别。很多前端面试题就问这个。后面我会一个个拆开讲尤其是箭头函数写法它现在是前端开发的绝对主力不彻底搞懂很容易踩坑。2. 函数声明的底层机制提升、重名和匿名2.1 变量提升为什么函数先调用后声明也能运行先看一段代码。你把函数声明写在调用之后代码依然正常运行sayHello(); // hello function sayHello() { console.log(hello); }要是换成其他语言这多半会报错。但JavaScript引擎在执行代码之前会先做一遍“预编译”把函数声明整体提升到当前作用域的顶部。这就是变量提升Hoisting。所以你在代码底部声明函数顶部直接调用也不会出问题。但函数表达式没有这个待遇。因为它是把匿名函数赋值给一个变量遵循的是变量提升规则。变量提升只会提升变量声明本身不会提升赋值操作sayHello(); // TypeError: sayHello is not a function const sayHello function () { console.log(hello); };这里之所以报错是因为代码执行到sayHello()时sayHello确实已经被声明了值为undefined但还没被赋值为函数。对一个undefined加括号调用自然就抛错。注意这也是一个经典面试陷阱——函数声明会整体提升函数表达式只提升变量名。所以能用函数声明就尽量用函数声明能避免不少“我明明定义了函数为什么调用不了”的奇奇怪怪的问题。2.2 匿名函数与具名函数的取舍函数表达式和箭头函数经常写成匿名的也就是函数没有名字。匿名函数短小精悍尤其在回调场景里很常见setTimeout(function () { console.log(一秒后执行); }, 1000);但匿名函数有一个实际痛点调试的时候调用栈里显示的名字是anonymous你根本看不出是哪个函数出了问题。如果给函数表达式起个名字变成具名函数表达式调试体验会好很多const handler function clickHandler() { console.log(点击了); };另外具名函数在递归场景下也更有优势。匿名函数没法在函数体内直接引用自己——虽然可以用arguments.callee但这个特性已经被废弃不建议使用。我在项目里的习惯是简单的单行回调用箭头函数复杂的具名逻辑就用具名函数声明。既保证可读性又方便错误堆栈定位。这个习惯帮我省下过不少排查时间认真讲是值得养成的。3. 参数、默认值与返回值的完整拆解3.1 参数传递是“值传递”但对象有例外JavaScript里的参数传递基础类型数字、字符串、布尔值是典型的按值传递函数内部怎么改都不会影响外面的变量let x 10; function change(num) { num 100; } change(x); console.log(x); // 10但对象类型数组、对象等就不一样了。传进去的是对象的引用可以理解为地址函数内部修改对象的属性外部也会跟着变const user { name: 小明 }; function changeName(obj) { obj.name 小红; } changeName(user); console.log(user.name); // 小红很多人把这种情况理解成“按引用传递”。严格来说JavaScript传的是“引用的一份拷贝”也就是共享同一个对象。所以细节是重新给参数赋值不会影响外部但修改参数对象的属性会影响外部。function test(obj) { obj { name: 新对象 }; // 只改局部引用外部不受影响 }这个点在面试题里反复出现。记清楚函数内部改参数本身不会影响外部改参数指向对象的内部属性会影响外部。3.2 默认参数、arguments 与 rest 参数ES6之前JavaScript函数没有默认参数语法大家只能用arguments对象或者逻辑判断来处理缺省值function greet(name) { name name || 朋友; console.log(你好 name); }arguments是函数内部自带的类数组对象包含了所有传入的参数。但它不是真正的数组没有map、filter这些方法用起来很别扭。ES6之后就好办多了直接在参数位置写默认值function greet(name 朋友) { console.log(你好 name); } greet(); // 你好朋友 greet(小红); // 你好小红如果参数个数不确定可以用rest参数也叫剩余参数它把多余参数收集成一个真正的数组function sum(...numbers) { return numbers.reduce((total, n) total n, 0); } sum(1, 2, 3, 4); // 10rest参数是个好东西但要注意它必须放在参数列表的最后一位。function sum(...numbers, extra) {}这种写法会直接报语法错误。实操心得新项目里不要依赖arguments了统一用默认参数加rest参数。代码可读性强很多而且rest参数是真正的数组处理起来灵活得多。3.3 return 的隐藏规则return的作用是返回函数的结果同时结束函数的执行。有两点很容易被忽视。第一如果不写return函数默认返回undefined。这在回调场景里很容易踩坑比如map方法里忘记写return就会得到一堆undefinedconst arr [1, 2, 3]; const doubled arr.map(function (n) { n * 2; // 忘了return }); console.log(doubled); // [undefined, undefined, undefined]这种bug不会让程序崩溃但结果完全是错的。我自己就写过不少这样的低端错误遇到结果全为undefined的情况第一反应就是检查回调函数里有没有return。第二return后面如果直接换行会触发JavaScript的自动分号插入机制ASI函数会在换行处提前结束返回undefinedfunction test() { return 1; } console.log(test()); // undefined虽然现在很多lint工具会提示这个问题但养成“return值写在同一行”的习惯还是很有必要的。4. 作用域与闭包JavaScript函数的灵魂4.1 词法作用域与作用域链“作用域”决定了一组变量在哪些地方能被访问到。JavaScript是词法作用域意思是函数的作用域在定义的时候就确定了而不是在调用的时候。看这个例子const name 外部; function outer() { const name 内部; function inner() { console.log(name); } inner(); } outer(); // 内部inner定义在outer里面所以它访问name时会沿着定义时的链条一路找先找自己内部的变量找不到就往外层找直到全局。这个链条叫作用域链。重点来了JavaScript的函数能访问定义它们那个作用域里的变量哪怕这个函数被拿到别的地方去执行也依然记得原来的环境。这个“记住环境”的能力就是闭包的本质。4.2 闭包的本质函数记住了定义时的环境闭包没有想象中那么神秘。当一个函数引用了外部作用域的变量并且这个函数在定义它的作用域之外被使用就形成了闭包。更直白地说函数把定义时的环境和变量一起打包带走了。一个最经典的例子是计数器function createCounter() { let count 0; return function () { count; return count; }; } const counter createCounter(); console.log(counter()); // 1 console.log(counter()); // 2createCounter返回的匿名函数引用了count。照理说createCounter执行完局部变量count应该被销毁但因为返回的函数还引用着它JavaScript引擎就会把这部分变量保留下来。于是counter每次调用都能操作同一个count。这带来了什么能力最直接的是私有变量。count对外完全不可见只能通过返回的函数来操作它。在很多框架内部实现里正是用闭包来创建私有状态避免污染全局命名空间。闭包的实际案例也很多。比如一个页面里有多个按钮你想要每个按钮点击时输出自己的序号。用闭包可以这么写for (var i 0; i 3; i) { buttons[i].onclick function () { console.log(i); }; }如果是var声明你会发现不管点哪个按钮输出的都是3。为什么因为var声明的i是全局的循环结束后i已经变成3所有回调函数访问的都是同一个i。解决办法有两个。一个是把var换成letlet的块级作用域让每次循环都产生独立的绑定for (let i 0; i 3; i) { buttons[i].onclick function () { console.log(i); }; }另一个是用立即执行函数把i作为参数传进闭包for (var i 0; i 3; i) { buttons[i].onclick (function (n) { return function () { console.log(n); }; })(i); }这个“循环闭包”问题是面试题里出现频率极高的题目值得反复练几遍。理解透了闭包基本就算掌握大半了。4.3 闭包与内存管理闭包也有副作用因为函数一直持有外部变量的引用这些变量不会被垃圾回收可能造成内存泄漏。实际开发里如果创建了大量闭包而且不再需要它们时还存着引用内存占用就会慢慢涨上去。解决办法也很简单用完的闭包把引用置为null让它可以被垃圾回收。这个习惯在写长生命周期应用比如管理后台、数据大屏时尤其重要运行几小时不卡顿很多细节都是从这些地方抠出来的。5. 高阶函数与回调把函数当参数用起来5.1 回调函数把决定权交给别人什么是回调函数简单说把一个函数作为参数传给另一个函数在合适的时候被调用。这个“合适的时候”可能同步也可能异步。比如setTimeout的经典写法setTimeout(function () { console.log(一秒后执行); }, 1000);这里function () {...}就是回调函数。setTimeout拿到这个函数后自己设定倒计时时间到了就帮你调用它。你不需要关心倒计时具体怎么实现只需要告诉它“倒计时结束后做什么”。回调函数不一定是异步的。数组的forEach就是同步回调[1, 2, 3].forEach(function (n) { console.log(n); });它会在forEach内部立即执行回调并不会等什么。所以“回调”这个词的重点不是异步而是“把函数传给别人去调用”。在前面讲闭包时用过的事件绑定本质也是一样的——元素自己不知道什么时候会被点击但一旦被点击就调用你提供的回调函数。5.2 常用高阶函数map、filter、reduce接受函数作为参数的函数就叫高阶函数。JavaScript里数组的map、filter、reduce还有浏览器里的addEventListener、setTimeout全是高阶函数。map是“对每个元素做加工”const prices [100, 200, 300]; const discount prices.map(p p * 0.8); // [80, 160, 240]filter是“选出满足条件的元素”const nums [1, 2, 3, 4, 5]; const even nums.filter(n n % 2 0); // [2, 4]reduce是把整个数组合并成一个值用途非常强大比如算总分const scores [80, 90, 100]; const total scores.reduce((sum, s) sum s, 0); // 270这三个方法配合箭头函数写法已经成为前端开发里最常用的模式之一。它们比for循环写法更声明式你告诉代码“要做什么”而不是“怎么做”代码的意图一目了然。这也是为什么很多前端团队在代码规范里鼓励用这些数组方法而不是手动写循环。5.3 this 指向最绕的一环函数内部的this指向是新手最容易糊涂的地方。记住一个最核心的判断逻辑看函数是怎么被调用的。普通调用fn()非严格模式下this指向全局对象浏览器里是window严格模式下是undefinedfunction fn() { console.log(this); } fn(); // window 或 undefined严格模式作为对象的方法调用const obj { name: obj, say() { console.log(this.name); } }; obj.say(); // obj构造函数调用new fn()this指向新建的对象。箭头函数有点特殊它本身不绑定this用的是定义时外层的thisconst obj { name: obj, say: function () { setTimeout(() { console.log(this.name); }, 100); } }; obj.say(); // obj这里如果用普通函数写setTimeout的回调this会指向window拿不到name。箭头函数则直接继承say方法里的this所以能正常输出。箭头函数这个特性在事件回调、定时器、Promise等场景里太好用了所以现在新代码里箭头函数几乎成了标配。需要改this指向时还有call、apply、bind三兄弟function greet() { console.log(this.name); } const user { name: 小明 }; greet.call(user); // 小明 greet.apply(user); // 小明 const boundGreet greet.bind(user); boundGreet(); // 小明为了避免困惑我给一个简单的表调用方式普通函数的 this箭头函数的 this直接调用全局/undefined外层作用域的 this对象方法调用调用者对象外层作用域的 this构造函数调用新实例不能被当作构造函数call/apply/bind指定的对象无法改变固定继承外层 this箭头函数无法作为构造函数这一点也经常被拿来出题。如果你确实需要一个会改变this的函数那就老老实实用普通函数或者bind。6. 内置函数与常见报错速查动手调试的经验之谈6.1 高频内置函数要熟练除了自己定义函数JavaScript还提供了一批内置函数日常几乎每天都在用。parseInt把字符串转整数。注意老规矩能传进制就传进制尤其是处理“08”、“09”这种字符串时不传可能出问题parseInt(10, 10); // 10isNaN判断是不是非数字。但它有隐式转换的坑isNaN(abc); // true因为 abc 被转成 NaN isNaN(123); // false因为 123 被转成了数字123更严谨的判断方式是用Number.isNaN或者直接看typeof。前端面试题里经常在这里设陷阱。还有一组很常用的函数String()、Number()、Boolean()可以把值转成对应类型。虽然现在更多推荐用String(value)、Number(value)这样显式转换但这些内置函数依然是基本功。6.2 常见报错与排查方法函数相关的报错出现频率最高的应该就是xxx is not a function。这个报错意思很直白你调用了一个不存在或当前不是函数的值。最常见的几个原因第一种变量名拼错。比如把console.log里的log拼成了lg。这种问题小得很但排查几分钟也是常有的事。第二种异步操作还没完成就调用了函数。比如const data fetchData(); data.map(...); // TypeError: data.map is not a functionfetchData很可能返回的是undefined因为数据还没到位。解决办法是把调用放到异步回调或Promise的.then里。第三种函数被赋值成非函数。比如function handleClick() { console.log(click); } handleClick hello;如果你在某处把函数重新赋值成字符串后面再调用就会报错。这种情况在复杂项目里偶尔遇到排查时可以用typeof fn function先判断一下。再比如Cannot read property x of undefined这类报错通常是因为某个对象还没被赋值或初始化。排查思路是先确认对象有没有拿到数据再考虑要不要加容错处理// 更稳妥的写法 const name user user.name;社区里常说的“不要相信任何从网络来的数据”就是这个意思。写代码时给外部数据加上边界判断能省掉大量线上问题。7. 前端面试题实战函数相关的经典套路答题思路7.1 真题拆解与答题框架前端面试题2026里函数相关的高频题基本集中在下面几类。函数声明和函数表达式的区别。答题要点有三个函数声明有提升机制函数表达式的变量只有变量提升且值为undefined函数声明有具名表达式可以是匿名的箭头函数是表达式的一种它没有自己的this且不能作为构造函数。箭头函数和普通函数的区别。答题要点语法更简洁没有自己的this沿用外层this不能作为构造函数内部没有arguments对象有rest参数替代。面试官如果追问“为什么箭头函数不能当构造函数”原理是箭头函数没有prototype属性也没有自己的this可供绑定。闭包的场景。不要只背定义要结合实际。常见应用场景有模拟私有变量、回调函数中保存状态、实现防抖节流、模块化编程。我一般会说计数器例子和循环绑定例子然后补一句“闭包会造成引用持续保留需要注意内存管理”这个加分项能体现你真写过代码。防抖和节流是函数应用层面的经典面试题。防抖是事件触发后等待一段时间再执行期间再次触发则重新计时function debounce(fn, delay 300) { let timer null; return function (...args) { clearTimeout(timer); timer setTimeout(() { fn.apply(this, args); }, delay); }; }节流是固定时间间隔内只执行一次function throttle(fn, interval 300) { let last 0; return function (...args) { const now Date.now(); if (now - last interval) { last now; fn.apply(this, args); } }; }这两位是闭包、this指向、rest参数、箭头函数的一个综合应用写完基本能把本章的大部分知识串起来。面试官让你手写时不光要看结果更看你能不能解释清楚每一步在干什么。7.2 工程实践中的函数规范代码规范非常重要。我在实际项目里会坚持几条简单原则函数要做单一职责。一个函数只做一件事。如果一个函数又发请求、又改DOM、又算数值调试的时候会非常痛苦。宁可拆成三五个小函数也不要把逻辑都堆在一起。命名要能读出来。函数名用动词开头比如getUserInfo、renderList、formatDate。看到名字就知道它干什么这是对团队伙伴和未来自己的一种尊重。尽量少写副作用。对同一入参函数应该尽量返回相同的结果不偷偷修改外部变量。这种“纯函数”更好测试、更好维护。在React这类框架里纯函数组件更是核心思路。合理使用默认参数和rest参数。函数参数多了容易遗漏或传错合理的做法是参数少用单个变量参数多用对象结构。比如function createUser({ name, age, email }) { // ... } createUser({ name: 小明, age: 18, email: xexample.com });调用时一目了然新增字段也不用改函数签名。这种写法在很多团队里已经是默认规范了。7.3 给新手的函数学习路线建议我理解很多刚开始学前端的朋友会有一个困惑函数声明、函数表达式、箭头函数、回调、闭包、高阶函数这些概念一股脑全涌过来到底先学什么我的建议是按这个顺序来先掌握函数声明和普通调用理解参数和返回值然后用函数表达式配合数组的map、filter练手熟悉“函数作为值传来传去”的思路接着看箭头函数把写法的差异掌握好最后啃闭包和this指向这两块靠大量做小练习来强化。闭包这块我特别建议你上手手写几个小例子做计数器、模拟私有变量、解决循环绑定问题、手写防抖节流。现在很多面试题库都能搜到这些代码但一定不要只是看答案要自己在控制台里跑一遍错了就打断点看变量一步步怎么走的。这样踩过一遍坑之后闭包的概念才会真正长在你脑子里。关于JavaScript函数的这门课该聊的基本都聊到了。从函数怎么定义到参数怎么传再到作用域、闭包、回调、高阶函数以及各种报错怎么排查、面试题怎么答。函数是整个JavaScript语言的脊梁你可能短时间内没法一次全部消化没关系把它当作一份可以反复查阅的手册。写代码的时候遇到一个困惑就回来看一节几次之后你会发现自己对前端开发的整体掌控力又上了一个台阶。
返回列表