函数

函数是 JavaScript 最核心的概念,也是与 Java 差异最大的部分。最根本的一点:JS 的函数是一等公民——函数是”值”,可以赋给变量、当参数传、当返回值返回;并且 this运行时动态绑定的,与 Java 的 this 含义完全不同。

这一篇覆盖函数从基础到进阶的全部常用知识点:函数声明/表达式、默认参数、rest 参数、箭头函数、IIFE、闭包、this、回调与高阶函数、call/apply/bind。

一、差异总览

维度 Java JavaScript
函数地位 方法,属于类 一等公民,是值
定义方式 返回值 方法名(参数) {} 声明式 / 表达式 / 箭头函数
返回类型 / 参数类型 必须声明 无类型声明
重载 支持 不支持(后定义覆盖前面的)
参数个数 固定(可变参数除外) 随意,多传少传都行
this 指向当前实例 运行时动态绑定
默认参数 无(靠重载) 支持 (a = 1)

二、函数基础

1. 定义与调用(与 Java 对照)

1
2
3
4
function add(a, b) {
return a + b;
}
add(1, 2); // 3
  • 没有返回类型、参数类型,用 function 关键字。
  • 没有返回值时,默认返回 undefined(Java 的 void)。

2. 函数声明 vs 函数表达式

1
2
3
4
5
6
// 函数声明:有"提升",可先调用后声明
console.log(add(1, 2));
function add(a, b) { return a + b; }

// 函数表达式:匿名函数赋给变量,无提升(const 有暂时性死区)
const minus = function(a, b) { return a - b; };

⚠️ 函数声明会被提升到作用域顶部,所以”先调用后定义”也能运行;函数表达式(用 const)则必须先定义再调用。

3. 函数是一等公民

函数是值,可以到处传递:

1
2
3
4
5
const fn = add;          // 赋给变量
[1, 2, 3].map(add); // 作为参数传入
function makeFn() {
return function() {}; // 作为返回值返回
}

三、参数

1. 默认参数

1
2
3
4
5
function greet(name = "World") {
return `Hello, ${name}`;
}
greet(); // "Hello, World"(未传参用默认值)
greet("Tom"); // "Hello, Tom"
  • 只在未传参或传 undefined 时生效(传 null 不会用默认值)。
  • Java 没有默认参数,靠重载模拟。

2. rest 参数 ...args

收集剩余参数成数组,对应 Java 的可变参数 int... nums

1
2
3
4
function sum(...nums) {
return nums.reduce((a, b) => a + b, 0);
}
sum(1, 2, 3, 4); // 10
  • ... 必须放在参数列表最后

3. 参数解构

1
2
3
4
function print({ name, age }) {
console.log(name, age);
}
print({ name: "Tom", age: 18 }); // Tom 18

4. arguments 对象(旧语法)

1
2
3
function f() {
console.log(arguments); // 类数组对象,包含所有实参
}

⚠️ arguments 是类数组(有 length、下标,但没有数组方法),且箭头函数没有 arguments,现代代码一般用 rest 参数替代。

四、箭头函数

箭头函数是更简洁的函数写法,但行为与普通函数有本质区别

1. 语法

1
2
3
4
const add = (a, b) => a + b;        // 单表达式,直接返回(省略 return 和 {})
const add2 = (a, b) => { return a + b; }; // 有 {} 需写 return
const double = x => x * 2; // 单参数可省略括号
const hello = () => "hi"; // 无参数必须写 ()

2. 与普通函数的区别(重点)

特性 普通函数 箭头函数
自己的 this 有(动态绑定) 没有,继承外层作用域的 this
arguments 没有
能否 new 不能(会报错)
prototype 没有

⚠️ 最核心:箭头函数**没有自己的 this**,它用的 this 是定义时外层作用域的 this。这既是它最大的优点(解决 this 丢失),也是新手最常踩的坑(见下面 this 章节)。

五、立即执行函数 IIFE

IIFE(Immediately Invoked Function Expression)定义后立即执行一次

1
2
3
4
5
6
7
8
9
(function() {
// 代码,作用域被隔离
console.log("立即执行");
})();

// 箭头函数版
(() => {
console.log("立即执行");
})();

作用:隔离作用域,避免变量污染全局。在早期没有 let/const 块级作用域时是必备技巧;现在 let/const 已能隔离块级作用域,但 IIFE 仍常用于一次性初始化、模块化封装等场景。

六、闭包 closure

闭包 = 函数 + 它定义时所在的外层作用域。内层函数”记住”并能访问外层函数的变量,即使外层函数已执行完毕。

1
2
3
4
5
6
7
8
9
10
function makeCounter() {
let count = 0; // 外层变量
return function() { // 内层函数"捕获"了 count
count++;
return count;
};
}
const counter = makeCounter();
counter(); // 1
counter(); // 2(count 被记住并持续累加)

对比 Java:类似匿名内部类捕获局部变量,但 Java 要求变量是 final/effectively final(不可改);JS 闭包可以直接修改外层变量

