Appearance
函数二义性
一、什么是函数二义性?
在 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捕获了外层greet的this - 即使
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 | 兼容旧环境 | 兼容性好 | 代码冗余 |
| 箭头函数 | 回调函数场景 | 简洁、自动继承 | 不能用于对象方法 |
| 类字段 | 类组件方法 | 自动绑定实例 | 需要类语法支持 |