遇到的this问题的翻车事故

this对于当时刚接触js的人来说幽灵一般的存在,就是老司机也不免有大意翻车的时候,印象中很多前端面试的基础部分考察prototypethis是必不可少的。
前几天改一个bug改出this的问题,大意翻车,虽然很快就解决了,但还是警醒下自己,温习梳理以免再出错。
问题类似下面的代码块,对象外提取公共模块导入,对象内部方法和属性通过对象传参params的方式进入setTime方法执行业务逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function setTime(params){
setTimeout(function fn4() {
params.keyFn2();
},0)
}
var obja={
valname: 'xzhang',
fn1(){
return this.valname + ' did fn1';
},
fn2(){
return this.fn1()+' and did fn2';
},
fn3(){
setTime({
keyFn2: this.fn2
});
}
}
obja.fn3();

但是执行后报错,fn2方法是执行过后,但是里面却找不到fn1这个方法,看到这个bug一脸懵逼。
老司机也会翻车好像说的就是我,当时就想当然的的认为(以下是错误思考过程)fn2通过this.fn2传参进入,this也会传进去,所以this.fn1this理所应当的指向obja,所有的方法都会找到自己的对象宿主去执行(错误思考过程结束),还是因为懒,思想都不愿意往前多想一步。
遇到问题总是要解决的。
先思考后动手是我的习惯,这里报错很显然是this指向出现了问题,找到这个问题根源,也就不难发现根源在哪里、怎么去解决了。
so…
万年不变的console调式大法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function setTime(params){
setTimeout(function fn4() {
console.log(this, 'fn3-fn4');
params.keyFn2();
},0)
}
var obja={
valname: 'xzhang',
fn1(){
console.log('fn1',this);
return this.valname + ' did fn1';
},
fn2(){
console.log('fn2',this);
return this.fn1()+' and did fn2';
},
fn3(){
setTime({
keyFn2: this.fn2
});
}
}
obja.fn3();

结果如下:

果不其然fn2中的this指向出现了问题,所以找不到fn1方法很正常。错误的this指向了一个对象,既不是windows也不是obja,而是{keyFn2: f},看着好眼熟,嗯?这个对象不就是我们setTime传进去的对象参数params么?
再看看fn4里的调用方法params.keyFn2(),所以一切都很明了了:setTime里的keyFn2方法能够通过this.fn2传参进去,方法能够得到执行,但是fn2方法里面还有this,这个this的指向就有问题了,它指向的是调用这个方法的params也就是{keyFn2: f},这个对象里根本没有fn1方法!
进一步fn1就算得到执行,方法里面的依然有this的问题,所以也得进行手动指向。
如果想得到正确的结果必须明确this的指向。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function setTime(params){
setTimeout(function fn4() {
console.log(params.keyFn2(params.self));
},0)
}
var obja={
valname: 'xzhang',
fn1(s){
const self = s? s : this;
return self.valname + ' did fn1';
},
fn2(s){
const self = s? s : this;
return self.fn1(s)+' and did fn2';
},
fn3(){
setTime({
keyFn2: this.fn2,
self: this
});
}
}
obja.fn3();


大功告成,到这里bug就算修复完毕了,但是思考一个问题,我们工作就是为了码代码,修复bug的么? 这个是不是最佳的处理方式呢?

上面的代码没有提取公共模块之前是下面这么写的,一点问题都没有,this指向正常,除了通过_this纠正了es5的指向,其他并不需要操心。
但是需要使用额外的外界方法,并且方法中涉及this指向的问题就需要小心了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var objb={
valname: 'xzhang',
fn1(){
console.log('fn1',this);
return this.valname + ' did fn1';
},
fn2(){
console.log('fn2',this);
return this.fn1()+' and did fn2';
},
fn3(){
const _this = this;
setTimeout(function fn4() {
console.log("fn3", this, _this);
console.log(_this.fn2());
},0)
}
}
objb.fn3();

this指向总结