经典坑(循环里的 var)

1
2
3
4
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
// 打印 3, 3, 3(var 是函数作用域,所有回调共享同一个 i)
1
2
3
4
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
// 打印 0, 1, 2(let 是块级作用域,每轮循环是独立的 i)

⚠️ 记忆点:闭包记住的是变量本身(引用),不是当时的快照值。循环里用 var 会共享变量,用 let 才能每轮独立——这正是第 1 篇强调”循环用 let 不用 var”的原因。

闭包的实际应用

  • 封装私有变量(计数器、缓存)。
  • 事件监听、定时器回调中保留状态。
  • 函数柯里化、防抖/节流等(进阶)。

七、this 绑定规则

JS 的 this **不指向定义它的对象,而取决于”谁调用”**,是运行时决定的。

1. 默认绑定:独立调用

1
2
3
4
function f() {
console.log(this);
}
f(); // window(非严格模式)/ undefined(严格模式)

2. 隐式绑定:谁调用指向谁

1
2
3
4
5
6
7
const obj = {
name: "Tom",
say() {
console.log(this.name);
}
};
obj.say(); // "Tom"(this 是 obj)

3. 显式绑定:call / apply / bind

见下一节。

4. new 绑定:构造函数

new 调用时,this 指向新创建的对象。

5. 箭头函数:不绑定 this

1
2
3
4
5
6
7
8
9
const obj = {
name: "Tom",
say: function() {
setTimeout(() => {
console.log(this.name); // "Tom"(箭头函数继承外层 say 的 this)
}, 0);
}
};
obj.say();

用普通函数会打印 undefined(回调里 this 指向 window),用箭头函数则正确——这是箭头函数最经典的用法。

6. this 丢失的坑

1
2
3
const obj = { name: "Tom", say() { console.log(this.name); } };
const fn = obj.say;
fn(); // undefined(方法被单独取出,this 丢了)

解决:用 bind 固定 this,或用箭头函数。

八、call / apply / bind

三者都用于改变函数的 this

1
2
3
4
5
6
7
8
9
function greet(greeting) {
console.log(`${greeting}, ${this.name}`);
}
const user = { name: "Tom" };

greet.call(user, "Hello"); // "Hello, Tom" 立即调用,参数逐个传
greet.apply(user, ["Hi"]); // "Hi, Tom" 立即调用,参数用数组
const bound = greet.bind(user, "Hey"); // 返回绑定了 this 的新函数
bound(); // "Hey, Tom" 不立即调用
方法 是否立即调用 参数形式 返回
call 参数列表 函数返回值
apply 参数数组 函数返回值
bind 参数列表 绑定了 this 的新函数

记忆:call/apply 立即执行,bind 返回新函数;call 传列表、apply 传数组(apply 的 a = array)。

九、回调与高阶函数

  • 回调函数:作为参数传给另一个函数、在适当时机被调用的函数。
  • 高阶函数:接收函数作为参数、或返回函数的函数。
1
2
3
4
5
6
7
8
// map 是高阶函数,x => x * 2 是回调
[1, 2, 3].map(x => x * 2); // [2, 4, 6]

// 自定义高阶函数
function repeat(n, action) {
for (let i = 0; i < n; i++) action(i);
}
repeat(3, i => console.log(i)); // 0 1 2

回调分同步(如 map)和异步(如 setTimeout、事件监听),异步回调是 JS 处理”耗时操作”的基础,后续深入。

十、速查表

需求 写法
声明函数 function f(a, b) {}
函数表达式 const f = function(a, b) {};
箭头函数 (a, b) => a + b
默认参数 function f(a = 1) {}
剩余参数 function f(...args) {}
立即执行 (function(){})(); / (() => {})();
闭包计数 外层 let n = 0 + 返回内层函数
绑定 this fn.call(obj, a) / fn.apply(obj, [a]) / fn.bind(obj)
高阶函数 arr.map(x => ...)

十一、复习要点

  1. 函数是一等公民:是值,可赋给变量、传参、返回。
  2. 函数声明会提升,函数表达式(const)不会。
  3. 参数无类型、个数随意、不支持重载;默认参数、rest 参数、解构是常用特性。
  4. 箭头函数没有自己的 this / arguments / prototype,不能 new,this 继承外层。
  5. IIFE 立即执行,用于隔离作用域。
  6. 闭包 = 函数 + 外层作用域,记住的是变量引用;循环用 var 会共享变量(打印全一样),用 let 才独立。
  7. this 由”谁调用”决定:默认 window、隐式绑定调用者、显式 call/apply/bind、new 绑定、箭头函数继承外层。
  8. 方法被单独取出会丢失 this,用 bind 或箭头函数解决。
  9. call/apply 立即执行(call 列表、apply 数组),bind 返回新函数。
  10. 回调是传给别的函数的函数,高阶函数是接收/返回函数的函数。

下一篇预告:对象与常用内置 API——对象字面量、解构、原型与 class、String/Math/Date/JSON 等。