函数详解
函数是 JavaScript 中组织逻辑的基本单元,也是理解 this、闭包、高阶函数等进阶概念的基石。本文从定义方式讲起,系统对比函数声明与函数表达式、箭头函数的差异,并详解参数处理、arguments、IIFE 等核心特性。
一、函数声明与函数表达式
1.1 两种定义方式
// 函数声明(Function Declaration)
function greet(name) {
return "你好," + name;
}
// 函数表达式(Function Expression)
const greet2 = function (name) {
return "你好," + name;
};两者的核心区别在于函数提升:函数声明会被整体提升到作用域顶部,可以在声明之前调用;函数表达式只有变量名被提升,赋值之前的调用会报错。
console.log(sum(1, 2)); // 3,函数声明整体提升,可以先调用
function sum(a, b) {
return a + b;
}
console.log(multiply(2, 3)); // TypeError: multiply is not a function
const multiply = function (a, b) {
return a * b;
};1.2 对比表格
| 对比项 | 函数声明 | 函数表达式 |
|---|---|---|
| 语法 | function f() {} | const f = function () {} |
| 提升 | 整体提升,可提前调用 | 仅变量提升,赋值前调用报错 |
| 是否具名 | 自带函数名 | 通常匿名,也可命名(具名函数表达式) |
| 使用场景 | 模块级公共函数 | 回调、按条件定义、赋值给变量 |
具名函数表达式(Named Function Expression)在递归和调试时有优势:
const factorial = function fact(n) {
return n <= 1 ? 1 : n * fact(n); // 内部用 fact 递归,函数名只在此作用域可见
};
console.log(factorial(5)); // 120二、箭头函数
箭头函数是 ES6 引入的简洁语法,且与普通函数存在若干本质差异:
// 基本语法
const add = (a, b) => a + b;
const square = (x) => x * x; // 单个参数可省略括号
const noop = () => console.log("无参数");
// 函数体为表达式时直接返回,多语句需写 return
const stats = (arr) => {
const sum = arr.reduce((acc, n) => acc + n, 0);
return sum / arr.length;
};2.1 this 绑定不同
箭头函数没有自己的 this,它捕获定义时外层作用域的 this(词法绑定),且 this 一旦确定不会随调用方式改变:
const user = {
name: "张三",
greet: function () {
// 普通函数:setTimeout 回调里 this 不再指向 user
setTimeout(function () {
console.log(this.name); // undefined(非严格模式为 window.name)
}, 100);
// 箭头函数:捕获外层 greet 调用时的 this
setTimeout(() => {
console.log(this.name); // 张三
}, 100);
},
};
user.greet();2.2 无 arguments 与不能作构造函数
const f = () => {
console.log(arguments); // ReferenceError: arguments is not defined
};
// new 箭头函数会直接抛错
const F = () => {};
new F(); // TypeError: F is not a constructor箭头函数也没有 prototype 属性,因此不能使用 new 调用,这也与它"无自身 this"的设计一脉相承。
2.3 箭头函数与普通函数对比
| 对比项 | 普通函数 | 箭头函数 |
|---|---|---|
| this | 调用时动态绑定 | 定义时词法捕获,不可改变 |
| arguments | 有 | 没有(可用剩余参数替代) |
| 作为构造函数 | 可以 | 不可以 |
| prototype | 有 | 没有 |
| 适合场景 | 方法、构造函数、需要动态 this | 回调、简单映射、需要外层 this |
三、参数处理
3.1 默认参数
ES6 支持在形参后写默认值,仅在实参为 undefined 时生效:
function greet(name = "陌生人", prefix = "你好") {
return `${prefix},${name}`;
}
console.log(greet()); // 你好,陌生人
console.log(greet("李四", "早上好")); // 早上好,李四
console.log(greet(undefined, "嗨")); // 嗨,陌生人
console.log(greet(null)); // 你好,null(null 不是 undefined,默认值不生效)默认参数可以引用前面的参数,但注意此时函数体中不能再声明同名变量:
function calc(base = 1, multiple = base * 2) {
return base * multiple;
}
console.log(calc()); // 2
console.log(calc(3)); // 183.2 剩余参数
剩余参数用 ... 收集剩余所有实参为真数组,只能写在参数列表最后:
function sum(...nums) {
return nums.reduce((acc, n) => acc + n, 0);
}
console.log(sum(1, 2, 3, 4)); // 10
function log(prefix, ...rest) {
console.log(prefix, rest);
}
log("结果:", 1, 2, 3); // 结果: [1, 2, 3]3.3 参数解构
参数可以按对象或数组解构,直接提取所需字段:
function printUser({ name, age, city = "未知" }) {
console.log(`${name},${age} 岁,来自${city}`);
}
printUser({ name: "王五", age: 28 }); // 王五,28 岁,来自未知
function swap([a, b]) {
return [b, a];
}
console.log(swap([1, 2])); // [2, 1]四、arguments 对象
arguments 是普通函数内部自动存在的类数组对象,按实参顺序保存所有传入值:
function collect() {
console.log(arguments.length); // 3
console.log(arguments[0]); // a
console.log(Array.isArray(arguments)); // false,是类数组不是真数组
}
collect("a", "b", "c");4.1 转真数组的三种方式
function toArray() {
const a1 = Array.prototype.slice.call(arguments); // 经典方式
const a2 = Array.from(arguments); // ES6
const a3 = [...arguments]; // 扩展运算符
return [a1, a2, a3];
}
console.log(toArray(1, 2, 3));
// [[1,2,3], [1,2,3], [1,2,3]]4.2 与剩余参数对比
| 对比项 | arguments | 剩余参数 |
|---|---|---|
| 类型 | 类数组 | 真数组 |
| 包含内容 | 全部实参 | 剩余部分实参 |
| 与形参联动 | 非严格模式下标联动 | 无联动 |
| 箭头函数中 | 不存在 | 可用 |
| 推荐程度 | 兼容旧代码 | 现代首选 |
// 非严格模式下 arguments 与形参下标联动
function f(a) {
a = 100;
console.log(arguments[0]); // 100(非严格模式);严格模式下为 10
}
f(10);五、返回值
- 函数体内
return后立即结束执行并返回指定值; - 没有
return或return后面为空,返回undefined; return与返回值之间不能换行,否则会触发自动插入分号导致意外返回undefined。
function noReturn() {}
console.log(noReturn()); // undefined
function earlyReturn(n) {
if (n < 0) return; // 提前退出,返回 undefined
return n * 2;
}
console.log(earlyReturn(-1)); // undefined
console.log(earlyReturn(5)); // 10
// 注意:return 后换行会被分号自动插入截断
function bad() {
return;
42;
}
console.log(bad()); // undefined箭头函数单行表达式写法自带返回值;若要返回对象字面量必须加括号,否则花括号会被当成函数体:
const f = () => ({ id: 1, name: "张三" });
console.log(f()); // { id: 1, name: '张三' }六、IIFE(立即执行函数)
IIFE(Immediately Invoked Function Expression)是定义后立刻执行的函数表达式,语法为在函数表达式外再包一层括号:
(function () {
console.log("立即执行");
})(); // 立即执行
// 也可以把调用括号放在外层括号内部
(function () {
console.log("另一种写法");
}());6.1 为什么必须用括号包裹
函数声明必须以 function 关键字开头,解释器遇到 function 会当作声明解析,而声明不能立即调用。用括号包裹后,function 出现在表达式位置,就变成了函数表达式,从而可以被立即调用:
// 直接写会语法错误
function () { console.log(1); }(); // SyntaxError
// 包一层括号变为表达式
(function () { console.log(1); })(); // 正常执行其他把函数变成表达式的写法同样可行,例如 !function(){}()、+function(){}()。
6.2 作用
- 隔离作用域:内部变量不会污染全局(这也是模块化早期实现方式);
- 创建私有环境:配合闭包保存数据。
const counter = (function () {
let count = 0; // 外部无法直接访问
return {
inc: () => ++count,
get: () => count,
};
})();
counter.inc();
counter.inc();
console.log(counter.get()); // 2
console.log(counter.count); // undefined,私有变量不可达七、name 与 length 属性
每个函数都有只读的 name 和 length 属性:
function add(a, b) {}
const sub = function (x, y, z) {};
const arrow = (p) => {};
console.log(add.name); // add,函数声明取自声明名
console.log(sub.name); // sub,函数表达式取自变量名
console.log(arrow.name); // arrow
console.log(add.length); // 2,形参个数(不含默认参数与剩余参数)
console.log(sub.length); // 3
console.log(arrow.length); // 1length 只统计默认参数之前的形参个数:
function f(a, b = 1, c) {}
console.log(f.length); // 1八、函数提升与声明位置
函数声明整体提升到当前作用域顶部,因此声明位置无关紧要,但同名函数声明会相互覆盖,且提升只发生在所在作用域内:
console.log(typeof f); // function
f(); // 我是覆盖后的版本
function f() {
console.log("我是第一个版本");
}
function f() {
console.log("我是覆盖后的版本");
}if (true) {
function g() {
console.log("块内声明");
}
}
// 在模块/严格模式下,g 只在 if 块内可见函数表达式赋值则遵循变量提升规则:const/let 声明的变量存在暂时性死区,在赋值前访问会报错。建议模块顶层优先使用函数声明,逻辑需要按条件创建时再用函数表达式。
九、回调函数与调用时机
回调函数是"把函数作为参数传给另一个函数,由对方在合适的时机调用"。关键在于:你定义回调,但调用时机由外部决定。
function process(data, onSuccess, onError) {
if (data >= 0) {
onSuccess(data * 2);
} else {
onError("数据非法");
}
}
process(10, (r) => console.log("成功:", r), (e) => console.log("失败:", e));
// 成功: 20
process(-1, (r) => console.log("成功:", r), (e) => console.log("失败:", e));
// 失败: 数据非法回调的典型场景:
// 数组方法:同步回调,立即逐个调用
[1, 2, 3].forEach((n) => console.log(n * 2));
// 定时器:异步回调,稍后调用
setTimeout(() => console.log("一秒后执行"), 1000);
// 事件监听:回调在事件触发时调用
// document.addEventListener("click", () => console.log("点击了"));
// 高阶函数:返回的新函数中调用原回调
function withLog(fn) {
return function (...args) {
console.log("调用参数:", args);
return fn(...args);
};
}
const loggedAdd = withLog((a, b) => a + b);
console.log(loggedAdd(2, 3)); // 调用参数: [2, 3] 然后输出 5调用时机由环境决定,常见的几类时机:
| 回调类型 | 调用时机 | 示例 |
|---|---|---|
| 同步回调 | 当前调用栈内立即执行 | forEach、map、sort |
| 宏任务回调 | 事件循环下一轮 | setTimeout、setInterval |
| 微任务回调 | 当前任务结束后立即执行 | Promise.then |
| 事件回调 | 事件触发时 | click、load |
理解回调的调用时机,是掌握异步编程与事件循环的第一步,也是后续学习 Promise、async/await 的重要基础。