丢失 this 问题
今天在 Vue 项目开发的时候,在 mount 生命周期内用到了定时器,定时器函数内获取了this
中的一个方法,结果用控制台一打印,出来的是undefined
???
再打印一边this
,就发现了问题所在,控制台输出的居然是window
对象??
这是为啥?我翻阅古籍,终于找到了问题所在。
我们先来看一个例子
let user = {
firstName: 'John',
sayHi() {
alert(`Hello, ${this.firstName}!`)
},
}
setTimeout(user.sayHi, 1000) // Hello, undefined!
正如我们所看到的,输出没有像 this.firstName
那样显示 “John”,而显示了 undefined
!
这是因为 setTimeout
获取到了函数 user.sayHi
,但它和对象分离开了。最后一行可以被重写为:
let f = user.sayHi
setTimeout(f, 1000) // 丢失了 user 上下文
浏览器中的 setTimeout
方法有些特殊:它为函数调用设定了 this=window
(对于 Node.js,this
则会变为计时器(timer)对象,但在这儿并不重要)。所以对于 this.firstName
,它其实试图获取的是 window.firstName
,这个变量并不存在。在其他类似的情况下,通常 this
会变为 undefined
。
知道了问题所在,那么解决办法呢?
解决方案 1:包装器
let user = {
firstName: 'John',
sayHi() {
alert(`Hello, ${this.firstName}!`)
},
}
setTimeout(function () {
user.sayHi() // Hello, John!
}, 1000)
现在它可以正常工作了,因为它从外部词法环境中获取到了 user
,就可以正常地调用方法了。
看起来不错,但是我们的代码结构中出现了一个小漏洞。
如果在 setTimeout
触发之前(有一秒的延迟!)user
的值改变了怎么办?那么,突然间,它将调用错误的对象!
let user = {
firstName: 'John',
sayHi() {
alert(`Hello, ${this.firstName}!`)
},
}
setTimeout(() => user.sayHi(), 1000)
// ……user 的值在不到 1 秒的时间内发生了改变
user = {
sayHi() {
alert('Another user in setTimeout!')
},
}
// Another user in setTimeout!
下一个解决方案保证了这样的事情不会发生。
解决方案 2:bind(推荐)
函数提供了一个内建方法 bind,它可以绑定 this
。
基本的语法是:
// 稍后将会有更复杂的语法
let boundFunc = func.bind(context)
func.bind(context)
的结果是一个特殊的类似于函数的“外来对象(exotic object)”,它可以像函数一样被调用,并且透明地(transparently)将调用传递给 func
并设定 this=context
。
换句话说,boundFunc
调用就像绑定了 this
的 func
。
举个例子,这里的 funcUser
将调用传递给了 func
同时 this=user
:
let user = {
firstName: 'John',
}
function func() {
alert(this.firstName)
}
let funcUser = func.bind(user)
funcUser() // John
这里的 func.bind(user)
作为 func
的“绑定的(bound)变体”,绑定了 this=user
。
所有的参数(arguments)都被“原样”传递给了初始的 func
,例如:
let user = {
firstName: 'John',
}
function func(phrase) {
alert(phrase + ', ' + this.firstName)
}
// 将 this 绑定到 user
let funcUser = func.bind(user)
funcUser('Hello') // Hello, John(参数 "Hello" 被传递,并且 this=user)
现在我们来尝试一个对象方法:
let user = {
firstName: 'John',
sayHi() {
alert(`Hello, ${this.firstName}!`)
},
}
let sayHi = user.sayHi.bind(user) // (*)
// 可以在没有对象(译注:与对象分离)的情况下运行它
sayHi() // Hello, John!
setTimeout(sayHi, 1000) // Hello, John!
// 即使 user 的值在不到 1 秒内发生了改变
// sayHi 还是会使用预先绑定(pre-bound)的值,该值是对旧的 user 对象的引用
user = {
sayHi() {
alert('Another user in setTimeout!')
},
}
我们取了方法 user.sayHi
并将其绑定到 user
。sayHi
是一个“绑定后(bound)”的方法,它可以被单独调用,也可以被传递给 setTimeout
—— 都没关系,函数上下文都会是正确的。
这里我们能够看到参数(arguments)都被“原样”传递了,只是 this
被 bind
绑定了:
let user = {
firstName: 'John',
say(phrase) {
alert(`${phrase}, ${this.firstName}!`)
},
}
let say = user.say.bind(user)
say('Hello') // Hello, John!(参数 "Hello" 被传递给了 say)
say('Bye') // Bye, John!(参数 "Bye" 被传递给了 say)
如果一个对象有很多方法,并且我们都打算将它们都传递出去,那么我们可以在一个循环中完成所有方法的绑定:
for (let key in user) {
if (typeof user[key] == 'function') {
user[key] = user[key].bind(user)
}
}
JavaScript 库还提供了方便批量绑定的函数,例如 lodash 中的 _.bindAll(object, methodNames)。