Skip to content

函数二义性

一、什么是函数二义性?

在 JavaScript 中,函数二义性指的是同一个函数在不同调用方式下,this 指向不同对象的现象。这是 JavaScript 灵活但容易出错的特性之一。

核心问题

函数内部的 this 不是在函数定义时确定的,而是在函数调用时根据调用方式动态确定的。这导致同一个函数在不同场景下,this 可能指向完全不同的对象。

示例演示

js
const person = {
  name: '张三',
  greet: function () {
    console.log('你好,我是' + this.name)
  }
}

person.greet()

const greetFn = person.greet
greetFn()

const btn = document.getElementById('btn')
btn.addEventListener('click', person.greet)

常见的 this 指向规则

调用方式this 指向示例
普通函数调用window(严格模式下是 undefined)func()
方法调用调用该方法的对象obj.method()
构造函数调用新创建的实例对象new Func()
call/apply/bind 调用指定的第一个参数func.call(obj)
箭头函数外层作用域的 this() => {}

二、消除二义性的方案

方案一:使用 call / apply / bind

这三个方法可以显式指定函数调用时的 this 指向。

call 方法

立即调用函数,并指定 this 和参数列表。

js
function greet(greeting, punctuation) {
  console.log(greeting + ', ' + this.name + punctuation)
}

const person = { name: '张三' }

greet.call(person, '你好', '!')

apply 方法

call 类似,但参数以数组形式传递。

js
greet.apply(person, ['你好', '!'])

bind 方法

返回一个新函数,this 被永久绑定,不会再次改变。

js
const boundGreet = greet.bind(person)
boundGreet('你好', '!')

const btn = document.getElementById('btn')
btn.addEventListener('click', boundGreet)

三者对比:

方法是否立即执行参数形式this 是否可变
call逐个传递
apply数组传递
bind否,返回新函数逐个传递否(永久绑定)

方案二:使用闭包保存 this

在嵌套函数或回调中,通过外部变量保存 this 的引用。

js
const person = {
  name: '张三',
  friends: ['李四', '王五'],
  showFriends: function () {
    const self = this

    this.friends.forEach(function (friend) {
      console.log(self.name + ' 的朋友是 ' + friend)
    })
  }
}

person.showFriends()

优点: 兼容性好,适用于所有 JavaScript 环境。

缺点: 需要额外声明变量,代码稍显冗余。


方案三:使用箭头函数

箭头函数没有自己的 this,它会捕获外层作用域的 this

js
const person = {
  name: '张三',
  friends: ['李四', '王五'],
  showFriends: function () {
    this.friends.forEach(friend => {
      console.log(this.name + ' 的朋友是 ' + friend)
    })
  }
}

person.showFriends()

方案四:使用类字段语法(Class Fields)

在类中使用箭头函数作为类字段,自动绑定实例。

js
class Counter {
  count = 0

  increment = () => {
    this.count++
    console.log(this.count)
  }
}

const counter = new Counter()
const btn = document.getElementById('btn')
btn.addEventListener('click', counter.increment)

方案五:在构造函数中 bind

在构造函数中预先绑定方法。

js
class Counter {
  constructor() {
    this.count = 0
    this.increment = this.increment.bind(this)
  }

  increment() {
    this.count++
    console.log(this.count)
  }
}

三、箭头函数能消除二义性吗?

答案:能,但有前提条件

箭头函数能够消除二义性,但前提是外层作用域的 this 是确定的

箭头函数的 this 特性

js
const person = {
  name: '张三',
  greet: function () {
    const arrowGreet = () => {
      console.log('你好,我是' + this.name)
    }
    return arrowGreet
  }
}

const fn = person.greet()
fn()

解析:

  • greet 是普通函数,作为 person 的方法调用,this 指向 person
  • 箭头函数 arrowGreet 捕获了外层 greetthis
  • 即使 fn 在全局调用,this 仍然指向 person

箭头函数失效的情况

如果外层本身就是全局作用域或普通函数调用,箭头函数的 this 仍然会有问题:

js
const person = {
  name: '张三',
  greet: () => {
    console.log('你好,我是' + this.name)
  }
}

person.greet()

原因: 箭头函数定义在对象字面量中,外层作用域是全局,所以 this 指向 window

正确使用箭头函数的场景

js
const person = {
  name: '张三',
  greet: function () {
    setTimeout(() => {
      console.log('你好,我是' + this.name)
    }, 1000)
  }
}

person.greet()

四、箭头函数的作用总结

1. 消除 this 二义性

箭头函数没有自己的 this,继承外层作用域的 this,避免了回调函数中 this 丢失的问题。

2. 简洁的语法

js
const numbers = [1, 2, 3, 4, 5]

const doubled = numbers.map(n => n * 2)

const sum = numbers.reduce((a, b) => a + b, 0)

3. 没有 arguments 对象

箭头函数没有自己的 arguments,可以使用剩余参数代替:

js
const sum = (...args) => args.reduce((a, b) => a + b, 0)
console.log(sum(1, 2, 3, 4))

4. 不能作为构造函数

箭头函数不能使用 new 调用,没有 prototype 属性:

js
const Person = name => {
  this.name = name
}

new Person('张三')

5. 适用场景总结

场景是否适用箭头函数
数组方法回调(map, filter, forEach 等)✅ 推荐
事件处理(需要访问外层 this)✅ 推荐
定时器回调✅ 推荐
对象方法❌ 不推荐
构造函数❌ 不能使用
需要 arguments 的场景❌ 不推荐
需要 call/apply/bind 改变 this❌ 无效

五、最佳实践

推荐做法

js
class Component {
  constructor() {
    this.state = { count: 0 }
  }

  handleClick = () => {
    this.setState({ count: this.state.count + 1 })
  }

  render() {
    return `<button onclick="${this.handleClick}">点击</button>`
  }
}

避免的做法

js
const obj = {
  name: '张三',
  greet: () => {
    console.log(this.name)
  }
}

六、总结对比表

方案适用场景优点缺点
call/apply临时改变 this灵活每次调用都要写
bind需要永久绑定一次绑定永久生效需要额外调用
闭包保存 this兼容旧环境兼容性好代码冗余
箭头函数回调函数场景简洁、自动继承不能用于对象方法
类字段类组件方法自动绑定实例需要类语法支持

基于 VitePress 的本地知识库