前几天在优化代码的时候遇到了一个很奇怪的问题,当使用 Function
作为 Vue 组件的 Prop 时传入一个对象内的函数(如 this.$api.user.get
时,这个函数将会丢失 this
,换句话来说是函数内的 this
由原来的对象 user
变成了 Vue 实例 (vm),就会造成函数无法调用原本自身对象的属性或函数。经过查阅一些资料后发现,这个问题也出现在 setTimeout
中,并且有解决方案。
在 setTimeout 中
如下写法会造成 sayHi 最终打印出来的值是 Hello, undefined!
let user = {
firstName: "John",
sayHi() {
alert(`Hello, ${this.firstName}!`);
}
};
setTimeout(user.sayHi, 1000); // Hello, undefined!
如果尝试在控制台中打印 this
会发现 this
指向的是 window
(在 Vue 中指向的是 $vm)
而 firstName
在 window
中是 undefined
,所以打印的结果是 Hello, undefined!
解决方法1:使用匿名函数包装
当采用下面的写法时,可以正确打印出 Hello, John!
let user = {
firstName: "John",
sayHi() {
alert(`Hello, ${this.firstName}!`);
}
};
setTimeout(() => user.sayHi(), 1000); // Hello, John!
但这样的写法也存在问题,比如下面这个例子。
let user = {
firstName: "John",
sayHi() {
alert(`Hello, ${this.firstName}!`);
}
};
setTimeout(() => user.sayHi(), 1000);
// ...the value of user changes within 1 second
user = {
sayHi() { alert("Another user in setTimeout!"); }
};
// Another user in setTimeout!
如果在 setTimeout 的一秒内,user.sayHi 函数发生了改变,则上面打印的最终结果是 Another user in setTimeout!
下面的解决方法就不会出现这个问题。
解决方法2: bind
Javascript 提供内置的方法 bind
用于固定 this
这是它的基础语法
// more complex syntax will come a little later
let boundFunc = func.bind(context);
比如下面这个例子,我们为 func
手动指定 this
,让他作为一个普通函数可以调用对象内的函数和方法。
let user = {
firstName: "John"
};
function func() {
alert(this.firstName);
}
let funcUser = func.bind(user);
funcUser(); // John
再来看看刚才的例子
let user = {
firstName: "John",
sayHi() {
alert(`Hello, ${this.firstName}!`);
}
};
let sayHi = user.sayHi.bind(user); // (*)
// can run it without an object
sayHi(); // Hello, John!
setTimeout(sayHi, 1000); // Hello, John!
// even if the value of user changes within 1 second
// sayHi uses the pre-bound value which is reference to the old user object
user = {
sayHi() { alert("Another user in setTimeout!"); }
};
使用 bind
函数后,就算1秒内改变了 user
对象,我们也能得到按顺序打印的结果。
在 Vue 组件中
为了复现这个问题,我们构造一个组件 Test
<template>
<p>
{{ func() }}
{{ obj }}
</p>
</template>
<script>
export default {
name: 'Test',
props: {
func: Function,
obj: Object,
},
created() {}
}
</script>
然后在另一个组件中调用它
<template>
<test :obj="obj" :func="func"/>
</template>
<script>
import Test from '@/views/other/Test'
class myClass {
name = ''
myFunc = this._myFunc
constructor(name) {
this.name = name
}
_myFunc() {
console.log(this)
console.log(this.name)
return this.name
}
}
const obj = new myClass('0xJacky')
export default {
name: 'About',
components: {Test},
data() {
return {
obj: obj,
func: obj.myFunc
}
}
}
</script>
在控制台中会出现 _myFunc()
的打印及错误,可以很清楚的看到 this
不再是 obj
而是 $vm
,并且因为 $vm
里没有 name
这个属性而出现了错误。
在页面中可以看到传入 obj
渲染的数据情况,直接传入 Object 时,this
不发生改变。
下面我们稍微用 bind 改造一下代码
在 myFunc = this._myFunc
后面加上 .bind(this)
<template>
<test :obj="obj" :func="func"/>
</template>
<script>
import Test from '@/views/other/Test'
class myClass {
name = ''
myFunc = this._myFunc.bind(this)
constructor(name) {
this.name = name
}
_myFunc() {
console.log(this)
console.log(this.name)
return this.name
}
}
const obj = new myClass('0xJacky')
export default {
name: 'About',
components: {Test},
data() {
return {
obj: obj,
func: obj.myFunc
}
}
}
</script>
然后再回到刚才的页面
控制台打印的 this
就是 myClass
页面也能渲染出正确的数据了
参考资料
Function binding: https://javascript.info/bind
即使是setTimeout(fn, 0)也一样
setTimeout那个问题和js事件循环有关,setTimeout是宏任务,其回调是在其它微任务执行完成后才执行的,你原来代码真实的执行顺序应该是下面这个
let user = {
firstName: “John”,
sayHi() {
alert(`Hello, ${this.firstName}!`);
}
};
user = {
sayHi() { alert(“Another user in setTimeout!”); }
};
user.sayHi();
call 跟 apply 也可以哦
@CSJerry: 好的哥哥