JavaScript 函数

基本上所有的高级语言都支持函数,JavaScript 也不例外。JavaScript
的函数不但是“头等公民”,而且可以像变量一样使用,具有非常强大的抽象能力。

借助抽象,我们才能不关心底层的具体计算过程,而直接在更高的层次上思考问题。写计算机程序也是一样,函数就是最基本的一种代码抽象的方式。

函数(function)的定义

在 JavaScript 中,定义函数的方式如下:

1
2
3
4
5
6
7
function abs(x) {
if (x >= 0) {
return x;
} else {
return -x;
}
}

上述 abs() 函数的定义如下:

  • function 指出这是一个函数定义;
  • abs是函数的名称;
  • (x)括号内列出函数的参数,多个参数以,分隔;
  • { ... }之间的代码是函数体,可以包含若干语句,甚至可以没有任何语句。

请注意,函数体内部的语句在执行时,一旦执行到 return
时,函数就执行完毕,并将结果返回。因此,函数内部通过条件判断和循环可以实现非常复杂的逻辑。

如果没有 return 语句,函数执行完毕后也会返回结果,只是结果为 undefined

由于 JavaScript 的函数也是一个对象,上述定义的 abs()
函数实际上是一个函数对象,而函数名 abs 可以视为指向该函数的变量。

因此,第二种定义函数的方式如下:

1
2
3
4
5
6
7
let abs = function (x) {
if (x >= 0) {
return x;
} else {
return -x;
}
};

在这种方式下,function (x) { ... }是一个匿名函数,它没有函数名。但是,这个匿名函数赋值给了变量
abs,所以,通过变量 abs 就可以调用该函数。

上述两种定义_完全等价_,注意第二种方式按照完整语法需要在函数体末尾加一个;,表示赋值语句结束。

函数的特点:

  • 包含 0 或多个参数
  • 存在或无返回值

调用函数

调用函数时,按顺序传入参数即可:

1
2
abs(10); // 返回 10
abs(-9); // 返回 9

由于 JavaScript
允许传入任意个参数而不影响调用,因此传入的参数比定义的参数多也没有问题,虽然函数内部并不需要这些参数:

1
2
abs(10, "blablabla"); // 返回 10
abs(-9, "haha", "hehe", null); // 返回 9

传入的参数比定义的少也没有问题:

1
abs(); // 返回 NaN

此时abs(x)函数的参数 x 将收到undefined,计算结果为NaN

要避免收到undefined,可以对参数进行检查:

1
2
3
4
5
6
7
8
9
10
function abs(x) {
if (typeof x !== "number") {
throw "Not a number";
}
if (x >= 0) {
return x;
} else {
return -x;
}
}

arguments

JavaScript还有一个免费赠送的关键字
arguments,它只在函数内部起作用,并且永远指向当前函数的调用者传入的所有参数。arguments
类似Array 但它不是一个 Array

利用
arguments,你可以获得调用者传入的所有参数。也就是说,即使函数不定义任何参数,还是可以拿到参数的值:

1
2
3
4
5
6
7
8
9
10
11
function abs() {
if (arguments.length === 0) {
return 0;
}
let x = arguments[0];
return x >= 0 ? x : -x;
}

abs(); // 0
abs(10); // 10
abs(-9); // 9

rest 参数

由于 JavaScript 函数允许接收任意个参数,于是我们就不得不用 arguments
来获取所有参数:

1
2
3
4
5
6
7
8
9
10
11
function foo(a, b) {
let i, rest = [];
if (arguments.length > 2) {
for (i = 2; i < arguments.length; i++) {
rest.push(arguments[i]);
}
}
console.log("a = " + a);
console.log("b = " + b);
console.log(rest);
}

为了获取除了已定义参数 ab 之外的参数,我们不得不用
arguments,并且循环要从索引2开始以便排除前两个参数,这种写法很别扭,只是为了获得额外的
rest 参数,有没有更好的方法?

