一个老项目,里面有个用户权限校验模块频繁报错:
js
const user = {
name: 'Alice',
age: 28,
isAdmin: true,
delayCheck: function() {
setTimeout(function() {
console.log(`${this.name} 是管理员吗?${this.isAdmin}`)
}, 100)
}
}
user.delayCheck() // 输出:undefined 是管理员吗?undefined
新人一脸懵:"this
怎么丢了?"------这正是 箭头函数与普通函数最核心的区别。
一、问题场景:异步回调中的 this 陷阱
我们有个后台管理系统,需要在页面加载后延迟 200ms 显示欢迎弹窗。原始代码如下:
js
const dashboard = {
username: '张三',
role: 'admin',
showWelcome: function() {
// 延迟显示欢迎信息
setTimeout(function() {
alert(`欢迎回来,${this.username}!你的角色是:${this.role}`)
}, 200)
}
}
结果弹窗显示:
javascript
欢迎回来,undefined!你的角色是:undefined
为什么?因为 setTimeout
的回调是一个普通函数 ,它的 this
指向的是 window
(非严格模式),而不是 dashboard
。
二、解决方案:用箭头函数锁定上下文
我们把回调改成箭头函数:
js
const dashboard = {
username: '张三',
role: 'admin',
showWelcome: function() {
setTimeout(() => {
// 🔍 箭头函数没有自己的 this
// 它会沿作用域链向上找,找到 showWelcome 的 this
alert(`欢迎回来,${this.username}!你的角色是:${this.role}`)
}, 200)
}
}
dashboard.showWelcome() // ✅ 正确输出
现在 this
正确指向 dashboard
,问题解决。
三、原理剖析:从表面到引擎底层的五层差异
1. 第一层:this 指向机制(最核心区别)
普通函数 | 箭头函数 | |
---|---|---|
this 绑定时机 | 运行时动态绑定 | 定义时词法绑定 |
this 来源 | 调用方式决定(window、obj、new 等) | 外层作用域的 this |
能否被改变 | 可用 call /apply /bind 修改 |
❌ 不可修改 |
🔍 关键理解:
- 普通函数的
this
是"谁调用我,我就指向谁" - 箭头函数的
this
是"我在哪定义,就继承谁的 this"
我们来画一张 this 查找路径图:
graph TB
A["[箭头函数]"] --> B["无 own this"]
B --> C["向上查找"]
C --> D["[外层函数作用域]"]
D --> E["找到 this → 继承"]
D --> F["[全局作用域]"]
F --> G["window"]
style A fill:#9f9,stroke:#333,stroke-width:2px
style E fill:#f99,stroke:#333,stroke-width:2px
style G fill:#f99,stroke:#333,stroke-width:2px
而普通函数是:
graph TB
A["[函数执行]"] --> B["根据调用方式"]
B --> C1["obj.fn()"]
B --> C2["fn()"]
B --> C3["new Fn()"]
B --> C4["fn.call(ctx)"]
C1 --> D1["this = obj"]
C2 --> D2["this = window/global"]
C3 --> D3["this = 新对象"]
C4 --> D4["this = ctx"]
style A fill:#9f9,stroke:#333,stroke-width:2px
style D1 fill:#cce5ff
style D2 fill:#ffd699
style D3 fill:#d4edda
style D4 fill:#f8d7da
2. 第二层:构造函数能力
js
// 普通函数可以作为构造函数
function Person(name) {
this.name = name
}
const p1 = new Person('Bob') // ✅
// 箭头函数不能作为构造函数
const Animal = (type) => {
this.type = type
}
const a1 = new Animal('cat') // ❌ TypeError: is not a constructor
📌 原因:箭头函数没有 [[Construct]]
内部方法,V8 引擎在解析时就禁止了 new
操作。
3. 第三层:arguments 对象
js
function normalFn() {
console.log(arguments) // ✅ 类数组对象,包含所有参数
}
const arrowFn = () => {
console.log(arguments) // ❌ ReferenceError: arguments is not defined
}
✅ 替代方案:使用 剩余参数(rest parameters)
js
const arrowFn = (...args) => {
console.log(args) // ✅ 数组形式,更现代
}
4. 第四层:原型与 prototype
js
function Normal() {}
console.log(Normal.prototype) // ✅ 存在
const Arrow = () => {}
console.log(Arrow.prototype) // ❌ undefined
🔍 这也解释了为什么箭头函数不能用 new
:没有原型链,无法实现继承。
5. 第五层:语法与适用场景
特性 | 普通函数 | 箭头函数 |
---|---|---|
语法 | function fn() {} 或 const fn = function() {} |
() => {} |
单行返回 | 需要 return |
可省略 return |
适用场景 | 构造函数、对象方法、动态 this | 回调函数、工具函数、固定上下文 |
js
// 箭头函数的简洁语法优势
const numbers = [1, 2, 3]
const squares = numbers.map(n => n * n) // ✅ 简洁
// 对比:
const squares = numbers.map(function(n) { return n * n })
四、对比主流使用场景
场景 | 推荐用法 | 原因 |
---|---|---|
对象方法 | ❌ 箭头函数 | 会丢失对象自身 this |
事件监听器 | ✅ 箭头函数 | 避免手动 bind |
数组遍历回调 | ✅ 箭头函数 | 语法简洁,无需关心 this |
构造函数 | ✅ 普通函数 | 箭头函数不支持 new |
模块工具函数 | ✅ 箭头函数 | 无 this 需求,更轻量 |
五、实战避坑指南
❌ 错误用法:在对象方法中使用箭头函数
js
const calculator = {
value: 0,
add: () => {
this.value += 1 // ❌ this 指向 window,不是 calculator
}
}
✅ 正确做法:使用普通函数或方法简写
js
const calculator = {
value: 0,
add() { // 等价于 add: function()
this.value += 1 // ✅ this 指向 calculator
}
}
❌ 错误用法:试图用 call 改变箭头函数 this
js
const fn = () => console.log(this)
fn.call({ name: 'test' }) // 仍然输出 window
六、举一反三:三个变体场景实现思路
-
需要动态 this 的事件代理
使用普通函数或
.bind(element)
,确保this
指向当前触发元素。 -
封装带状态的函数工厂
外层用普通函数管理实例状态,内部用箭头函数作为回调,继承外层 this。
-
类中使用箭头函数作为方法
在 React 或 Vue 中,类属性箭头函数可自动绑定 this,避免手动 bind。
js
class MyComponent {
handleClick = () => {
// this 永远指向组件实例
console.log(this.state)
}
}
小结
箭头函数不是普通函数的"语法糖替代品",而是为特定场景设计的上下文锁定工具。
记住这个口诀:
普通函数管"身份"------this 随调用变;
箭头函数守"初心"------this 从定义来。
当你写函数时,先问自己:
- 需要动态
this
吗?→ 用普通函数 - 需要固定外层上下文吗?→ 用箭头函数
- 要用
new
吗?→ 只能用普通函数