箭头函数 vs 普通函数:从“this 指向混乱”到写出真正健壮的代码

269 阅读4分钟

一个老项目,里面有个用户权限校验模块频繁报错:

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 显示欢迎弹窗。原始代码如下:

const dashboard = {
  username: '张三',
  role: 'admin',

  showWelcome: function() {
    // 延迟显示欢迎信息
    setTimeout(function() {
      alert(`欢迎回来,${this.username}!你的角色是:${this.role}`)
    }, 200)
  }
}

结果弹窗显示:

欢迎回来,undefined!你的角色是:undefined

为什么?因为 setTimeout 的回调是一个普通函数,它的 this 指向的是 window(非严格模式),而不是 dashboard


二、解决方案:用箭头函数锁定上下文

我们把回调改成箭头函数:

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. 第二层:构造函数能力

// 普通函数可以作为构造函数
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 对象

function normalFn() {
  console.log(arguments) // ✅ 类数组对象,包含所有参数
}

const arrowFn = () => {
  console.log(arguments) // ❌ ReferenceError: arguments is not defined
}

✅ 替代方案:使用 剩余参数(rest parameters)

const arrowFn = (...args) => {
  console.log(args) // ✅ 数组形式,更现代
}

4. 第四层:原型与 prototype

function Normal() {}
console.log(Normal.prototype) // ✅ 存在

const Arrow = () => {}
console.log(Arrow.prototype) // ❌ undefined

🔍 这也解释了为什么箭头函数不能用 new:没有原型链,无法实现继承。


5. 第五层:语法与适用场景

特性普通函数箭头函数
语法function fn() {}const fn = function() {}() => {}
单行返回需要 return可省略 return
适用场景构造函数、对象方法、动态 this回调函数、工具函数、固定上下文
// 箭头函数的简洁语法优势
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 需求,更轻量

五、实战避坑指南

❌ 错误用法:在对象方法中使用箭头函数

const calculator = {
  value: 0,
  add: () => {
    this.value += 1 // ❌ this 指向 window,不是 calculator
  }
}

✅ 正确做法:使用普通函数或方法简写

const calculator = {
  value: 0,
  add() { // 等价于 add: function()
    this.value += 1 // ✅ this 指向 calculator
  }
}

❌ 错误用法:试图用 call 改变箭头函数 this

const fn = () => console.log(this)
fn.call({ name: 'test' }) // 仍然输出 window

六、举一反三:三个变体场景实现思路

  1. 需要动态 this 的事件代理
    使用普通函数或 .bind(element),确保 this 指向当前触发元素。

  2. 封装带状态的函数工厂
    外层用普通函数管理实例状态,内部用箭头函数作为回调,继承外层 this。

  3. 类中使用箭头函数作为方法
    在 React 或 Vue 中,类属性箭头函数可自动绑定 this,避免手动 bind。

class MyComponent {
  handleClick = () => {
    // this 永远指向组件实例
    console.log(this.state)
  }
}

小结

箭头函数不是普通函数的“语法糖替代品”,而是为特定场景设计的上下文锁定工具

记住这个口诀:

普通函数管“身份”——this 随调用变;
箭头函数守“初心”——this 从定义来。

当你写函数时,先问自己:

  • 需要动态 this 吗?→ 用普通函数
  • 需要固定外层上下文吗?→ 用箭头函数
  • 要用 new 吗?→ 只能用普通函数