ES6 标准引入了 rest 参数,上面的函数可以改写为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function foo(a, b, ...rest) {
console.log("a = " + a);
console.log("b = " + b);
console.log(rest);
}

foo(1, 2, 3, 4, 5);
// 结果:
// a = 1
// b = 2
// Array [ 3, 4, 5 ]

foo(1);
// 结果:
// a = 1
// b = undefined
// Array []

rest参数只能写在最后,前面用 ... 标识,从运行结果可知,传入的参数先绑定
ab,多余的参数以数组形式交给变量 rest,所以,不再需要arguments
我们就获取了全部参数。

如果传入的参数连正常定义的参数都没填满,也不要紧,rest
参数会接收一个空数组(注意不是 undefined)。

因为 rest 参数是 ES6 新标准,所以你需要测试一下浏览器是否支持。

小心你的 return 语句

前面我们讲到了 JavaScript 引擎有一个在行末自动添加分号的机制,这可能让你栽到
return 语句的一个大坑:

1
2
3
4
5
function foo() {
return { name: "foo" };
}

foo(); // { name: 'foo' }

如果把return语句拆成两行:

1
2
3
4
5
6
7
8
function foo() {
return;
{
name: "foo";
}
}

foo(); // undefined

要小心了,由于 JavaScript
引擎在行末自动添加分号的机制,上面的代码实际上变成了:

1
2
3
4
5
6
function foo() {
return; // 自动添加了分号,相当于 return undefined;
{
name: "foo";
}// 这行语句已经没法执行到了
}

所以正确的多行写法是:

1
2
3
4
5
function foo() {
return { // 这里不会自动加分号,因为{表示语句尚未结束
name: "foo",
};
}

方法

在一个对象中绑定函数,称为这个对象的方法。

这里我们给xiaoming绑定一个函数,写个age()方法,返回 xiaoming 的年龄:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
"use strict";

const xiaoming = {
name: "小明",
birth: 1990,
age: function () {
let y = new Date().getFullYear();
return y - this.birth;
},
age_2() { // ES6 简写
return new Date().getFullYear() - this.birth;
},
};

xiaoming.age(); // ✅ 安全,this 是 xiaoming

this 关键字

this 是函数被调用时,JS
引擎动态绑定的一个特殊变量。它不是编译期确定的,而是运行期根据调用方式决定。

绑定规则(按调用方式)

调用方式 this 指向 安全度
obj.method() obj ⭐⭐⭐⭐⭐
new Constructor() 新创建的实例 ⭐⭐⭐⭐⭐
fn.call(obj) / fn.apply(obj) 指定的 obj ⭐⭐⭐⭐⭐
fn.bind(obj) 返回的新函数 绑定的 obj ⭐⭐⭐⭐⭐
箭头函数 () => {} 定义时外层作用域的 this ⭐⭐⭐⭐⭐
普通函数 fn()(严格模式) undefined ⭐⭐⭐(会报错)
普通函数 fn()(非严格) window / global ⭐(静默失败,最坑)
嵌套普通函数内部 同普通函数调用,不继承外层 ⭐(容易踩坑)

this 不看定义在哪,只看怎么被调用。

口诀 含义
对象点方法,this 是该对象 obj.fn()thisobj
直接调函数,thisundefined fn() → 严格模式下报错
箭头函数没自己的 this,抄外层的 内层用箭头,安全继承
不确定就 bind,显式绑定最稳 fn.bind(obj) 永不出错

常见陷阱

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
"use strict";

class Person {
constructor(birth) {
this.birth = birth;
}

age() {
return new Date().getFullYear() - this.birth;
}
}

const p = new Person(1990);

// 陷阱 1:把方法当回调传
setTimeout(p.age, 1000); // ❌ this 丢失,报错

// 陷阱 2:先赋值再调用
const fn = p.age;
fn(); // ❌ this 是 undefined