老司机就不用往下看了,自己写出来也算是一种加深印象,就像小学时候写错一个字拿两杆笔写罚抄一百遍一样,为了不再犯错。
*注:以下都为Browser环境

普通function里的this

此时this指向全局windows,凡是挂载到windows上的变量方法此时都可以通过this调用。箭头函数也是如此。

1
2
3
4
5
6
7
var glb = 'windows';
function testThis(){
console.log(this,this.glb);
}
const arrowTestThis = ()=>{
console.log(this,this.glb);
}


不管函数嵌套多少层,this返回的都是windows

new调用function里的this

此时的this指向new出来的实例。

1
2
3
4
5
6
7
8
9
10
11
function newfun(params){
if(typeof new.target == 'undefined') {
throw new Error('constructor must be called with new');
}
console.log('newfun里的this指向:',this);
this.name = params.name;
this.takebus = function(){
return this.name + ' takes bus';
}
}
var newObj = new newfun({name:'xzhang'});


可以看到打印出来的this指向了new出来的实例,这里的this就暂时可以想象成newObj

prototype里的this

Constructor的prototype属性上挂载的属性和方法都可以被实例所共享继承使用,避免了每次生成实例都需要累计占用内存的情况。
Constructor里的this和prototype的挂载方法里的this在Constructor实例化后都将指向当前的实例对象,所以挂载在Constructor里的this上的方法属性以及挂载在prototype上的属性和方法,在Constructor里和prototype上挂载的方法里都是可以通过this调用(实例都可以访问的到)。

1
2
3
4
5
6
7
8
9
function Construc(name){
this.name = name;
}
Construc.prototype.age = '秘密';
Construc.prototype.introduction = function(){
console.log('prototype里的this:',this);
console.log('prototype:', Construc.prototype);
console.log(this.name + ' age is ' + this.age);
}


在prototype里打印当前的this以及Construc.prototype
prototype1里的this指向实例,例如construc
Construc.prototype为当前的prototype对象,里面包含constructorConstruc

上面的代码块可以理解为下面的这种,只是帮助理解。

1
2
3
4
5
6
7
function Construc(name){
this.name = name;
this.age = '秘密';
this.introduction = function(){
console.log(this.name + ' age is ' + this.age);
}
}

prototype上的属性方法完全被’代理’到了当前它的Constructor上,它的所有属性和方法都是Constructor的属性和方法,Construc里的this指向实例,Construc.prototype里的this自然也指向实例。

Object里的this

这里的Object指的是字面量的,此时对象字面量里的this指向windows(对象属性中调用this)或者此object(对象方法函数中调用this)本身。

1
2
3
4
5
6
7
8
var testOBJ ={
name:'xzhang',
age:20,
testThis: this,
dosome:function(){
console.log(this);
}
}

event里的this

此时的this指向事件绑定的dom节点,但是可以通过bind改变this指向。

1
2
3
4
5
6
7
<div id="Objecthis">...</div>
<script>
var _dom = document.getElementById('Objecthis');
_dom.addEventListener('click',function(e){
console.log(this); // <div id="Objecthis">...</div>
});
</script>

html里的this

此时this指向dom本身

1
<h5 id="html里的this" onclick="console.log(this,typeof this)"><a href="#html里的this" class="headerlink" title="html里的this"></a>html里的this</h5>

我们在html里添加一个点击事件,并在点击事件中打印出this,此时我么可以看到console打印台里出现的是这个dom本身,注意这里this打印出来的不是字符串,而是一个dom对象。

如果把这点击事件提取出来封装成一个方法,点击事件时候调用这个方法,那么此时的this指向的是…windows,因为很显然上面提到了普通调用function的时候this都会指向全局

1
2
3
4
<h5 id="html里的this" onclick="dosomething();"><a href="#html里的this" class="headerlink" title="html里的this"></a>html里的this</h5>
<script>
function doSomething(){console.log(this);} //Window {postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, frames: Window, …}
</script>

原创内容,欢迎交流转载请注明出处