写完几个月的业务代码,我发现一个规律:编程水平的分水岭,往往不在语法本身,而在怎么对待函数。基础阶段我们学的是def、function、函数声明、传参、返回值;进阶阶段要跨越的坎,是意识到函数不只是“一段被调用的代码”,它还可以像数据一样被传递、被存储、被动态组合,甚至被临时加工。这篇函数进阶笔记,我想把这条线上最值钱的东西串起来:从函数声明到底层作用域,从回调函数到闭包,再到柯里化、偏函数、装饰器和常用内置高阶函数。想讲的标题就一句话:让代码更灵活的核心技巧,本质上不是多背几个API,而是切换对函数的理解方式。
适合谁看?已经会写基本函数、但总觉得代码很难复用的初学者;也适合写了两三年业务代码、每天在“复制粘贴—改参数—加if”循环里挣扎的开发者。读的时候别着急,每段代码都亲手跑一遍,体会会深很多。
1. 先转变认知:函数不只是一段代码,而是可以流动的数据
1.1 从“调用”到“传递”:函数名不带括号才是真进阶
很多人写函数写了好几个月,潜意识里仍然认为函数是一个“动作”,add(1,2) 就是在执行动作。这种理解没有错,但太局限了。进阶的第一步,是把函数看成一种“对象”——它跟数字、字符串、数组一样,可以被赋值给变量、放进数据结构、作为参数传出去。
javascript复制function add(x, y) {
return x + y;
}
// 基础阶段:直接调用
console.log(add(1, 2)); // 3
// 进阶阶段:把它当数据用
const f = add;
console.log(f(1, 2)); // 3
const ops = [add, (x, y) => x - y];
console.log(ops[1](10, 3)); // 7
注意看,函数名后面加不加括号,差别巨大。加括号是“立刻执行”,不加括号是“拿到这个函数本身”。这是一道分水岭:一旦你习惯把函数名本身当作一个值传来传去,后面讲的回调、闭包、装饰器就全都通了。
数据结构和函数混在一起用,听起来有点奇怪,但它真实存在于很多框架里。比如一个计算器的算子表、一套策略模式的配置项,本质上就是一个函数数组或函数对象。你不需要写一长串if...else去判断调哪个函数,你只需要查表取函数、再执行,拿到结果。这就是所谓“策略模式”的雏形,也是函数进阶带来的第一层灵活。
1.2 函数作为返回值:行为也可以“批量生产”
比“存函数”更进阶的是“造函数”:一个函数运行之后,返回一个全新的函数。这个结构非常常见,很多人却意识不到它有多重要。
javascript复制function makeGreeter(greeting) {
return function(name) {
return `${greeting}, ${name}!`;
};
}
const sayHi = makeGreeter("你好");
const sayBye = makeGreeter("再见");
console.log(sayHi("小明")); // 你好, 小明!
console.log(sayBye("小红")); // 再见, 小红!
这里makeGreeter内部的greeting在外部函数执行完之后,竟然还能被返回的函数继续使用。这就是后文要重点讲的“闭包”。但这一节我们先只看模式本身:一个函数通过配置参数,返回一个定制化的新函数。
这个模式的实战价值在哪儿?举个例子,排序时经常要按对象的不同字段排序:
javascript复制function makeComparator(key) {
return (a, b) => a[key] - b[key];
}
const users = [
{ name: "A", age: 20 },
{ name: "B", age: 18 },
{ name: "C", age: 25 }
];
users.sort(makeComparator("age"));
console.log(users);
// 输出:[{ name: "B", age: 18 }, { name: "A", age: 20 }, { name: "C", age: 25 }]
如果没有这层思维,你可能要分别写sortByAge、sortByName、sortByScore三个几乎一模一样的函数。有了“函数返回函数”的思路,一个makeComparator就全搞定了。代码不是靠行数堆出来的,是靠“抽象层级”省出来的。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 函数声明与表达式的区别:理解函数什么时候“存在”
2.1 提升(Hoisting)带来的“先调用后声明”错觉
JavaScript里有一个让很多人迷惑的现象:函数声明可以在定义之前调用。
javascript复制sayHello("小李"); // 正常运行:你好, 小李
function sayHello(name) {
console.log("你好,", name);
}
原因是函数声明在代码编译阶段就被绑定到了当前作用域,整个作用域里都能看到它。但你如果把函数改成“函数表达式”写法,结果立刻不一样:
javascript复制sayBye("小李"); // 报错:sayBye is not defined
const sayBye = function(name) {
console.log("再见,", name);
};
const声明的变量存在暂时性死区,赋值之前访问它就是报错。就算把const换成var,也只是从“报错”变成“undefined is not a function”,依然不能用。
很多人遇到这类报错第一反应是“函数名拼错了”,其实根源是“函数在内存里还不存在”。理解这一点,对排查问题非常有帮助:函数声明的作用域和存活时间,不是你写了就能立刻用,要看它是以什么方式声明的,以及声明语句执行了没有。
2.2 Shell报错带来的启发:函数名查找和命令查找是同一类问题
你大概率在终端里见过这类报错:
claude : 无法将“claude”项识别为 cmdlet、函数、脚本文件或可运行程序的名称。
git : 无法将“git”项识别为 cmdlet、函数、脚本文件或可运行程序的名称。
这类报错的本质,是Shell按照环境变量PATH里记录的目录列表,挨个找有没有叫这个名字的命令,找了一圈没找到,就报“无法识别”。解决方案通常就三类:确认是否真的安装了、把安装目录加入PATH、检查拼写有没有出错。
函数在程序内部的解析逻辑,跟这个非常像。JavaScript引擎遇到一个函数名,会沿着“当前作用域 → 外层作用域 → 全局作用域”这条链一路找下去,找不到才报not defined。不同的只是查找路径换成了“作用域链”而不是PATH。所以排查“函数未定义”时,正确的思路是:
- 函数名拼写是