// 陷阱 3:内层普通函数
const obj = {
birth: 1990,
age: function () {
function inner() {
return this.birth; // ❌ this 不是 obj
}
return inner();
},
};

正确写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
"use strict";

class Person {
constructor(birth) {
this.birth = birth;
}

// 方案 1:类方法(推荐)
age() {
return new Date().getFullYear() - this.birth;
}

// 方案 2:箭头函数属性(自动绑定 this,但每个实例一份)
getAge = () => {
return new Date().getFullYear() - this.birth;
};
}

const p = new Person(1990);

// 安全调用
p.age(); // ✅ 35

// 回调场景
setTimeout(() => p.age(), 1000); // ✅ 包装一层箭头函数
setTimeout(p.age.bind(p), 1000); // ✅ 显式绑定 this

// 先赋值再调用
const fn = p.age.bind(p);
fn(); // ✅ 35

bind、call、apply

这三个方法都是用来控制函数执行时的 this 指向的,区别只在于调用方式和参数传递。

方法 作用 是否立即执行 参数形式
call 指定 this,调用函数 ✅ 立即 逐个传入 fn.call(obj, a, b)
apply 指定 this,调用函数 ✅ 立即 数组传入 fn.apply(obj, [a, b])
bind 指定 this绑定函数 ❌ 不立即,返回新函数 逐个传入 fn.bind(obj, a, b)

对普通函数调用,我们通常把this绑定为null

三者对比示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function add(a, b) {
return this.base + a + b;
}

const obj = { base: 100 };

// call:立即执行,逐个传参
add.call(obj, 1, 2); // 103

// apply:立即执行,数组传参
add.apply(obj, [1, 2]); // 103

// bind:不执行,返回新函数
const boundAdd = add.bind(obj, 1); // 还可以预设参数(柯里化)
boundAdd(2); // 103

使用场景速查

场景 用哪个
临时借用其他对象的方法 call
参数已经在数组里,且是老代码 apply
需要把方法当回调传,但怕 this 丢失 bind
需要预设部分参数(柯里化) bind
现代开发,参数在数组里 ... 展开语法
现代开发,回调保 this 箭头函数 () =>

现代推荐

1
2
3
4
5
6
7
8
9
10
11
12
// 老代码
var self = this;
setTimeout(function () {
self.doSomething();
}, 100);

// 现代代码(最常用)
setTimeout(() => this.doSomething(), 100);

// 或方法需要复用时
const handler = this.doSomething.bind(this);
button.addEventListener("click", handler);

高阶函数

高阶函数英文叫Higher-order function。那么什么是高阶函数?

JavaScript
的函数其实都指向某个变量。既然变量可以指向函数,函数的参数能接收变量,那么一个函数就可以接收另一个函数作为参数,这种函数就称之为高阶函数。

一个最简单的高阶函数:

1
2
3
function add(x, y, f) {
return f(x) + f(y);
}

当我们调用 add(-5, 6, Math.abs) 时,参数 xyf 分别接收 -56
和函数 Math.abs,根据函数定义,我们可以推导计算过程为:

1
2
3
4
5
x = -5;
y = 6;
f = Math.abs;
f(x) + f(y) ==> Math.abs(-5) + Math.abs(6) ==> 11;
return 11;

编写高阶函数,就是让函数的参数能够接收别的函数。

闭包

一句话理解:闭包 = 函数 + 函数创建时能访问到的外部变量

函数"记住"了它被创建时的环境,即使后来离开了那个环境,依然能访问那些变量。

1
2
3
4
5
6
7
8
9
10
11
12
function test() {
let count = 0;
return function () {
count++; // ✅ 可以修改!
return count;
};
}

const counter = test();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3

JS 闭包里的变量是活的引用,可以读也可以改。

闭包的日常使用场景

1. 事件监听 / 回调(最常见)

