函数是 JavaScript 的"一等公民",也是这门语言中最核心、最灵活、最容易被低估的概念。它不仅仅是一段可复用的代码块,更是一个可以赋值给变量、作为参数传递、作为返回值返回、甚至动态创建的对象。本文将从最基础的函数声明讲起,系统梳理函数声明、函数表达式、箭头函数、IIFE、参数处理、返回值、四种调用方式、this 绑定规则、call/apply/bind、闭包、高阶函数、递归、柯里化与纯函数等知识点,并配以大量可运行的代码示例与可视化图解。35 分钟沉浸式学习,帮你真正吃透 JavaScript 函数的定义与调用。
函数到底是什么
在 JavaScript 中,函数是一段被封装起来的、可以重复执行的代码。但从语言层面看,函数更准确的定义是:一个可调用的对象(callable object)。它拥有普通对象的属性(比如 name、length),同时内部还带有 [[Call]] 内部方法,因此可以被 () 调用执行。
function sayHi() {}
console.log(typeof sayHi); // "function"
console.log(sayHi.name); // "sayHi"
console.log(sayHi.length); // 0(形参个数)
// 可以给它挂属性
sayHi.version = '1.0';
console.log(sayHi.version); // "1.0"
// 可以把它放进数组、对象里
const fns = [sayHi, Math.max];
console.log(fns.length); // 2
函数 = 代码逻辑 + 对象属性 + 词法环境
一等公民:函数可以像普通值一样使用
理解这一点非常重要:正是因为函数是对象,我们才能实现回调函数、高阶函数、工厂函数、装饰器等高级模式。接下来,我们逐一拆解函数的各种定义方式。
函数声明(Function Declaration)
函数声明是最经典、最直观的定义方式,使用 function 关键字开头,后面紧跟函数名、参数列表和函数体。
function add(a, b) {
return a + b;
}
console.log(add(2, 3)); // 5
console.log(add.name); // "add"
console.log(add.length); // 2
// 关键特性:函数声明提升(Hoisting)
console.log(mul(4, 5)); // 20 —— 调用在前也不报错
function mul(a, b) {
return a * b;
}
mul 被提升到顶部
mul(4, 5)
20,不报错
函数声明整体被提升(含函数体)
适用场景:需要在文件或模块顶层定义的、逻辑独立、可复用的具名函数。函数声明的提升特性让代码组织更自由,但也容易掩盖"先使用后定义"的逻辑问题,团队协作时建议统一风格。
函数表达式(Function Expression)
函数表达式是把一个匿名(或具名)函数赋值给变量。它与函数声明最大的区别在于:不会被整体提升,只有在赋值语句执行之后才能调用。
const sub = function (a, b) {
return a - b;
};
// 具名函数表达式(推荐,便于调试)
const div = function divide(a, b) {
if (b === 0) throw new Error('除数不能为 0');
return a / b;
};
console.log(sub(10, 4)); // 6
console.log(div.name); // "divide"
// 提升行为对比
console.log(typeof foo); // "undefined"(变量提升,值为 undefined)
console.log(typeof bar); // "function"
var foo = function () {};
function bar() {}
适用场景:需要把函数作为值传递、写在对象字面量中、或者希望明确表达"这是一个变量"的语义时。使用具名函数表达式是社区推荐的做法,能显著提升线上错误的可读性。
箭头函数(Arrow Function)
ES6 引入的箭头函数是最简洁的函数定义方式,同时它带来两个根本性的变化:没有自己的 this,以及没有 arguments 对象。
const f1 = () => 42;
const f2 = x => x * 2;
const f3 = (a, b) => a + b;
const f4 = (a, b) => {
const sum = a + b;
return sum * 2;
};
// 返回对象字面量必须加括号
const f5 = () => ({ ok: true });
// 词法 this:继承外层作用域的 this
const counter = {
count: 0,
start() {
setInterval(() => {
this.count++; // this 指向 counter
}, 1000);
}
};
箭头函数不能用作对象方法(若方法内需要 this)
常见陷阱:箭头函数没有 prototype,因此不能用作构造函数;也不适合定义对象的方法,因为方法中的 this 需要指向调用对象。但它在回调、数组方法、异步链式调用中几乎是最佳选择。
其他定义方式:IIFE 与 Function 构造器
除了三大主流方式,JavaScript 还提供了立即执行函数表达式和 Function 构造器。前者在模块化之前被广泛使用,后者则因性能与安全问题极少推荐。
(function () {
const secret = '私有变量';
console.log(secret);
})();
// 带参数与返回值的 IIFE
const result = (function (n) {
return n * n;
})(9);
console.log(result); // 81
// 2. Function 构造器(不推荐)
const sum = new Function('a', 'b', 'return a + b');
console.log(sum(3, 4)); // 7
// 3. 对象方法简写(ES6)
const user = {
name: '张玥',
greet() { return 'Hi, ' + this.name; }
};
console.log(user.greet()); // "Hi, 张玥"
参数处理:默认值、剩余参数与 arguments
JavaScript 的参数机制非常宽松:形参数量与实际传入数量可以不一致,缺少的参数是 undefined,多余的参数则被忽略(但可通过 arguments 或剩余参数访问)。
function createUser(name, role = 'guest', active = true) {
return { name, role, active };
}
console.log(createUser('Amy'));
// { name: 'Amy', role: 'guest', active: true }
// 默认值可以是表达式,甚至依赖前面的参数
function area(w, h = w) {
return w * h;
}
console.log(area(5)); // 25(正方形)
// 2. 剩余参数:收集为真数组
function sumAll(...nums) {
return nums.reduce((acc, n) => acc + n, 0);
}
console.log(sumAll(1, 2, 3, 4)); // 10
// 3. arguments:类数组对象(仅普通函数有)
function legacy() {
console.log(arguments.length);
console.log(Array.from(arguments));
}
legacy(1, 2, 3); // 3 ; [1, 2, 3]
sumAll(1, 2, 3, 4) 时nums 数组
arguments 是类数组,没有 map、reduce 等方法。
关于解构参数:当参数较多时,使用对象解构可以显著提升可读性,避免"参数顺序地狱"。
function request({
url,
method = 'GET',
timeout = 5000,
headers = {}
}) {
console.log(url, method, timeout, headers);
}
request({ url: '/api/user', timeout: 3000 });
// /api/user GET 3000 {}
返回值与 return 语句
函数通过 return 把结果交还给调用者。理解 return 的终止行为与"无返回值"的默认结果,是写好函数的基础。
function noReturn() {
const x = 1 + 1;
}
console.log(noReturn()); // undefined
// 2. return 会立即终止函数
function check(n) {
if (n < 0) return '负数';
if (n === 0) return '零';
return '正数'; // 前面的分支都不满足才执行
}
// 3. 返回多个值:用数组或对象
function minMax(arr) {
return [Math.min(...arr), Math.max(...arr)];
}
const [min, max] = minMax([3, 9, 1]);
console.log(min, max); // 1 9
undefinedundefinedreturn { ok: true };实际等价于
return;,务必把值写在同一行。
四种调用方式与 this 绑定
同一个函数,用不同方式调用,this 的指向完全不同。这是 JavaScript 中最容易出错、也最值得花时间掌握的部分。
function whoAmI() {
'use strict';
return this;
}
console.log(whoAmI()); // undefined
// 2. 方法调用 → this 指向调用对象
const obj = {
name: 'obj',
getName() { return this.name; }
};
console.log(obj.getName()); // "obj"
// 3. 构造函数调用 → this 指向新对象
function Person(name) {
this.name = name;
}
const p = new Person('张玥');
console.log(p.name); // "张玥"
// 4. 间接调用 → call / apply / bind 指定 this
function greet(greeting) {
return greeting + ', ' + this.name;
}
console.log(greet.call({ name: 'Amy' }, 'Hello'));
// "Hello, Amy"
new 绑定(最高)丢失 this 的经典场景:把方法赋值给变量、作为回调传入、在定时器中调用,都会导致 this 丢失。解决方案有三种:箭头函数、bind 绑定、或者在外层用变量(如 const self = this)缓存。
const timer = {
seconds: 0,
start() {
setInterval(function () {
this.seconds++; // ❌ this 不是 timer
}, 1000);
}
};
// 方案一:箭头函数(推荐)
setInterval(() => { this.seconds++; }, 1000);
// 方案二:bind 显式绑定
setInterval(function () {
this.seconds++;
}.bind(this), 1000);
// 方案三:缓存 this(ES6 之前常用)
const self = this;
setInterval(function () { self.seconds++; }, 1000);
call、apply 与 bind 的实战用法
这三个方法都用于显式指定函数执行时的 this,区别在于传参方式与是否立即执行。
return `${this.name} 在 ${city} 做 ${job}`;
}
const person = { name: '张玥' };
// call:参数逐个传入,立即执行
console.log(introduce.call(person, '北京', '前端'));
// apply:参数以数组传入,立即执行
console.log(introduce.apply(person, ['上海', '架构师']));
// bind:返回新函数,不立即执行
const bound = introduce.bind(person, '深圳');
console.log(bound('工程师'));
// 实用技巧:借用数组方法
const arrayLike = { 0: 'a', 1: 'b', length: 2 };
const realArr = Array.prototype.slice.call(arrayLike);
console.log(realArr); // ['a', 'b']
记忆口诀:call 逗号,apply 数组,bind 返回
在现代代码中,apply 的使用频率已经大幅下降,因为扩展运算符 ... 可以更优雅地完成数组展开。但 bind 依然是 React 类组件、事件处理器等场景的常用工具。
作用域、闭包与函数工厂
函数在定义时就会"记住"自己所在的词法环境,这就是闭包。闭包让函数拥有了"私有状态",也是模块模式、防抖节流、柯里化的基础。
function createCounter(initial = 0) {
let count = initial;
return {
increment() { return ++count; },
decrement() { return --count; },
value() { return count; }
};
}
const c1 = createCounter(10);
const c2 = createCounter();
c1.increment();
console.log(c1.value(), c2.value()); // 11 0(互不影响)
// 经典循环陷阱
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
// 输出 3 3 3(var 没有块级作用域)
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
// 输出 0 1 2(let 每次迭代创建新绑定)
再看一个实战中极常见的例子——防抖函数,它正是闭包的经典应用:
function debounce(fn, delay = 300) {
let timer = null; // 被闭包保存
return function (...args) {
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}
const onSearch = debounce(function (kw) {
console.log('搜索:', kw);
}, 500);
onSearch('a');
onSearch('ab');
onSearch('abc'); // 只有这次真正执行
高阶函数与回调
接受函数作为参数、或者返回一个函数的函数,称为高阶函数。它是函数式编程的核心,也是 JavaScript 数组方法的设计基础。
const nums = [1, 2, 3, 4, 5];
const doubled = nums.map(n => n * 2);
const evens = nums.filter(n => n % 2 === 0);
const total = nums.reduce((a, b) => a + b, 0);
// 自定义高阶函数:包装日志
function withLog(fn) {
return function (...args) {
console.log('调用参数:', args);
const res = fn.apply(this, args);
console.log('返回值:', res);
return res;
};
}
const loggedAdd = withLog((a, b) => a + b);
loggedAdd(2, 8);
// 调用参数: [2, 8]
// 返回值: 10
map 映射filter 过滤reduce 归约forEach 遍历sort 排序find 查找回调让逻辑与数据解耦,代码更声明式
回调地狱与异步:当回调层层嵌套时,代码可读性会急剧下降。现代 JavaScript 使用 Promise 与 async/await 来解决这一问题,但本质依然是函数的传递与调用。
递归函数
递归是函数调用自身的一种技巧,适用于处理树形结构、分治问题、数学定义等场景。写递归必须明确两件事:终止条件与递推关系。
function factorial(n) {
if (n <= 1) return 1; // 终止条件
return n * factorial(n - 1);
}
console.log(factorial(5)); // 120
// 遍历树形结构
function walk(node, depth = 0) {
console.log(' '.repeat(depth) + node.name);
(node.children || []).forEach(c => walk(c, depth + 1));
}
walk({
name: 'root',
children: [
{ name: 'a', children: [{ name: 'a1' }] },
{ name: 'b' }
]
});
// root / a / a1 / b
4 × factorial(3)
3 × factorial(2)
2 × factorial(1)
→ 1(触底)
Maximum call stack size exceeded,可考虑改用循环或尾递归优化。
柯里化、纯函数与副作用
柯里化(Currying)把接受多个参数的函数,转换成一系列只接受一个参数的函数。它能显著提升函数的复用性与组合能力。
const add3 = (a, b, c) => a + b + c;
add3(1, 2, 3); // 6
// 柯里化写法
const curryAdd = a => b => c => a + b + c;
curryAdd(1)(2)(3); // 6
// 实战:创建可复用的校验器
const checkLength = min => max => str =>
str.length >= min && str.length <= max;
const isUsername = checkLength(3)(16);
console.log(isUsername('zhangyue')); // true
console.log(isUsername('ab')); // false
// 纯函数:相同输入必得相同输出,无副作用
const pureAdd = (a, b) => a + b; // ✅ 纯
let total = 0;
const impureAdd = n => total += n; // ❌ 有副作用
函数编写最佳实践
- 单一职责:一个函数只做一件事,函数名要能说明它做了什么
- 参数精简:参数超过 3 个时,改用对象解构传参
- 尽早返回:用卫语句代替深层嵌套的 if-else
- 优先使用 const 定义函数:避免意外的重新赋值与提升带来的困惑
- 回调优先用箭头函数:简洁且 this 行为符合直觉
- 对象方法用方法简写:需要 this 时不要用箭头函数
- 保持函数短小:超过 50 行的函数通常可以拆分
- 避免过多的副作用:核心逻辑尽量写成纯函数
- 为递归设置明确边界:防止栈溢出
- 善用默认参数与可选链:减少防御性判断代码
function process(data, type, flag, cb) {
if (data) {
if (type === 'a') {
if (flag) { /* ... */ }
}
}
}
// 正面示例:单一职责 + 对象参数 + 尽早返回
const processOrder = ({ data, type, needCache = false }) => {
if (!data) return null; // 卫语句
if (type !== 'a') return data;
const result = normalize(data);
return needCache ? cache(result) : result;
};
/* 函数设计的黄金法则 */
/* 输入清晰 → 逻辑单一 → 输出可预期 */
全文总结
本文从函数的本质出发,系统梳理了 JavaScript 中函数的定义方式与调用方式。回顾一下核心要点:
function decl() {} // 声明,会提升
const expr = function () {}; // 表达式,不提升
const arrow = () => {}; // 箭头,无自身 this
(function () {})(); // IIFE,立即执行
const o = { m() {} }; // 方法简写
// 调用方式速查
fn(); // 普通调用
obj.fn(); // 方法调用
new Fn(); // 构造调用
fn.call(ctx, ...args); // 显式绑定
fn.apply(ctx, argsArray); // 数组绑定
const b = fn.bind(ctx); // 返回绑定函数
函数是 JavaScript 的骨架,值得反复咀嚼
建议把文中的示例逐个在控制台运行一遍,并尝试修改参数观察结果变化。真正理解函数,你才算真正踏入了 JavaScript 的大门。下一篇我们将深入探讨 this 绑定规则与原型链,敬请期待。