JavaScript 函数与作用域
AI 辅助生成本文由 AI 起草并经人工核查,示例均标明软件版本;发现错误请以官方文档为准。
函数是 JavaScript 的一等公民:可以赋值给变量、作为参数传递、作为返回值返回。理解函数,核心是理解作用域链与闭包。
三种定义方式
// 1. 函数声明:会被提升,可在定义前调用
function add(a, b) {
return a + b;
}
// 2. 函数表达式:赋值给变量,不提升
const multiply = function (a, b) {
return a * b;
};
// 3. 箭头函数:更短,且没有自己的 this
const divide = (a, b) => a / b;
区别要点:
- 函数声明有提升,可以先调用后定义;函数表达式和箭头函数必须先定义再使用。
- 箭头函数的
this来自外层作用域(词法绑定),普通函数的this由调用方式决定。 - 需要递归命名、需要
this、或者要作为构造函数时,用function;回调、数组方法里的短函数用箭头函数。
参数:默认值、剩余参数、解构
function greet(name = "匿名", punctuation = "!") {
return `你好,${name}${punctuation}`;
}
greet(); // "你好,匿名!"
greet("ann"); // "你好,ann!"
默认值只在参数为 undefined 时生效,传 null 不会触发:
greet(null); // "你好,null!"
参数个数不确定用剩余参数(收集成真数组):
function sum(...nums) {
return nums.reduce((total, n) => total + n, 0);
}
sum(1, 2, 3); // 6
注意剩余参数必须是最后一个形参。函数内部还有 arguments 对象,但它是类数组、没有数组方法,新代码一律用 ...。
参数较多时用一个对象,配合解构:
function createUser({ name, role = "user", active = true }) {
return { name, role, active };
}
createUser({ name: "ann", role: "admin" });
// { name: "ann", role: "admin", active: true }
这样调用方不必记住参数顺序。
返回值
没有 return 或 return; 的函数返回 undefined:
function log(msg) {
console.log(msg);
}
log("hi"); // 控制台输出 hi,表达式的值是 undefined
箭头函数返回对象字面量时要加括号,否则 {} 会被当成函数体:
const makeUser = (name) => ({ name, createdAt: Date.now() });
作用域与作用域链
作用域分三层:全局作用域、函数作用域、块级作用域(let / const + {})。
查找变量时从内向外逐层找,这条链路叫作用域链:
const global = "外层";
function outer() {
const mid = "中层";
function inner() {
const local = "内层";
console.log(local, mid, global); // "内层 中层 外层"
}
inner();
}
内层能读外层,外层不能读内层:
function outer() {
const secret = "不可见";
}
console.log(secret); // ReferenceError
闭包
函数记住了定义时的作用域,即使在外层函数执行结束后,依然能访问其中的变量——这就是闭包。
function makeCounter() {
let count = 0;
return function () {
count += 1;
return count;
};
}
const next = makeCounter();
next(); // 1
next(); // 2
next(); // 3
count 没有随 makeCounter() 返回而销毁,因为返回的函数还持有它。闭包的典型用途:保存私有状态。
function requireMin(n) {
return (value) => value.length >= n;
}
const longEnough = requireMin(8);
["abc", "abcdefghij"].filter(longEnough); // ["abcdefghij"]
var 在循环里的经典陷阱
// 问题代码
const fns = [];
for (var i = 0; i < 3; i++) {
fns.push(() => i);
}
fns.forEach((fn) => console.log(fn())); // 3 3 3
var i 是函数作用域,三个闭包共享同一个 i,循环结束时 i 已经是 3。
改成 let,每次迭代生成独立的块级绑定:
const fns = [];
for (let i = 0; i < 3; i++) {
fns.push(() => i);
}
fns.forEach((fn) => console.log(fn())); // 0 1 2
这也是「新代码不用 var」最直接的理由之一。
this 的一句话说明
普通函数的 this 取决于怎么被调用:
const obj = {
value: 42,
getValue() {
return this.value;
},
};
obj.getValue(); // 42,this 指向 obj
const detached = obj.getValue;
detached(); // undefined(严格模式下),this 丢失
箭头函数没有自己的 this,会沿用外层,因此适合作为回调:
const timer = {
seconds: 0,
start() {
setInterval(() => {
this.seconds += 1; // this 仍是 timer
}, 1000);
},
};
常见错误与注意事项
- 在定义前调用箭头函数:不提升,会抛
ReferenceError。 - 箭头函数里用
this期望指向调用者:箭头函数继承外层this,做对象方法时用普通函数写法。 - 箭头函数直接返回对象忘了括号:
() => { name }返回undefined,正确写法是() => ({ name })。 - 闭包意外持有大对象:变量被闭包引用就不会被回收,长生命周期的闭包要注意别抓住整个大数组。
- 循环里用
var+ 异步回调:全部拿到最后一次的值,改用let。
小结
- 定义方式:需要提升或
this用function,短回调用箭头函数。 - 参数支持默认值、剩余参数、对象解构。
- 内层可读外层,外层读不到内层;闭包就是「函数带着定义时的作用域一起走」。
- 循环里声明计数器一律用
let。