写完几个月的业务代码,我发现一个规律:编程水平的分水岭,往往不在语法本身,而在怎么对待函数。基础阶段我们学的是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。所以排查“函数未定义”时,正确的思路是:
- 函数名拼写是否正确;
- 函数是否真的被声明/导入到了当前文件;
- 函数声明是否在调用语句之后,且该声明方式是否支持提前调用;
- 有没有被另一个同名函数或变量覆盖。
在Python里则是另一套规则:def本质是运行时执行的赋值语句,模块导入顺序会影响函数是否已经存在。所以Python项目里偶尔会出现“循环导入导致函数取不到”的怪问题:A模块import B,B模块又import A,某个函数在导入时还没定义,结果在另一个文件里调用就报AttributeError。遇到这种问题,不要盯着函数本身看,要检查模块的加载顺序和函数声明位置。函数名查找问题,永远是作用域和生命周期问题,不是“灵异事件”。
3. 回调函数:把“做什么”交给调用者,灵活性就来了
3.1 回调的本质:你定义行为,框架决定时机
回调函数(callback)是“函数作为参数”最典型、最广泛的应用。很多初学者学回调时,容易死记“回调就是传一个函数进去”,但没想明白为什么需要这个设计。其实一句话就能说透:库或框架关心的是“什么时候做”,至于“做什么”,交给调用者决定。
最简单也最常用的例子是排序:
javascript复制const scores = [95, 60, 80, 45];
scores.sort((a, b) => b - a);
console.log(scores); // [95, 80, 60, 45]
sort内部负责了排序算法、元素交换、顺序调整这些脏活累活,但“谁大谁小”这个策略是调用者给出来的。你不传这个函数,默认排序可能是按字符串字典序排的,结果[10, 2, 23]就会排成[10, 2, 23]而不是你预期的[2, 10, 23]。一个函数参数,决定了完全不同的排序结果,这就是灵活性。
再比如事件监听和Node.js读取文件:
javascript复制button.addEventListener('click', () => {
console.log("用户点击了按钮");
});
javascript复制const fs = require('fs');
fs.readFile('config.json', 'utf8', (err, data) => {
if (err) {
console.error("读取失败:", err.message);
return;
}
console.log("配置内容:", data);
});
事件系统不知道用户点击后要干什么,文件系统也不知道拿到数据后你想怎么处理。它们只负责在合适的时机把控制权交回来,具体怎么做,全看回调里怎么写。这种“控制反转”的思想,是框架设计的灵魂,而回调就是它最简单的体现。
3.2 不止JavaScript:C语言的函数指针也是同一个道理
别以为“函数作为参数”只是高级语言的专利。C语言里没有闭包、没有lambda,但它有函数指针,排序照样可以传比较函数:
c复制#include <stdio.h>
#include <stdlib.h>
int desc(const void *a, const void *b) {
return *(int *)b - *(int *)a;
}
int main() {
int arr[] = {3, 1, 4, 1, 5, 9, 2, 6};
int n = sizeof(arr) / sizeof(arr[0]);
qsort(arr, n, sizeof(int), desc);
for (int i = 0; i < n; i++) {
printf("%d ", arr[i]);
}
return 0;
}
qsort是C标准库里的排序函数,它要求你传一个比较函数指针。这跟JavaScript的sort传规则、Python的sorted(key=...)传提取函数,骨子里是同一个设计:把可变的部分留给调用者,把稳定的算法逻辑留在库里。理解了这个,你会发现所谓“框架思维”其实跨语言、跨平台都是通的。
3.3 回调地狱到Promise:函数作为流程的接力棒
回调也有它的阴暗面:一旦多个异步操作存在依赖,回调层层嵌套,代码很快就变成“箭头堆成的金字塔”,也就是著名的回调地狱。
javascript复制getUser(1, (user) => {
getOrders(user.id, (orders) => {
getOrderDetail(orders[0].id, (detail) => {
console.log(detail);
});
});
});
这个问题的根源,是异步流程被活生生写成了同步嵌套。后来出现的Promise、async/await,本质上还是在用“函数”,只不过把“函数作为参数”的模式又包了一层:.then(callback)里传的依然是回调函数。语法变好看了,但核心还是把后续步骤作为一个函数传下去。所以我不建议大家跳过回调直接学async/await,回调的思想不搞清楚,Promise用起来也只是会抄代码,不理解为什么要这么设计。
4. 闭包:让函数记住外部状态的秘密
4.1 闭包原理:函数带着“记忆”回到你的代码里
闭包(closure)是很多人的噩梦,但它一旦讲透,非常简单。先看一个经典例子:
javascript复制function createCounter() {
let count = 0;
return function() {
count += 1;
return count;
};
}
const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3
按常规理解,createCounter执行完之后,函数内部的局部变量count应该被销毁了。但它没有。原因是返回的那个匿名函数还持有一个指向count的“引用”,JavaScript引擎为了让这个函数以后能正常工作,只能把count所在的环境继续保留在内存里。这就是闭包:函数 + 它在创建时所能访问到的外部变量环境 = 闭包。
可以把它类比成“一个出差回来的员工,虽然离开原部门了,但口袋里还揣着部门的门禁卡和通讯录”。他走到哪儿都有权限访问原来部门的信息。这就是闭包最直白的画面。
4.2 闭包的实战价值:私有变量、计数器与防抖节流
闭包最常见的一个作用是模拟私有变量。模块里不希望外部直接改内部状态,就用返回函数的方式来“暴露接口但隐藏数据”。前面说的高阶函数、柯里化,底层其实都依赖闭包保存状态。
再一个是前端几乎必用的防抖(debounce)。防抖的核心需求是:用户连续触发事件时,不急着执行,等停顿一段时间后再执行。问题是,每次触发事件都要知道上一次的计时器状态,这个状态放全局会污染,放对象里不够通用,闭包正好接这个活:
javascript复制function debounce(fn, delay = 300) {
let timer = null;
return function(...args) {
clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}
// 使用
window.addEventListener('resize', debounce(() => {
console.log("窗口大小变化,执行计算");
}, 500));
这里timer就是闭包保存的“记忆”。每一次新事件触发,都先清掉上一次的定时器,再开一个新定时器。如果用户一直触发,timer一直被重置,函数永远不会立即执行;直到用户停下来超过500ms,函数才真正跑一次。这个效果,没有闭包很难干净地实现。
4.3 闭包的代价:内存问题不是吓唬人的
闭包不是免费的。因为它延长了外部变量的生命周期,如果大量创建闭包且长时间持有,内存占用会明显上升。举个例子,在循环里反复创建闭包函数,并把这些函数存到数组里长期使用,那外部变量数组就会被整个闭包链拖着,无法被垃圾回收。
javascript复制const handlers = [];
for (let i = 0; i < 10000; i++) {
const data = new Array(1000).fill(i);
handlers.push(() => {
console.log(data.length);
});
}
// 这 10000 个 data 数组会被闭包一直引用着,内存一直占着
所以在工程里用闭包,要克制。如果一个函数创建之后只运行一两次,闭包的开销可以忽略;但如果要创建大量函数并且长时间保留,就要评估是否值得,或者是否可以让它们共享同一个外部变量而不是各自新建。
5. 柯里化与偏函数:把参数的准备过程拆开
5.1 柯里化:一次只收一个参数
柯里化(currying)的概念听起来学术,落地却很实用。它把一个接受多个参数的函数,改造成一次只接受一个参数、返回下一个函数的链式结构。
javascript复制// 普通函数
function multiply(a, b, c) {
return a * b * c;
}
// 柯里化形式
function curriedMultiply(a) {
return function(b) {
return function(c) {
return a * b * c;
};
};
}
// 用箭头函数写更短
const curriedMultiply = a => b => c => a * b * c;
console.log(curriedMultiply(2)(3)(4)); // 24
好处是什么?最直观的是“参数分批给”。比如你需要一个计算商品价格的函数,原价、折扣率、税率三个参数。如果每次都把三个值凑齐才能调用,那是因为业务场景确实复杂;但如果“折扣率”在一个页面里是固定的,就可以提前固定下来:
javascript复制const discountedPrice = curriedMultiply(1000); // 原价 1000
const finalPrice = discountedPrice(0.8)(1.13); // 打8折,再乘税点
更常见的是日志函数。不同的模块要给日志打不同的级别前缀,每次都写log('INFO', '...')有点烦,提前固定第一个参数会舒服很多。
5.2 偏函数:固定一部分参数
偏函数(partial application)和柯里化解决的是同一类问题——参数太多,想少传一点。区别在于柯里化通常把多参数函数变成多个单参数函数串联,偏函数则是“固定一部分参数,生成一个新函数”。
JavaScript里可以用bind实现:
javascript复制function log(level, message) {
console.log(`[${level}] ${message}`);
}
const info = log.bind(null, "INFO");
const error = log.bind(null, "ERROR");
info("服务启动成功");
error("连接数据库失败");
Python里则是functools.partial:
python复制from functools import partial
def power(base, exp):
return base ** exp
square = partial(power, exp=2)
cube = partial(power, exp=3)
print(square(5)) # 25
print(cube(5)) # 125
这两种写法的本质,都是通过“闭包临时保存一部分参数”,生成一个参数更少的新函数。好处显而易见:配置只需要做一次,后面处处复用;代码语义也更清晰,info和error一看就知道干什么的,不用每次重复传级别字符串。
5.3 函数组合:把灵活函数拼成业务
当函数变成了可以传来传去的零件,下一个自然动作就是“组合”。组合的概念很简单:把多个单步处理的函数串起来,前一个函数的输出作为后一个函数的输入。
javascript复制const compose = (...fns) => x => fns.reduceRight((acc, fn) => fn(acc), x);
const addOne = x => x + 1;
const double = x => x * 2;
const addOneThenDouble = compose(double, addOne);
console.log(addOneThenDouble(3)); // 先 +1 得 4,再 *2 得 8
compose本身就是一个高阶函数:它接收一组函数,返回一个全新的函数。这种模式在Redux中间件、Express中间件、Unix管道里都有体现。你在中间件里看到的next()链式调用,本质上也跟函数组合脱不开关系。把函数当零件,用组合的方式搭流水线,比在单个函数里堆一大堆if...else要清晰得多。
6. 装饰器:不改原函数,也能增强函数
6.1 包装函数:最简单也最通用的增强方式
要给你的函数加日志、加耗时统计、加权限校验,最粗暴的做法是直接改函数体。但改函数体有几个坏处:一是污染原始逻辑,二是如果十个函数都要加,要改十处。进阶的做法是写一个“包装函数”。
javascript复制function withLogging(fn) {
return function(...args) {
console.log("调用参数:", args);
const result = fn(...args);
console.log("返回结果:", result);
return result;
};
}
function divide(a, b) {
if (b === 0) return "除数不能为0";
return a / b;
}
const loggedDivide = withLogging(divide);
loggedDivide(10, 2);
// 输出:
// 调用参数: [ 10, 2 ]
// 返回结果: 5
关键在于,原来的divide函数一行都没改,但它的行为被增强了。这就是装饰器模式最朴素的形态。很多框架里的“拦截器”“中间件”“切面”,底层思路都是这样。你不入侵原有的功能,只是在它外面套一层,在它执行前后做一些额外的事。
6.2 Python装饰器语法糖:好看又强大的背后
Python把这种包装写成了语法糖,用@符号一行搞定,几乎每个Python工程师都见过:
python复制import time
import functools
def timer(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
cost = time.perf_counter() - start
print(f"{func.__name__} 耗时 {cost:.4f}s")
return result
return wrapper
@timer
def compute():
return sum(range(1_000_000))
compute()
@timer这行等价于:
python复制compute = timer(compute)
也就是说,装饰器本质上还是“接受函数、返回函数”的高阶函数。@只是一个让代码更好看的语法糖,底层逻辑跟前面的withLogging一模一样。
有一个细节必须提:上面代码里的@functools.wraps(func)不是可有可无。wrapper.__name__默认会变成wrapper,不再是原函数名,这会导致日志、调试、文档生成时看到一堆wrapper,排查很痛苦。functools.wraps的作用就是把原函数的__name__、__doc__等元信息复制到包装函数上。经验之谈:自己写装饰器时,这个装饰器一定要加,不然坑的是后面接手的同事(大概率是你自己)。
6.3 带参数的装饰器:再套一层,控制力更强
装饰器本身还能接受参数,比如“权限校验”需要指定角色:
python复制import functools
def require_role(role):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
user_role = kwargs.get("user_role")
if user_role != role:
raise PermissionError(f"需要 {role} 角色")
return func(*args, **kwargs)
return wrapper
return decorator
@require_role("admin")
def delete_user(username):
print(f"删除用户: {username}")
# delete_user(user_role="editor", username="test") # 抛异常
delete_user(user_role="admin", username="test") # 正常执行
注意看这里有三层函数:require_role负责接收装饰器参数,decorator负责接收原函数,wrapper负责在调用时做增强。很多人学到这里会晕,但拆开看其实就是“函数返回函数再返回函数”,跟第1章、第5章的内容完全打通了。只要前面“函数是一等公民”的认知牢固,这一层只是叠加复杂度,不是新概念。
7. 内置高阶函数:日常开发直接能用的“灵活函数”
7.1 map、filter、reduce:把循环压进行
写业务时最常干的事是什么?遍历一个数组,筛选出符合条件的元素,然后把它们变换成新结构,最后汇总成一个结果。过去你写三个循环,现在用内置高阶函数几行搞定:
python复制nums = [1, 2, 3, 4, 5, 6]
even = list(filter(lambda x: x % 2 == 0, nums))
squares = list(map(lambda x: x ** 2, even))
print(squares) # [4, 16, 36]
JavaScript对应写法:
javascript复制const nums = [1, 2, 3, 4, 5, 6];
const squares = nums
.filter(n => n % 2 === 0)
.map(n => n ** 2);
console.log(squares); // [4, 16, 36]
map、filter之所以叫高阶函数,是因为它们接收一个函数作为参数。你告诉它们“怎么转换”“怎么筛选”,它们负责遍历和组装结果。归纳汇总的reduce也是同一个套路:
javascript复制const nums = [1, 2, 3, 4, 5, 6];
const total = nums.reduce((acc, n) => acc + n, 0);
console.log(total); // 21
Python里对应的是functools.reduce。这类函数的意义不是“炫技”,而是让意图更明显:看到filter就知道是筛选,看到map就知道是映射转换,不用再一步步跟着循环变量走一遍。
7.2 np.sum的axis参数:库函数如何把“策略”暴露给你
热搜词里有个高频搜索:np.sum()函数 axis。很多人在NumPy里被axis=0和axis=1搞晕。其实这个参数特别能体现“函数如何通过参数控制内部行为”。
python复制import numpy as np
matrix = np.array([[1, 2, 3],
[4, 5, 6]])
print(matrix.sum(axis=0)) # [5 7 9] 按列求和
print(matrix.sum(axis=1)) # [ 6 15] 按行求和
不要死记“0是行还是列”。我们把二维数组想象成坐标轴:axis=0是沿着第0个轴(从上往下)压扁,也就是每一列内的元素相加;axis=1是沿着第1个轴(从左往右)压扁,每一行的元素相加。axis这个参数,本质上是把一个“内部策略”暴露给了调用者:同一个sum函数,通过切换参数值,就能改变计算行为,而不用分别设计sumAlongRows和sumAlongCols两个接口。
这就是库函数设计的教科书级示范。设计API时,如果某个行为差异可以用一个参数表达清楚,就不要拆成两个函数。
同样的思想还在sorted(key=...)、min/max(key=...)里出现:key参数接收一个提取函数,告诉内置算法“按什么标准比较”。你传入不同的函数,排序结果完全不同,而排序算法本身一行没改。
7.3 库函数本身就是“别人设计的好函数”
看到这你会发现,内置函数和第三方库函数(比如Arduino里控制蜂鸣器的tone函数、ESP32上采集麦克风数据的库接口、图像处理库里的回调接口),本质上都是“某个程序员设计出来的函数”。你要学的不仅是调用它们,更是揣摩设计者的意图:为什么参数要这么设计?为什么这里接收一个函数而不是一个普通值?为什么这里用axis而不是再拆一个函数?
这种“读函数背后的设计”的习惯,比背一百个API都值钱。API会忘,设计眼光不会。以后你自己封装工具函数时,也会下意识去想:这里要不要留一个回调参数?那里是不是该把策略参数化?这种思考一多,你写出的代码自然就开始“灵活”起来。
8. 函数进阶最容易踩的坑
8.1 循环闭包、this丢失、可变默认参数:三个高频翻车点
循环闭包捕获是JavaScript面试经典题:
javascript复制for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// 输出:3 3 3
原因很简单:var声明的i是全局变量,三个定时器回调共享同一个i;等100ms过去后,i已经变成了3,所以打印全是3。用let声明i,每次循环都会创建一个新的绑定:
javascript复制for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// 输出:0 1 2
如果还需要支持旧环境、用不了let,常见解法是包一层IIFE把i传进新作用域。这类问题看似“语法特性”,本质还是闭包捕获的时机问题。
this指向丢失是另一个高频坑。对象方法被当作回调传给第三方函数时,this常常从“原对象”变成“undefined”或“全局对象”。
javascript复制const user = {
name: "小明",
greet() {
console.log(`你好,我是${this.name}`);
}
};
setTimeout(user.greet, 100); // 输出:你好,我是undefined
解决方式是用箭头函数(箭头函数不绑定自己的this,会沿外层作用域找)或者用bind显式绑定:
javascript复制setTimeout(() => user.greet(), 100); // 正常
setTimeout(user.greet.bind(user), 100); // 也正常
Python可变默认参数是老生常谈的坑:
python复制def add_item(item, items=[]):
items.append(item)
return items
print(add_item("a")) # ['a']
print(add_item("b")) # ['a', 'b'] <- 第二次调用还带上了第一次的结果
问题在于默认参数[]只在函数定义时创建一次,之后每次调用都复用同一个列表。正确做法是:
python复制def add_item(item, items=None):
if items is None:
items = []
items.append(item)
return items
这段代码的核心教训是:别在函数定义的默认值里放“可变对象”。如果哪天你把默认参数从一个int改成list或dict,这个坑就会悄悄找上你。
8.2 函数未定义报错的排查顺序
我在第2章讲了Shell命令找不到和函数查找的相似性,这里给一个更具体的排查顺序。遇到xxx is not defined、NameError: name 'xxx' is not defined时,按以下顺序查,基本都能定位:
- 拼写:最容易也最低级,但确实最常见。特别是大小写,
getUser写成getuser,报错信息不会提示你“是不是大小写写错”。 - 作用域:函数是定义在哪个作用域里的?在函数A内部定义的函数B,只能在A内部用,拿到外面调用会报未定义。
- 导入/挂载顺序:Python循环导入、JavaScript模块加载顺序、浏览器里脚本加载顺序,都会导致“代码明明写了,为什么找不到”。
- 同名覆盖:一个内层作用域的同名变量遮住了外层函数,调用时看起来是函数,实则是个普通变量或个人对象,直接执行就报错。
这套流程很像排查Shell的“无法将X识别为 cmdlet、函数、脚本文件”:先看装没装(函数在不在),再看路径在不在(作用域能不能访问),最后看拼写和名称冲突。把函数名解析的机制想清楚了,这类报错就不会再让你一脸懵。
最后再分享一个我自己的体会。以前我拿到一段代码,第一反应是逐行读,看它做了什么;现在拿到代码,第一反应是看它的函数形态:哪些是普通函数,哪些是返回函数的函数,哪些是接受函数的函数。一旦这三类分清楚,代码的意图和边界就清楚了一大半。函数进阶带给我的不是更多API,而是一套看待代码的“坐标系”。这套坐标系往小里说能让代码少写一半,往大里说,直接决定了一个人写的是“脚本”还是“软件”。
