JavaScript 函数
基本上所有的高级语言都支持函数,JavaScript 也不例外。JavaScript
的函数不但是“头等公民”,而且可以像变量一样使用,具有非常强大的抽象能力。
借助抽象,我们才能不关心底层的具体计算过程,而直接在更高的层次上思考问题。写计算机程序也是一样,函数就是最基本的一种代码抽象的方式。
函数(function)的定义
在 JavaScript 中,定义函数的方式如下:
1 | function abs(x) { |
上述 abs() 函数的定义如下:
function指出这是一个函数定义;abs是函数的名称;(x)括号内列出函数的参数,多个参数以,分隔;{ ... }之间的代码是函数体,可以包含若干语句,甚至可以没有任何语句。
请注意,函数体内部的语句在执行时,一旦执行到 return
时,函数就执行完毕,并将结果返回。因此,函数内部通过条件判断和循环可以实现非常复杂的逻辑。
如果没有 return 语句,函数执行完毕后也会返回结果,只是结果为 undefined。
由于 JavaScript 的函数也是一个对象,上述定义的 abs()
函数实际上是一个函数对象,而函数名 abs 可以视为指向该函数的变量。
因此,第二种定义函数的方式如下:
1 | let abs = function (x) { |
在这种方式下,function (x) { ... }是一个匿名函数,它没有函数名。但是,这个匿名函数赋值给了变量
abs,所以,通过变量 abs 就可以调用该函数。
上述两种定义_完全等价_,注意第二种方式按照完整语法需要在函数体末尾加一个;,表示赋值语句结束。
函数的特点:
- 包含 0 或多个参数
- 存在或无返回值
调用函数
调用函数时,按顺序传入参数即可:
1 | abs(10); // 返回 10 |
由于 JavaScript
允许传入任意个参数而不影响调用,因此传入的参数比定义的参数多也没有问题,虽然函数内部并不需要这些参数:
1 | abs(10, "blablabla"); // 返回 10 |
传入的参数比定义的少也没有问题:
1 | abs(); // 返回 NaN |
此时abs(x)函数的参数 x 将收到undefined,计算结果为NaN。
要避免收到undefined,可以对参数进行检查:
1 | function abs(x) { |
arguments
JavaScript还有一个免费赠送的关键字
arguments,它只在函数内部起作用,并且永远指向当前函数的调用者传入的所有参数。arguments
类似Array 但它不是一个 Array。
利用
arguments,你可以获得调用者传入的所有参数。也就是说,即使函数不定义任何参数,还是可以拿到参数的值:
1 | function abs() { |
rest 参数
由于 JavaScript 函数允许接收任意个参数,于是我们就不得不用 arguments
来获取所有参数:
1 | function foo(a, b) { |
为了获取除了已定义参数 a、b 之外的参数,我们不得不用
arguments,并且循环要从索引2开始以便排除前两个参数,这种写法很别扭,只是为了获得额外的
rest 参数,有没有更好的方法?
ES6 标准引入了 rest 参数,上面的函数可以改写为:
1 | function foo(a, b, ...rest) { |
rest参数只能写在最后,前面用 ... 标识,从运行结果可知,传入的参数先绑定
a、b,多余的参数以数组形式交给变量 rest,所以,不再需要arguments
我们就获取了全部参数。
如果传入的参数连正常定义的参数都没填满,也不要紧,rest
参数会接收一个空数组(注意不是 undefined)。
因为 rest 参数是 ES6 新标准,所以你需要测试一下浏览器是否支持。
小心你的 return 语句
前面我们讲到了 JavaScript 引擎有一个在行末自动添加分号的机制,这可能让你栽到
return 语句的一个大坑:
1 | function foo() { |
如果把return语句拆成两行:
1 | function foo() { |
要小心了,由于 JavaScript
引擎在行末自动添加分号的机制,上面的代码实际上变成了:
1 | function foo() { |
所以正确的多行写法是:
1 | function foo() { |
方法
在一个对象中绑定函数,称为这个对象的方法。
这里我们给xiaoming绑定一个函数,写个age()方法,返回 xiaoming 的年龄:
1 | ; |
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() → this 是 obj |
直接调函数,this 是 undefined |
fn() → 严格模式下报错 |
箭头函数没自己的 this,抄外层的 |
内层用箭头,安全继承 |
不确定就 bind,显式绑定最稳 |
fn.bind(obj) 永不出错 |
常见陷阱
1 | ; |
正确写法
1 | ; |
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 | function add(a, b) { |
使用场景速查
| 场景 | 用哪个 |
|---|---|
| 临时借用其他对象的方法 | call |
| 参数已经在数组里,且是老代码 | apply |
需要把方法当回调传,但怕 this 丢失 |
bind |
| 需要预设部分参数(柯里化) | bind |
| 现代开发,参数在数组里 | ... 展开语法 |
| 现代开发,回调保 this | 箭头函数 () => |
现代推荐
1 | // 老代码 |
高阶函数
高阶函数英文叫Higher-order function。那么什么是高阶函数?
JavaScript
的函数其实都指向某个变量。既然变量可以指向函数,函数的参数能接收变量,那么一个函数就可以接收另一个函数作为参数,这种函数就称之为高阶函数。
一个最简单的高阶函数:
1 | function add(x, y, f) { |
当我们调用 add(-5, 6, Math.abs) 时,参数 x,y 和 f 分别接收 -5,6
和函数 Math.abs,根据函数定义,我们可以推导计算过程为:
1 | x = -5; |
编写高阶函数,就是让函数的参数能够接收别的函数。
闭包
一句话理解:闭包 = 函数 + 函数创建时能访问到的外部变量
函数"记住"了它被创建时的环境,即使后来离开了那个环境,依然能访问那些变量。
1 | function test() { |
JS 闭包里的变量是活的引用,可以读也可以改。
闭包的日常使用场景
1. 事件监听 / 回调(最常见)
1 | function setupButton() { |
2. 模块化 / 私有变量(模拟 Java 的 private)
1 | // 类似 Java 的类,但有私有变量 |
这其实就是 JS 模块模式(Module Pattern),现代 ES6 模块化背后也是类似的思路。
3. 函数工厂 / 柯里化
1 | // 生成乘法器,类似 Java 的工厂方法 |
4. 异步操作保持上下文
1 | function fetchUserData(userId) { |
箭头函数
ES6标准新增了一种新的函数:箭头函数(Arrow Function)。
为什么叫箭头函数?因为它的定义用的就是一个箭头:
1 | ((x) => x * x); |
上面的箭头函数相当于:
1 | function (x) { |
箭头函数相当于匿名函数,并且简化了函数定义。箭头函数有两种格式,一种像上面的,只包含一个表达式,连{ ... }和return都省略掉了。还有一种可以包含多条语句,这时候就不能省略{ ... }和return:
1 | ((x) => { |
如果参数不是一个,就需要用括号()括起来:
1 | // 两个参数: |
如果要返回一个对象,就要注意,如果是单表达式,这么写的话会报错:
1 | // SyntaxError: |
因为和函数体的{ ... }有语法冲突,所以要改为:
1 | // ok: |
箭头函数修复了 this 的指向,this 总是指向词法作用域,也就是外层调用者obj:
1 | let obj = { |
由于 this 在箭头函数中已经按照词法作用域绑定了,所以,用 call()
或者apply()调用箭头函数时,无法对this进行绑定,即传入的第一个参数被忽略。
默认参数
在此之前的版本(ES5 及更早),要实现默认参数通常需要手动判断:
1 | function foo(a, b) { |
改成 ES6 默认参数写法:
1 | function foo(a = "default", b = 0) { |
标签函数(Tagged Template)
标签函数是模板字符串的高级用法,日常开发中直接手写不多,但通过库间接使用非常普遍。
标签函数长什么样
1 | function tag(strings, ...values) { |
核心机制:标签函数接收两个参数:
strings:字符串片段数组(包含原始字符串raw属性)...values:所有${}插值的值
生成器(Generator)
生成器(Generator)是 ES6 引入的一种特殊的可暂停函数,通过 function*
声明,配合 yield 关键字使用。它的核心特点是:执行到 yield
时暂停,下次调用时从暂停处继续。
基本用法
1 | function* gen() { |
关键点:
function*声明生成器函数yield暂停并返回一个值.next()恢复执行,返回{ value, done }
为什么能"返回多次"?
普通函数:一次调用,一次返回,执行完就销毁
生成器:一次调用,多次产出,状态保存在生成器对象里
实际应用场景
1. 惰性求值 / 无限序列
1 | function* fibonacci() { |
2. 遍历复杂数据结构
1 | class Tree { |
3. Redux-Saga(最知名的生产环境使用)
1 | import { call, put, take } from "redux-saga/effects"; |
Redux-Saga
用生成器实现可测试的、声明式的副作用管理,是生成器在业界的标杆应用。
日常用的多吗?
| 场景 | 使用频率 | 说明 |
|---|---|---|
| 手写无限序列/惰性计算 | ⭐⭐ 较少 | 业务代码中需求不多 |
| 异步流程控制 | ⭐ 几乎不用 | 已被 async/await 取代 |
| 遍历器协议(Symbol.iterator) | ⭐⭐⭐ 偶尔 | 自定义可迭代对象时会用到 |
| Redux-Saga | ⭐⭐⭐⭐ 较常见 | 使用 Redux 的中大型项目 |
| 阅读开源库源码 | ⭐⭐⭐⭐ 常见 | co、redux-saga、koa(早期)等 |
一句话总结
生成器是 ES6 的"高级特性",日常业务代码中直接手写不多,但它是
for...of、Redux-Saga、协程等机制的底层基础。作为初学者,了解原理即可,遇到
function\*能看懂就行,不必刻意使用。
参考
简介 - JavaScript 教程 -
廖雪峰的官方网站