1
2
3
4
5
6
7
8
9
10
function setupButton() {
let clickCount = 0;
const btn = document.getElementById("myBtn");

btn.addEventListener("click", function () {
clickCount++; // 闭包:回调记住了 clickCount
console.log(`点击了 ${clickCount} 次`);
});
}
// setupButton 执行完了,但 clickCount 还在,被回调函数"抓"住了

2. 模块化 / 私有变量(模拟 Java 的 private)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 类似 Java 的类,但有私有变量
function createUser(name) {
let _password = "secret"; // "私有"变量,外部直接访问不到

return {
getName: () => name,
checkPassword: (pwd) => pwd === _password,
setPassword: (newPwd) => {
_password = newPwd;
},
};
}

const user = createUser("张三");
console.log(user._password); // undefined,访问不到
console.log(user.checkPassword("secret")); // true

这其实就是 JS 模块模式(Module Pattern),现代 ES6 模块化背后也是类似的思路。

3. 函数工厂 / 柯里化

1
2
3
4
5
6
7
8
9
10
11
12
// 生成乘法器,类似 Java 的工厂方法
function makeMultiplier(factor) {
return function (number) {
return number * factor; // 记住了 factor
};
}

const double = makeMultiplier(2);
const triple = makeMultiplier(3);

console.log(double(5)); // 10
console.log(triple(5)); // 15

4. 异步操作保持上下文

1
2
3
4
5
6
7
8
9
10
11
function fetchUserData(userId) {
const startTime = Date.now();

fetch(`/api/user/${userId}`)
.then((res) => res.json())
.then((data) => {
// 闭包:这里还能访问到 startTime 和 userId
const duration = Date.now() - startTime;
console.log(`用户 ${userId} 数据加载完成,耗时 ${duration}ms`);
});
}

箭头函数

ES6标准新增了一种新的函数:箭头函数(Arrow Function)。

为什么叫箭头函数?因为它的定义用的就是一个箭头:

1
((x) => x * x);

上面的箭头函数相当于:

1
2
3
function (x) {
return x * x;
}

箭头函数相当于匿名函数,并且简化了函数定义。箭头函数有两种格式,一种像上面的,只包含一个表达式,连{ ... }return都省略掉了。还有一种可以包含多条语句,这时候就不能省略{ ... }return

1
2
3
4
5
6
7
((x) => {
if (x > 0) {
return x * x;
} else {
return -x * x;
}
});

如果参数不是一个,就需要用括号()括起来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 两个参数:
(x, y) => x * x + y * y

// 无参数:
() => 3.14

// 可变参数:
(x, y, ...rest) => {
let i, sum = x + y;
for (i=0; i<rest.length; i++) {
sum += rest[i];
}
return sum;
}

如果要返回一个对象,就要注意,如果是单表达式,这么写的话会报错:

1
2
3
4
// SyntaxError:
((x) => {
foo: x;
});

因为和函数体的{ ... }有语法冲突,所以要改为:

1
2
// ok:
((x) => ({ foo: x }));

箭头函数修复了 this 的指向,this 总是指向词法作用域,也就是外层调用者obj

1
2
3
4
5
6
7
8
9
let obj = {
birth: 1990,
getAge: function () {
let b = this.birth; // 1990
let fn = () => new Date().getFullYear() - this.birth; // this指向obj对象
return fn();
},
};
obj.getAge(); // 25

由于 this 在箭头函数中已经按照词法作用域绑定了,所以,用 call()
或者apply()调用箭头函数时,无法对this进行绑定,即传入的第一个参数被忽略。

默认参数

在此之前的版本(ES5 及更早),要实现默认参数通常需要手动判断:

1
2
3
4
function foo(a, b) {
a = a || "default";
b = b !== undefined ? b : 0;
}

改成 ES6 默认参数写法:

1
2
3
function foo(a = "default", b = 0) {
// ...
}

标签函数(Tagged Template)

标签函数是模板字符串的高级用法,日常开发中直接手写不多,但通过库间接使用非常普遍。

标签函数长什么样

