一
简而答之:不使用。
众所周知,js 中的 this 对象在不同作用域下指代不同的对象实例,并且在以下 4 种场景中经常会“不知所向”:
简而言之,在所有从 js 主线程之外的异步线程回调过来的函数内,this 经常会丢失。
为什么会丢失?
是因为调用代码没有将 this 对象传递过去。在 js 中所有函数或方法,其类型都是 Function,这个对象的三个方法call、apply、bind的第一个参数均是 thisArg。大多数情况下,这个 thisArg 不需要手动传递,js 解析器会根据执行上下文环境自动补全。但正由于自动补全,thisArg 有时候可能取了一个不恰当的值。
举个例子:
const USER_TOKEN_NAME = 'user-token';
function testThis(){
setTimeout(function() {
console.log("USER_TOKEN_NAME",this.USER_TOKEN_NAME)//undefined
}, 0);
}
export default {
testThis,
USER_TOKEN_NAME,
...
};
在上面的代码中,this.USER_TOKEN_NAME 取不到正常的值,因为”this“并不是模块的默认输出对象。(注:在export default对象中,才能访问this.USER_TOKEN_NAME)
如何想让代码正常工作,有两种改写方法:
1)使用箭头函数
function testThis(){
setTimeout(()=>{
console.log("USER_TOKEN_NAME",this.USER_TOKEN_NAME)//undefined
}, 0);
}
2)使用 bind 方法绑定 this 对象
function testThis(){
setTimeout(()=>{
console.log("USER_TOKEN_NAME",this.USER_TOKEN_NAME)//user-token
}.bind(this), 0);
}
在上面代码中,bind方法会将this与Function捆绑在一个闭包中并返回这个闭包。
但是,这样使用 this 必须小心翼翼,稍有不慎就可能出现难以查找的异常。所以最好的对象模块开发规范是,不使用 this 关键字。
这里指对象模块,默认导出是一个全局的对象这种场景;如果是导出 Class,在类方法中访问类属性,是必使用 this 关键字的。
二
在对象模块中,所有模块内使用的变量、常量请直接在文件顶部定义,如下所示:
hasPushedStream; //是否已经开始推流
所有函数,无论最终导出、还是不导出,都直接以最简单的 function 方式声明,如下所示:
// 开启视频头
function startPreview() {
util.trydo(_ => {
// videoIsOpen1 = !videoIsOpen1;
if (window.Chinook) {
window.Chinook.startPreview(getCoreId(), '');
videoIsOpen = true;
}
}, this);
}
export default {
startPreview,
...
}
在上面代码中,startPreview作为导出的对象模块的外露方法,可以这样链式调用:
api.cef.startPreview()
在startPreview函数内部,访问 videoIsOpen 不需要 this 关键字。即使setTimeout回调函数不是箭头函数,只要没有使用 this 关键字,videoIsOpen变量仍然可以找到。在 js 作用域链中,如果当前作用域找不到标识符,会自动向上一级作用域查找。前提是没有使用作用域限定符 this。
如果在export default的对象中,添加了一个 videoIsOpen 默认输出,如下所示:
export default {
videoIsOpen,
startPreview,
...
}
这个时候,在 startPreview 函数内使用videoIsOpen、还是this.videoIsOpen,都可以正常访问。但访问的却不是同一个变量。如果不清楚这个差别,可能程序会出现让人抓狂的 bug,但就是不知道错误在哪里。
对象模块维护自身状态,原则上它不需要、也不能向外暴露自己的私有变量。如果外界模块需要这个对象的一个只读属性,怎么办?
可以这样:
export default {
get videoIsOpen() {
return videoIsOpen;
},
...
}
虽然 getter 名称与变量名相同,但不会混淆。在外界使用 api.cef.videoIsOpen 这样的方式访问只读属性,在模块文件内部,直接使用 videoIsOpen 读写变量。访问的是同一个标识符。
Q/A
在回调中如何保证 this 对象的正确指向?
使用bind方法,在上面已经使用过了。附一个trydo函数示例:
/*
* example:
* `Util.trydo((a,b)=>{
* console.log('trydo func',a,b)
* },this,1,2)`
* 如果要在f内使用this对象,第二个参数一定要传递
*/
function trydo(f, thisArg, ...args) {
try {
f.bind(thisArg, ...args).apply(thisArg, args);
} catch (error) {
console.log('try error', error);
return false;
}
return true;
}
以上,不知道我讲明白没有。
2019/04/10 石桥码农