引言:为什么函数是JS的”灵魂”?
JavaScript作为前端开发的”老大哥”,函数绝对是其灵魂所在!无论是处理数据、操作DOM,还是实现复杂的异步逻辑,函数都像一把”瑞士军刀”,帮你搞定各种场景。但很多新手往往只会用console.log和for循环,面对map、reduce这些”高级货”时一脸懵,更别说Promise、async/await这些”异步神器”了。
本文就带你从零开始,系统梳理JS常用函数,从基础语法到实战技巧,再到性能优化,让你从”函数小白”逆袭成”函数大神”!话不多说,发车!🚀
一、函数基础:从”定义”到”调用”的正确姿势
1.1 函数的4种定义方式:总有一款适合你
在JS中定义函数就像”做蛋糕”,有不同的配方,适合不同的场景:
(1)函数声明:最经典的”传统配方”
functionadd(a, b){
return a + b;
}
console.log(add(1,2));// 3特点:函数声明会”提升”(hoisting),就像蛋糕提前发酵好,即使在定义前调用也不会报错。
(2)函数表达式:灵活的”匿名配方”
constmultiply=function(a, b){
return a * b;
};
console.log(multiply(3,4));// 12特点:赋值给变量的匿名函数,必须先定义后调用,适合作为回调函数传递。
(3)箭头函数:ES6的”极简配方”
constdivide=(a, b)=> a / b;// 一行代码直接return,爽!
console.log(divide(10,2));// 5特点:语法简洁到极致,没有自己的
this,适合简短逻辑和回调场景(比如map、filter)。但注意:箭头函数不能当构造函数,也没有arguments对象哦!
箭头函数没有arguments对象,调用时会报错,而普通函数可以正常获取参数长度(4)立即执行函数(IIFE):”即做即食”的配方
(function(){
console.log("我定义完就执行,不污染全局变量!");
})();
// 输出:我定义完就执行,不污染全局变量!特点:定义后立即执行,创建独立作用域,早期用来解决全局变量污染问题,现在模块化时代用得少了,但了解一下总没错~
1.2 函数的”五脏六腑”:参数与返回值
函数就像一台”机器”,参数是”原材料”,返回值是”产品”:
-
参数:除了普通参数,还支持默认参数(ES6+)和剩余参数(
...args):// 默认参数:缺省值自动补全
functiongreet(name ="陌生人"){
return`Hello, ${name}!`;
}
console.log(greet());// Hello, 陌生人!
// 剩余参数:把多个参数"打包"成数组
functionsum(...numbers){
return numbers.reduce((a, b)=> a + b,0);
}
console.log(sum(1,2,3,4));// 10 -
返回值:用
return输出结果,没有return则默认返回undefined。箭头函数如果只有一行代码,可以省略return和大括号,爽歪歪!
二、数组操作函数:数据处理的”王炸组合”
数组是JS中最常用的数据结构之一,而数组方法就是处理数据的”瑞士军刀”。掌握这些方法,从此告别for循环嵌套的”地狱”!
2.1 遍历三巨头:forEach、map、filter
(1)forEach:”逐个点名”式遍历
forEach就像老师点名,逐个对数组元素执行操作,但没有返回值(返回undefined):
const fruits =["苹果","香蕉","橙子"];
fruits.forEach((fruit, index)=>{
console.log(`第${index+1}个水果:${fruit}`);
});
// 输出:
// 第1个水果:苹果
// 第2个水果:香蕉
// 第3个水果:橙子
forEach遍历数组时,会依次将元素、索引和数组本身传给回调函数(2)map:”变形金刚”式转换
map是”映射”的意思,它会遍历数组,对每个元素执行操作后返回一个新数组,原数组不变。堪称处理数据的”神器”!const numbers =[1,2,3,4];
const doubled = numbers.map(num=> num *2);
console.log(doubled);// [2, 4, 6, 8](原数组numbers不变)实战场景:从接口数据中提取需要的字段
const users =[
{id:1,name:"张三",age:20},
{id:2,name:"李四",age:22}
];
const userNames = users.map(user=> user.name);
console.log(userNames);// ["张三", "李四"]
map函数遍历数组时,每个元素经过函数处理后,组成新数组返回(3)filter:”筛选机”式过滤
filter会根据条件筛选出符合要求的元素,返回一个新数组。比如从一堆数据中挑出”符合条件的崽”:const scores =[80,90,59,60,100];
const passed = scores.filter(score=> score >=60);
console.log(passed);// [80, 90, 60, 100](过滤掉不及格的59)实战场景:搜索功能实现
const products =[
{name:"手机",price:5000},
{name:"电脑",price:8000},
{name:"耳机",price:500}
];
constsearchProducts=(keyword)=>{
return products.filter(p=> p.name.includes(keyword));
};
console.log(searchProducts("电"));// [{name: "手机", ...}, {name: "电脑", ...}]
filter函数会遍历数组,只保留回调函数返回true的元素2.2 条件判断双雄:some vs every
(1)some:”有一个就行”
some判断数组中是否至少有一个元素满足条件,返回布尔值。就像考试:”只要有一门及格,就不算挂科”:const numbers =[1,3,5,7,2];
const hasEven = numbers.some(num=> num %2===0);
console.log(hasEven);// true(因为有2这个偶数)(2)every:”全部都要行”
every判断数组中是否所有元素都满足条件,返回布尔值。就像考试:”所有科目都及格,才算通过”:const scores =[80,90,70,60];
const allPassed = scores.every(score=> score >=60);
console.log(allPassed);// true(所有分数都>=60)
左侧some函数只要有一个元素满足条件就返回true,右侧every函数需要所有元素满足条件才返回true2.3 数据聚合神器:reduce
reduce是数组方法中的”全能选手”,可以实现求和、求积、分组、去重等各种聚合操作。它接收一个回调函数和初始值,遍历数组时累计计算结果:基础用法:求和
const numbers =[1,2,3,4];
const sum = numbers.reduce((acc, curr)=> acc + curr,0);
// acc:累计值(初始值为0),curr:当前元素
console.log(sum);// 10(0+1+2+3+4)高级用法:数组去重
const arr =[1,2,2,3,3,3];
const uniqueArr = arr.reduce((acc, curr)=>{
if(!acc.includes(curr)) acc.push(curr);
return acc;
},[]);// 初始值是空数组
console.log(uniqueArr);// [1, 2, 3]高级用法:对象分组
const students =[
{name:"张三",class:"一班"},
{name:"李四",class:"二班"},
{name:"王五",class:"一班"}
];
const grouped = students.reduce((acc, curr)=>{
// 按班级分组,key是班级名,value是学生数组
if(!acc[curr.class]) acc[curr.class]=[];
acc[curr.class].push(curr);
return acc;
},{});// 初始值是空对象
console.log(grouped);
// { "一班": [{name: "张三", ...}, {name: "王五", ...}], "二班": [{name: "李四", ...}] }
reduce函数通过累计值(acc)和当前元素(curr)的计算,最终得到聚合结果2.4 ES6+数组新特性:flat、flatMap、includes
(1)flat:”拍平”多维数组
flat(depth)可以将多维数组”拍平”成一维数组,depth是深度(默认1):const arr =[1,[2,[3,[4]]]];
console.log(arr.flat());// [1, 2, [3, [4]]](默认拍平1层)
console.log(arr.flat(2));// [1, 2, 3, [4]](拍平2层)
console.log(arr.flat(Infinity));// [1, 2, 3, 4](拍平所有层)(2)flatMap:map+flat的”组合拳”
先
map转换元素,再flat拍平1层(比map+flat更高效):const sentences =["Hello world","I love JS"];
const words = sentences.flatMap(sentence=> sentence.split(" "));
console.log(words);// ["Hello", "world", "I", "love", "JS"]
flatMap先对每个元素执行map操作,再将结果拍平1层(3)includes:判断元素是否存在
includes(value)返回布尔值,判断数组是否包含某个元素(比indexOf更直观):const fruits =["苹果","香蕉","橙子"];
console.log(fruits.includes("香蕉"));// true
console.log(fruits.includes("西瓜"));// false三、字符串处理函数:文本操作的”百宝箱”
字符串是前端开发中处理用户输入、展示文本的核心数据类型,这些函数能帮你轻松搞定字符串的各种操作!
3.1 字符串截取三兄弟:slice、substring、substr
这三个方法都能截取字符串,但细节差异很大,一不小心就踩坑!
(1)slice(start, end):最”规矩”的截取
start
:起始索引(必填),负数表示从末尾开始(如-1是最后一个字符) end
:结束索引(可选),不包含end位置的字符,默认截取到末尾
const str ="Hello, World!";
console.log(str.slice(7));// "World!"(从索引7开始截取)
console.log(str.slice(0,5));// "Hello"(从0到5,不包含5)
console.log(str.slice(-6,-1));// "World"(从倒数第6个到倒数第1个,不包含倒数第1个)(2)substring(start, end):”特殊规则”的截取
-
和 slice类似,但start和end为负数时会被转为0,且会自动交换大小(如start>end时,会交换两者)
const str ="Hello, World!";
console.log(str.substring(7,2));// "llo, "(start=7>end=2,自动交换为substring(2,7))
console.log(str.substring(-3));// "Hello, World!"(负数转为0,相当于substring(0))(3)substr(start, length):按”长度”截取
start
:起始索引(必填),负数表示从末尾开始 length
:截取长度(可选),默认截取到末尾
const str ="Hello, World!";
console.log(str.substr(7,5));// "World"(从索引7开始,截取5个字符)
console.log(str.substr(-6,5));// "World"(从倒数第6个开始,截取5个字符)
slice支持负数索引,substring会将负数转为0,substr第二个参数是长度3.2 字符串查找与替换:indexOf、includes、replace
(1)indexOf:查找子串位置
返回子串首次出现的索引,找不到返回-1:
const str ="Hello, World!";
console.log(str.indexOf("World"));// 7("World"从索引7开始)
console.log(str.indexOf("o"));// 4(第一个"o"的位置)
console.log(str.indexOf("o",5));// 8(从索引5开始找,第二个"o"的位置)(2)includes:判断是否包含子串
返回布尔值,比
indexOf更直观(ES6+):const str ="Hello, World!";
console.log(str.includes("World"));// true
console.log(str.includes("world"));// false(区分大小写)(3)replace:替换子串
替换字符串中的子串,默认只替换第一个匹配项,配合正则表达式可实现全局替换:
const str ="Hello, World! World!";
console.log(str.replace("World","JS"));// "Hello, JS! World!"(只替换第一个)
console.log(str.replace(/World/g,"JS"));// "Hello, JS! JS!"(全局替换,/g是全局标志)高级用法:正则表达式+回调函数
const str ="2023-10-01";
// 将"年-月-日"格式转为"月/日/年"
const newStr = str.replace(/(d{4})-(d{2})-(d{2})/,"$2/$3/$1");
console.log(newStr);// "10/01/2023"($1、$2、$3对应正则中的分组)3.3 字符串转换:toLowerCase、toUpperCase、trim
-
toLowerCase()/toUpperCase():转小写/大写const str ="Hello, World!";
console.log(str.toLowerCase());// "hello, world!"
console.log(str.toUpperCase());// "HELLO, WORLD!" -
trim():去除首尾空格(ES5+),trimStart()/trimEnd()去除开头/结尾空格(ES2019+)const str =" Hello, World! ";
console.log(str.trim());// "Hello, World!"(去除首尾空格)
四、异步编程函数:告别”回调地狱”的优雅方案
JS是单线程语言,处理异步操作(如网络请求、定时器)时,不能阻塞主线程。异步函数就是解决这个问题的”钥匙”!
4.1 回调函数:异步的”老祖宗”
回调函数是最原始的异步方案:把函数A作为参数传给函数B,函数B执行完后调用函数A。比如setTimeout:
console.log("1. 开始");
setTimeout(()=>{
console.log("3. 异步任务完成");// 2秒后执行
},2000);
console.log("2. 继续执行");
// 输出顺序:1. 开始 → 2. 继续执行 → 3. 异步任务完成(2秒后)问题:多层嵌套时会形成”回调地狱”,代码可读性极差:
// 回调地狱示例(不要学!)
getData1((data1)=>{
getData2(data1.id,(data2)=>{
getData3(data2.id,(data3)=>{
console.log(data3);// 嵌套3层,已经开始乱了...
});
});
});4.2 Promise:异步的”救世主”
ES6引入的Promise解决了回调地狱问题,它将异步操作封装成一个”承诺”,支持链式调用:
Promise的三种状态:
- pending
(进行中):初始状态 - fulfilled
(已成功):调用 resolve()后 - rejected
(已失败):调用 reject()后
基础用法:
const promise =newPromise((resolve, reject)=>{
setTimeout(()=>{
const success =true;
if(success){
resolve("操作成功!");// 成功时调用resolve
}else{
reject("操作失败!");// 失败时调用reject
}
},1000);
});
// 链式调用:成功用then,失败用catch
promise
.then(result=>{
console.log("成功:", result);// 1秒后输出:成功:操作成功!
})
.catch(error=>{
console.log("失败:", error);
});链式调用解决回调地狱:
// 用Promise改写回调地狱
getData1()
.then(data1=>getData2(data1.id))// 第一个异步完成后,调用第二个
.then(data2=>getData3(data2.id))// 第二个异步完成后,调用第三个
.then(data3=>console.log(data3))// 第三个异步完成后,处理结果
.catch(error=>console.log("出错了:", error));// 任何一步出错都会进入catch
Promise状态从pending→fulfilled/rejected,then接收成功结果,catch捕获错误4.3 async/await:异步的”语法糖”
ES2017引入的async/await,让异步代码看起来像同步代码,简直是”人类高质量语法糖”!
基础用法:
async
:声明异步函数,返回一个Promise await
:等待Promise完成,只能在async函数中使用
// 定义异步函数
asyncfunctionfetchData(){
try{
console.log("开始请求数据...");
// 等待Promise完成,获取结果
const result =awaitnewPromise((resolve)=>{
setTimeout(()=>resolve("数据请求成功!"),2000);
});
console.log(result);// 2秒后输出:数据请求成功!
return result;// 异步函数返回的是Promise
}catch(error){console.log("出错了:", error);
}
}
// 调用异步函数
fetchData();并行执行多个异步任务:
asyncfunctionfetchAllData(){
// 同时发起多个请求,用Promise.all包裹
const[data1, data2]=awaitPromise.all([
fetch("/api/data1"),
fetch("/api/data2")
]);
console.log("所有数据请求完成:", data1, data2);
}
async/await通过暂停执行(await)等待Promise完成,再恢复执行,代码像同步一样直观五、高级函数:函数式编程的”黑科技”
掌握这些高级函数,你就能写出更简洁、更优雅的函数式代码,告别”面条代码”!
5.1 高阶函数:”函数的函数”
高阶函数是指接收函数作为参数,或返回函数的函数。数组方法
map、filter、reduce都是高阶函数!自定义高阶函数示例:
// 接收函数作为参数
functionwithLogging(fn){
returnfunction(...args){
console.log(`调用函数:${fn.name},参数:${args}`);
const result =fn(...args);
console.log(`函数返回:${result}`);
return result;
};
}
// 使用高阶函数包装add函数
constadd=(a, b)=> a + b;
const addWithLogging =withLogging(add);
addWithLogging(1,2);
// 输出:
// 调用函数:add,参数:1,2
// 函数返回:35.2 柯里化:”参数拆分”的艺术
柯里化(Currying)是将接收多个参数的函数转换为一系列接收单个参数的函数的过程。比如
add(a, b, c)→add(a)(b)(c)。柯里化实现:
functioncurry(fn){
returnfunctioncurried(...args){
// 如果参数个数足够,直接调用原函数
if(args.length>= fn.length){
return fn.apply(this, args);
}
// 否则返回一个新函数,等待接收剩余参数
returnfunction(...nextArgs){
return curried.apply(this, args.concat(nextArgs));
};
};
}
// 测试柯里化
constadd=(a, b, c)=> a + b + c;
const curriedAdd =curry(add);
console.log(curriedAdd(1)(2)(3));// 6(分步传参)
console.log(curriedAdd(1,2)(3));// 6(先传2个,再传1个)应用场景:参数复用(比如固定某个参数,生成新函数)
// 固定税率,生成计算税后价格的函数
constcalculateTax=(taxRate, price)=> price *(1+ taxRate);
const curryTax =curry(calculateTax);
const withVat =curryTax(0.2);// 固定税率20%
console.log(withVat(100));// 120(100*1.2)
console.log(withVat(200));// 240(200*1.2)
柯里化通过分步传参,实现参数复用和函数定制5.3 记忆化:”缓存结果”的优化技巧
记忆化(Memoization)是将函数的计算结果缓存起来,避免重复计算,提升性能。适合计算量大、入参重复的场景(如斐波那契数列、动态规划)。
记忆化实现:
functionmemoize(fn){
const cache =newMap();// 用Map缓存结果
returnfunction(...args){
const key =JSON.stringify(args);// 将参数转为字符串作为key
if(cache.has(key)){
console.log("从缓存获取结果");
return cache.get(key);
}
const result = fn.apply(this, args);
cache.set(key, result);// 缓存结果
return result;
};
}
// 测试记忆化:计算斐波那契数列(递归版,计算量大)
functionfibonacci(n){
if(n <=1)return n;
returnfibonacci(n -1)+fibonacci(n -2);
}
const memoizedFib =memoize(fibonacci);
console.log(memoizedFib(10));// 55(首次计算,耗时较长)
console.log(memoizedFib(10));// 55(从缓存获取,瞬间完成)
记忆化函数通过缓存结果,执行时间远小于普通函数六、实战应用与性能优化:从”会用”到”用好”
6.1 函数性能优化技巧
(1)避免不必要的函数创建
在循环或频繁调用的场景中,避免每次创建新函数(如回调函数):
// 不好:每次循环创建新函数
for(let i =0; i <1000; i++){
arr.push(()=> i);
}
// 好:复用同一个函数
constgetI=(i)=>()=> i;
for(let i =0; i <1000; i++){
arr.push(getI(i));
}(2)使用箭头函数简化回调
箭头函数语法简洁,适合简短的回调逻辑:
// 普通函数写法
arr.map(function(item){
return item *2;
});
// 箭头函数写法(更简洁)
arr.map(item=> item *2);(3)合理使用记忆化和柯里化
对计算密集型函数使用记忆化,对参数复用场景使用柯里化,提升性能和代码复用性。
6.2 常见错误与避坑指南
(1)箭头函数的this绑定问题
箭头函数没有自己的
this,它的this继承自外层作用域。不要用箭头函数作为对象方法或构造函数:const obj ={
name:"张三",
sayHi:()=>{
console.log(this.name);// undefined(this指向全局,而非obj)
}
};
obj.sayHi();(2)数组方法修改原数组vs返回新数组
- 修改原数组
: push、pop、shift、unshift、splice、sort、reverse - 返回新数组
: map、filter、slice、concat、flat、flatMap
避坑:如果不想修改原数组,用返回新数组的方法,或先复制数组([...arr])。
(3)异步函数的错误处理
async/await必须配合try/catch捕获错误,否则错误会被静默忽略:
asyncfunctionfetchData(){
try{
const data =awaitfetch("/api/data");
return data.json();
}catch(error){
console.log("请求失败:", error);// 必须用try/catch捕获错误
}
}🎯 总结:函数之路,永无止境
JavaScript函数是前端开发的核心,从基础的函数声明到高级的柯里化、记忆化,从同步函数到异步的Promise、async/await,每一个知识点都值得深入学习。本文盘点了常用函数的用法、示例和实战技巧,但函数的世界远不止这些——函数式编程、设计模式、性能优化等领域还有更多宝藏等着你挖掘!
记住,最好的学习方式是”动手实践”:拿到一个需求,先思考用哪个函数最优雅,写完后再想想有没有优化空间。相信不久的将来,你也能写出”人见人爱,花见花开”的函数代码!
最后送大家一句名言:”函数是一等公民,对待它们要像对待初恋一样认真。” ❤️
