this是什么?
该怎样理解JS中的this?
勉强回答可以是:this有时候指的是全局作用域。有时候指的是对象本身。使用apply和call的时候this指向指定的上下文。
this有太多怪异的情况会导致程序员抓狂,很多时候很难搞清this指向,所以很多时候我们选用了self=this来防止this混乱。
《you don't know JS》一书中提到“软绑定”和“硬绑定”以及“丢失绑定”。除了这些,还有复杂引用中this的变化。本文也是在阅读该书的相关介绍后整理而成。
其实this并不神秘,主要有四个主体情形可以确认this的指向,另外再注意一些特殊情况就可以理解this了。
代码案例:
【代码-1】包含this指向的四种情况,初看会有不明白觉得重复的地方,没事。跳过,等通读全文后回过头来再看这里(建议直接跳过这段代码)。
function animal(newName){ if(newName=='pard'){ console.log(this.name); return; } this.name=newName;//this指向谁了? } var name='global animal'; var zoo={}; var zoo2={name:'zoo2 animal',getAnimal:animal}; var zoo3=zoo2.getAnimal; zoo3('pard');//'global animal' zoo2.getAnimal('monkey'); console.log(zoo2.name);//zoo2 monkey zoo2.getAnimal.call(zoo,'lion'); console.log(zoo.name);//zoo lion //第一段对比/// var forest1=new zoo2.getAnimal('horse'); console.log(zoo2.name,forest1.name);//zoo2 monkey forest1 horse //第二段对比/// zoo2.getAnimal.call(zoo,'thisCallAnimal'); console.log(zoo.name,zoo2.name); //第三段对比/// var forest2=animal.bind(zoo); forest2('panda'); console.log(zoo.name);//panda var BigForest=new forest2('tiger'); console.log(zoo.name,BigForest.name);//panda tiger
代码-1
this指向情形一:(即为默认情形,指向全局对象)
通过【代码-1】中关于情形一的截取:
function animal(newName){ console.log(this.name); } var name='global animal'; var zoo2={ name:'zoo2 animal', getAnimal:animal }; animal('pard');//'global animal' var zoo3=zoo2.getAnimal; zoo3('pard');//'global animal'
代码-2
通过【代码-2】我们看到,有两个打印。
先看直接执行animal('pard')的时候:
this.name指向了全局对象中的name,打印出'global animal',也就是说此时的this绑定在了全局对象上。为何如此?因为我们在调用animal()时,并没有做任何的特殊处理(那什么是特殊处理?比如下面几种情况将要介绍到的call,apply,bind,softBind等),所以这里符合默认情形(说白了就是备胎,其他情形都不符合才会考虑默认),this就会指向全局对象。
再看zoo3('pard'):
zoo3=zoo2.getAnimal,而zoo2.getAnimal指向的就是animal方法。也就是zoo3其实就是animal方法,zoo3('pard')和animal('pard')也就呈现出相同的结果了。所以符合默认情形,this就会指向全局对象。
this指向情形二&三之对象绑定:此时this会指向被绑定的对象
普通绑定(情形二):
对情形一中【代码-2】稍加修改
function animal(newName){ console.log(this.name); } var name='global animal'; var zoo2={ name:'zoo2 animal', getAnimal:animal }; zoo2.getAnimal('prad');//zoo2 animal var zoo3=zoo2.getAnimal; zoo3('pard');//'global animal'
代码-3
【代码-3】中zoo2完成了一次普通绑定,而zoo3在【代码-2】中已经提到了,出现在这里是为了跟此处zoo2做一个对比。
这里animal被zoo2的getAnimal属性引用了。我们在使用zoo2.getAnimal的时候,虽然执行了animal方法,但是该animal方法被调用的时候位置是在zoo2对象下的getAnimal,也就是,animal此时被调用的地方的上下文是zoo2而不是zoo2之外的全局对象。这就等于给animal绑定了上下文为zoo2,所以此时,this.name就成了zoo2.name。
这里通过引入了一个概念:方法被调用的地方的上下文可以绑定this,用这个概念我们来看作为对比的zoo3,情形一中提到zoo3=...,实际zoo3就是应用了animal,所以此时animal被调用的地方就是zoo3('..')这行代码出现的地方,这里上下文并没有明确,所以默认全局对象为上下文。这就是一开始说的“绑定丢失”(从zoo2丢失)
上述情形二一直都说的是普通绑定,那就还有非普通绑定,称为:显式绑定(明确通知想要哪个对象作为上下文!)。
显式绑定(情形三):
下面是对【代码-1】的提取
function animal(newName){ this.name=newName;//this指向谁了? } var zoo={}; var zoo2={name:'zoo2',getAnimal:animal}; zoo2.getAnimal.call(zoo,'lion'); console.log(zoo.name,zoo2.name);//lion zoo2
代码-4
call和apply可以实现对方法的继承,如:funa.call(obj)或者funa.apply(obj)都可以实现obj继承方法funa。同时,call和apply还会明确要求funa中this将指向obj,这种明确告知上下文的就是显示绑定。
来看【代码-4】:
zoo2.getAnimal.call(zoo,'lion')是让zoo继承了zoo2的getAnimal,同时传入执行参数'lion',因为该方法明确告知了this指向zoo,所以this.name=newName改变的就是zoo的name而非zoo2。通过输出结果就可以看出。
this指向情形四:this会指向new方法创造新的对象
下面是对【代码-1】的提取:
function animal(newName){ this.name=newName;//this指向谁了? } var zoo2={name:'monkey',getAnimal:animal}; var forest1=new zoo2.getAnimal('horse'); console.log(zoo2.name,forest1.name);//zoo2 monkey forest1 horse
代码-5
【代码-5】中new zoo2.getAnimal('...')实际就是new animal('...'),使用new创造了一个新的animal对象forest1,此时this指向了新的对象forest1。通过输出结果可以看出。
总结一:
至此,this的四种情况我们都了解清楚了。是否只是如此而已呢?如果一个地方四种情况同时出现,this岂不是手忙脚乱不知道到底该指向谁?其实不然。this是有自己的先后顺序的。
这个时候就是我们全面回顾【代码-1】的时候了。
注意【代码-1】中注释有“第一段对比”的代码书写方式:
var forest1=new zoo2.getAnimal('horse');
仔细看这行代码,有zoo2.getAnimal('horse')这个普通绑定的存在,也有forest1=new ....的new“构造””(加引号是因为JS没有真正意义的面向对象的概念,自然没有真正意义的构造函数)。通过输出结果我们可以看到zoo2.name没有变化,而forest1.name赋值为'horse'。
说明同时存在普通绑定和new “构造时,new“构造”会起作用,普通绑定会失效。
再看【代码-1】中注释有“第二段对比”的代码书写方式:
zoo2.getAnimal.call(zoo,'thisCallAnimal');
同样发现这里面包含了两种可以确定this指向的类型:zoo2.getAnimal为代表的普通绑定上下文类型和call(obj)为代表的显式绑定上下文类型。通过输出结果可以看出‘thisCallAnimal’被赋给了zoo而非zoo2。
说明同时存在普通绑定和显式绑定时,显式绑定会起作用,普通绑定会失效。
猜测一个结论:“new”状元--->“显式绑定”榜眼--->“普通绑定”探花--->被作为候选项的“默认绑定”可能连秀才都不是吧
本着无愧每位的寒窗苦读的学子,皇帝要求再检查一下显式绑定和new的答卷到底更优。为啥不复查“默认绑定”?因为皇帝根本懒得理这么底层的学渣。
在阅读第三段对比之前还得补充一个知识,就是一开始说的硬绑定”---bind方法。至于硬绑定的目的以及相应拓展就不在这里介绍了。贴出其实现代码给众看官看一下就知道bind实际就类似apply:
function bind(fn,obj){ return function(){fn.apply(obj,arguments)}; }
所以对比显式绑定和new就可以通过下面方法的实现:
且看【代码-1】中注释有“第三段对比”的代码书写方式:
var forest2=animal.bind(zoo); forest2('panda'); var BigForest=new forest2('tiger');
可以看到forest2是一个显式绑定,而bigFprest=new...是一个new“构造”,通过最终结果可以看出“tiger”给了bigForest而非forest2。
于是皇帝正式下发皇榜:新科状元:new!,殿试情况如下:
“new”状元--->“显式绑定”榜眼--->“普通绑定”探花 当然别忘了还有个备胎“默认绑定”。
至此,this的介绍就只缺少“软绑定”了。
软绑定介绍:
为何要有软绑定?解决啥问题了?
回答这个问题之前我们还是先简答介绍下硬绑定的作用和弊端吧。作用:硬绑定可以防止我们使用上下文绑定(情形二&情形三)时出现情形二中介绍的绑定丢失,硬绑定就是强制绑定不讲道理和原则。缺点:太生硬了,无法变通。比如,我们想通过显式绑定或者普通绑定来改变this,在使用硬绑定的情况下:sorry!no way!好不友好的情况。这时候就需要软绑定。
软绑定同样可以防止绑定丢失,但他的方法不是使用硬绑定,而是判断当前绑定有没有消失,如果消失了就回到指定的地方。翻译成人话就是:检查this的绑定,如果this绑定的是全局,说明this丢失了,此时就绑定一个指定的上下文给this,如果this不是指向全局,那就不做任何改变了。这样无论我们怎么使用call或者apply或者普通绑定,都不会被制止。
终于写完了。this终于介绍完了。终于可以休息了。介绍完了?当然没有!继续奋斗。特殊的用法需要介绍下。
情形一:
有个班级=[a,b,c,d,...],有个点名方法(calltheroll),我想给[a,b,c,d...]点一下名,需要班级继承点名方法。此时我们使用call或者apply的时候是这样用:calltheroll.apply(obj,[...]);obj哪来的?我根本不需要啊,这不是浪费我的存储么。果断改为calltheroll.apply(null,[...]);这时候后this就不知所指了,总不能绑定一个Null吧?此时this会指向全局。当然,此时如果有this的操作,自然也就污染全局了,所以还是把obj放进去吧,不过可以把obj设置为空对象。
情形二:
ES6中的箭头函数。
function(){
//scope1 this
()=>{
//scope2 this
}
}
scope2中的this指向就是scope1中的this。=>有个特殊功能,定义的方法this指向相对其外部不会改变。这在ES5中可以这么理解:
function(){
var self=this;
function(){
//self
}
}
才疏学浅,知之如此。
2017/03/14