先记住这个答案
普通函数的 this 是调用时才决定的,写成 obj.method() 调用,点号前面的 obj 才是 this。setTimeout(obj.method, 1000) 只是把函数本身交给了定时器,obj 没有跟着传过去。一秒后定时器单独调用这个函数,this 就不再是 obj 了:浏览器里是 window,Node.js 里是定时器对象,严格模式下自己单独调用则是 undefined。修法有两种:setTimeout(obj.method.bind(obj)) 把 this 固定住,或者 setTimeout(() => obj.method()) 在回调里重新按方法调用。
this看调用时的写法,不看函数定义在哪个对象上- 传
obj.method只传了函数,obj没跟过去 - 不只是
setTimeout,解构、数组回调、事件回调都会丢 - 修法:
bind、箭头函数包一层、类字段箭头函数
传出去的只是函数
下面用一个 run 函数模拟「把回调交给别人,之后再调用」,这正是 setTimeout、事件系统对回调做的事。
'use strict'
const user = {
name: 'Ada',
greet() {
return this ? this.name : 'this 是 undefined'
}
}
const run = (callback) => console.log(callback()) // 像定时器一样单独调用回调
run(user.greet) // 只传了函数
run(user.greet.bind(user)) // this 固定成 user
run(() => user.greet()) // 回调里重新写出 user.greet()查看输出与解释
this 是 undefined
Ada
Adauser.greet 这个表达式取到的就是一个函数,和 const fn = user.greet 拿到的一模一样。函数本身不记得自己是从哪个对象上取下来的。run 里调用 callback() 时前面没有点号,严格模式下 this 就是 undefined。
bind(user) 返回一个新函数,不管以后怎么调用,this 都是 user。箭头函数的写法则是把「方法调用」这个动作本身包进回调:回调执行时,重新执行了一次 user.greet(),点号前面又有了 user。
三种修法怎么选
bind 和箭头函数都能解决问题,区别在细节上。
bind、箭头包装和类字段bind在调用bind那一刻就固定了对象和预置参数,得到一个稳定的函数引用,适合需要保存下来、以后解绑的场景,比如addEventListener和removeEventListener要用同一个引用。 箭头函数包一层,是在回调真正执行时才去读user这个变量,读到的是那时候的对象,也方便顺手多传几个参数,比如() => user.greet('hi')。它每写一次就是一个新函数,同样不能拿来解绑。 在类里还有第三种写法:把方法写成类字段箭头函数,greet = () => { ... }。每个实例创建时都会生成一个this固定为该实例的函数,传到哪里都不会丢。React 类组件处理onClick常这么写。代价是函数挂在每个实例上而不是原型上,实例多了会多占内存,子类也不能用super.greet()调用它。
不只是 setTimeout
只要把方法「取下来」再交出去,都会遇到同样的问题。比如 arr.forEach(obj.handle),forEach 调用回调时没有点号前的对象;const { greet } = user 解构出来再调用;把 this.handleClick 直接交给事件监听。解决思路都一样:要么 bind,要么包一层箭头函数,forEach 还可以用第二个参数 thisArg 指定 this。
容易答错的地方
- 以为是异步导致的
- 很多人说「因为
setTimeout是异步的,所以this丢了」。其实同步代码里const fn = obj.method; fn()一样会丢,真正的原因是调用时前面没有了obj.,和执行早晚无关。 - 以为定时器回调里
this一定是undefined - 这取决于调用它的一方。浏览器的
setTimeout会主动把window作为this传给回调,所以即使在严格模式的代码里,this也是window,此时this.name读到的是window.name,常常是个空字符串,不报错却拿错数据,更难发现。Node.js 里this则是返回的那个Timeout对象。 - 在
render里每次都bind - 在 React 的
render里写onClick={this.handle.bind(this)},每次渲染都会生成新函数,子组件用memo做的浅比较会失效。应该在构造函数里bind一次,或者改用类字段箭头函数。
面试官还会怎么问?
对箭头函数用 bind 能改变 this 吗?
不能。箭头函数没有自己的 this,用的是定义位置外层的 this,bind、call、apply 传的 this 都会被忽略,只有参数能传进去。
setTimeout 能不能直接传参数给回调?
可以。从第三个参数开始,会原样传给回调,比如 setTimeout(fn, 1000, a, b)。但它只能传参数,不能指定 this,所以解决不了 this 丢失的问题。
事件监听里是该用 bind 还是用 event.currentTarget?
看你需要什么。addEventListener 调用普通函数时,this 本来就是绑定的元素,此时 bind 会把它覆盖掉。如果回调里既要用组件实例又要用元素,就 bind 实例或用箭头函数,元素改从 event.currentTarget 拿。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。