1
2
3
4
5
6
7
8
9
10
11
12
function tag(strings, ...values) {
// strings: ['你好,', ''] (按 ${} 分割的字符串数组)
// values: ['小明'] (插值表达式的值)

console.log(strings); // [ '你好,', '' ]
console.log(values); // [ '小明' ]

return "自定义结果";
}

const name = "小明";
const result = tag`你好,${name}`; // "自定义结果"

核心机制:标签函数接收两个参数

  1. strings:字符串片段数组(包含原始字符串 raw 属性)
  2. ...values:所有 ${} 插值的值

生成器(Generator)

生成器(Generator)是 ES6 引入的一种特殊的可暂停函数,通过 function*
声明,配合 yield 关键字使用。它的核心特点是:执行到 yield
时暂停,下次调用时从暂停处继续

基本用法

1
2
3
4
5
6
7
8
9
10
11
12
function* gen() {
yield 1; // 第一次 next() 到这里
yield 2; // 第二次 next() 从这里继续
return 3;
}

const g = gen(); // 返回一个生成器对象,不会立即执行
console.log(g.next()); // { value: 1, done: false }
console.log(g.next()); // { value: 2, done: false }
console.log(g.next()); // { value: 3, done: true }
console.log(g.next()); // { value: undefined, done: true }
console.log(g.next()); // { value: undefined, done: true }

关键点:

  • function* 声明生成器函数
  • yield 暂停并返回一个值
  • .next() 恢复执行,返回 { value, done }

为什么能"返回多次"?

普通函数:一次调用,一次返回,执行完就销毁
生成器:一次调用,多次产出,状态保存在生成器对象里

实际应用场景

1. 惰性求值 / 无限序列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function* fibonacci() {
let [a, b] = [0, 1];
while (true) {
yield a;
[a, b] = [b, a + b];
}
}

const fib = fibonacci();
console.log(fib.next().value); // 0
console.log(fib.next().value); // 1
console.log(fib.next().value); // 1
console.log(fib.next().value); // 2
console.log(fib.next().value); // 3
// 可以无限取下去,不会内存溢出,因为每次只计算一个值

2. 遍历复杂数据结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Tree {
constructor(value, children = []) {
this.value = value;
this.children = children;
}

*traverse() { // 深度优先遍历
yield this.value;
for (const child of this.children) {
yield* child.traverse(); // yield* 委托给子生成器
}
}
}

const tree = new Tree(1, [
new Tree(2, [new Tree(4)]),
new Tree(3),
]);

for (const value of tree.traverse()) {
console.log(value); // 1, 2, 4, 3
}

3. Redux-Saga(最知名的生产环境使用)

1
2
3
4
5
6
7
8
9
import { call, put, take } from "redux-saga/effects";

function* watchFetchUser() {
while (true) {
const action = yield take("FETCH_USER_REQUEST"); // 等待特定 action
const user = yield call(api.fetchUser, action.payload);
yield put({ type: "FETCH_USER_SUCCESS", payload: user });
}
}

Redux-Saga
用生成器实现可测试的、声明式的副作用管理,是生成器在业界的标杆应用。

日常用的多吗?

场景 使用频率 说明
手写无限序列/惰性计算 ⭐⭐ 较少 业务代码中需求不多
异步流程控制 ⭐ 几乎不用 已被 async/await 取代
遍历器协议(Symbol.iterator) ⭐⭐⭐ 偶尔 自定义可迭代对象时会用到
Redux-Saga ⭐⭐⭐⭐ 较常见 使用 Redux 的中大型项目
阅读开源库源码 ⭐⭐⭐⭐ 常见 co、redux-saga、koa(早期)等

一句话总结

生成器是 ES6 的"高级特性",日常业务代码中直接手写不多,但它是
for...of、Redux-Saga、协程等机制的底层基础。作为初学者,了解原理即可,遇到
function\* 能看懂就行,不必刻意使用。

参考

简介 - JavaScript 教程 -
廖雪峰的官方网站