JavaScript中this详解
都说JavaScript是一种很灵活的语言,这其实也可以说它是一个混乱的语言。它把函数式编程和面向对象编程糅合一起,再加上动态语言特性,简直强大无比(其实是不能和C++比的,^_^)。
这里的主题是this,不扯远了。this本身原本很简单,总是指向类的当前实例,this不能赋值。这前提是说this不能脱离类/对象来说,也就是说this是面向对象语言里常见的一个关键字。说的极端点,如果你编写的JS采用函数式写法,而不是面向对象式,你所有的代码里this会少很多,甚至没有。记住这一点,当你使用this时,你应该是在使用对象/类方式开发,否则this只是函数调用时的副作用。
JS里的this
在function内部被创建
指向调用时所在函数所绑定的对象(拗口)
this不能被赋值,但可以被call/apply 改变
以前用this时经常担心,不踏实,你不知道它到底指向谁?这里把它所有用到的地方列出
this和构造器
this和对象
this和函数
全局环境的this
this和DOM/事件
this可以被call/apply改变
ES5中新增的bind和this
ES6箭头函数(arrowfunction)和this
1.this和构造器
this本身就是类定义时构造器里需要用到的,和构造器在一起再自然不过。
/** *页签 * *@classTab *@paramnav{string}页签标题的class *@paramcontent{string}页面内容的class * */ functionTab(nav,content){ this.nav=nav this.content=content } Tab.prototype.getNav=function(){ returnthis.nav; }; Tab.prototype.setNav=function(nav){ this.nav=nav; }; Tab.prototype.add=function(){ };
按照JavaScript的习惯,this应该挂属性/字段,方法都应该放在原型上。
2.this和对象
JS中的对象不用类也可以创建,有人可能奇怪,类是对象的模板,对象都是从模板里copy出来的,没有类怎么创建对象?JS的确可以,并且你完全可以写上万行功能代码而不用写一个类。话说OOP里说的是面向对象编程,也没说面向类编程,是吧^_^。
vartab={ nav:'', content:'', getNav:function(){ returnthis.nav; }, setNav:function(n){ this.nav=n; } }
3.this和函数
首先,this和独立的函数放在一起是没有意义的,前面也提到过this应该是和面向对象相关的。纯粹的函数只是一个低级别的抽象,封装和复用。如下
functionshowMsg(){ alert(this.message) } showMsg()//undefined
定义showMsg,然后以函数方式调用,this.message是undefined。因此坚决杜绝在纯函数内使用this,但有时候会这么写,调用方式使用call/apply
functionshowMsg(){ alert(this.message) } varm1={ message:'输入的电话号码不正确' } varm2={ message:'输入的身份证号不正确' } showMsg.call(m1)//'输入的电话号码不正确' showMsg.call(m2)//'输入的身份证号不正确'
用这种方式可以节省一些代码量,比如当两个类/对象有一共相似的方法时,不必写两份,只要定义一个,然后将其绑定在各自的原型和对象上。这时候其实你还是在使用对象或类(方式1/2),只是间接使用罢了。
4.全局环境的this
前面提到this是“指向调用时所在函数所绑定的对象”,这句话拗口但绝对正确,没有一个多余的字。全局环境中有不同的宿主对象,浏览器环境中是window,node环境中是global。这里重点说下浏览器环境中的this。
浏览器环境中非函数内this指向window
alert(window===this)//true
因此你会看很很多开源JSlib这么写
(function(){
//...
})(this);
或这样写
(function(){
//...
}).call(this);
比如underscore和requirejs,大意是把全局变量window传入匿名函数内缓存起来,避免直接访问。至于为啥要缓存,这跟JS作用域链有关系,读取越外层的标识符性能会越差。请自行查阅相关知识,再说就扯远了。
浏览器中比较坑人,非函数内直接使用var声明的变量默认为全局变量,且默认挂在window上作为属性。
varandy='刘德华' alert(andy===window.andy)//true alert(andy===this.andy)//true alert(window.andy===this.andy)//true
因为这个特性,有些笔试题如
varx=10; functionfunc(){ alert(this.x) } varobj={ x:20, fn:function(){ alert(this.x) } } varfn=obj.fn func()//10 fn()//10
没错,最终输出的都是全局的10。永远记住这一点:判断this指向谁,看执行时而非定义时,只要函数(function)没有绑定在对象上调用,它的this就是window。
5.this和DOM/事件
W3C把DOM实现成了各种节点,节点嵌套一起形成DOMtree。节点有不同类型,如文本节点,元素节点等10多种。元素节点又分成了很多,对写HTML的人来说便是很熟悉的标签(Tag),如div,ul,label等。看W3C的API文档,会发现它完全是按照面向对象方式实现的各种API,有interface,extends等。如
看到了吧,这是用Java写的,既然是用面向对象方式实现的API,一定有类/对象(废话^_^),有类/对象,则一定有this(别忘了这篇文章的中心主题)。所有的HTMLtag类命名如HTMLXXXElement,如
HTMLDivElement
HTMLLabelElement
HTMLInputElement
...
前面说过this是指向当前类的实例对象,对于这些tag类来说,不看其源码也知它们的很多方法内部用到的this是指向自己的。有了这个结论,写HTML和JS时,this就清晰了很多。
示例A
<!--this指向div--> <divonclick="alert(this)"></div>
示例B
<divid="nav"></div> <script> nav.onclick=function(){ alert(this)//指向div#nav } </script>
示例C
$('#nav').on('click',function(){ alert(this)//指向nav })
以上三个示例可以看到,在给元素节点添加事件的时候,其响应函数(handler)执行时的this都指向Element节点自身。jQuery也保持了和标准一致,但却让人迷惑,按“this指向调用时所在函数所绑定的对象”这个定义,jQuery事件handler里的this,应该指向jQuery对象,而非DOM节点。因此你会发现在用jQuery时,经常需要把事件handler里的element在用$包裹下变成jQuery对象后再去操作。比如
$('#nav').on('click',function(){ var$el=$(this)//再次转为jQuery对象,如果this直接为jQuery对象更好 $el.attr('data-x',x) $el.attr('data-x',x) })
有人可能有如下的疑问
<divid="nav"onclick="getId()">ddd</div> <script> functiongetId(){ alert(this.id) } </script>
点击div后,为什么id是undefined,不说是指向的当前元素div吗?如果记住了前面提到的一句话,就很清楚为啥是undefined,把这句话再贴出来。
判断this指向谁,看执行时而非定义时,只要函数(function)没有绑定在对象上调用,它的this就是window
这里函数getId调用时没有绑定在任何对象上,可以理解成这种结构
div.onclick=function(){ getId() }
getId所处匿名函数里的this是div,但getId自身内的this则不是了。当然ES5严格模式下还是有个坑。
6.this可以被call/apply改变
call/apply是函数调用的另外两种方式,两者的第一个参数都可以改变函数的上下文this。call/apply是JS里动态语言特性的表征。动态语言通俗的定义
程序在运行时可以改变其结构,新的函数可以被引进,已有的函数可以被删除,即程序在运行时可以发生结构上的变化
通常有以下几点特征表示它为动态语言
动态的数据类型
动态的函数执行
动态的方法重写
动态语言多从世界第二门语言LISP发展而来,如死去的SmallTalk/VB,目前还活着的Perl/Python,以及还流行的Ruby/JavaScript。JS里动态数据类型的体现便是弱类型,执行的时候才去分析标识符的类型。函数动态执行体现为eval,call/aply。方法重写则体现在原型重写。不扯远,这里重点说下call/apply对this的影响。
varm1={ message:'ThisisA' } varm2={ message:'ThisisB' } functionshowMsg(){ alert(this.message) } showMsg()//undefined showMsg.call(m1)//'ThisisA' showMsg.call(m2)//'ThisisB'
可以看到单独调用showMsg返回的是undefined,只有将它绑定到具有message属性的对象上执行时才有意义。发挥想象力延伸下,如果把一些通用函数写好,可以任意绑定在多个类的原型上,这样动态的给类添加了一些方法,还节省了代码。这是一种强大的功能,也是动态语言的强表现力的体现。
经常会听到转向Ruby或Python的人提到“编程的乐趣”,这种乐趣是源自动态语言更接近人的思维(而不是机器思维),更符合业务流程而不是项目实现流程。同样一个功能,动态语言可以用更小的代码量来实现。动态语言对程序员生产力的提高,是其大行其道的主要原因。
性能方面,动态语言没有太大的优势,但动态语言的理念是:优化人的时间而不是机器的时间。提高开发者的生产力,宁肯牺牲部分的程序性能或者购买更高配置的硬件。随着IT业的不断发展和摩尔定律的作用,硬件相对于人件一直在贬值,这个理念便有了合理的现实基础。
JS里的call/apply在任何一个流行的lib里都会用到,但几乎就是两个作用
配合写类工具实现OOP,如mootools,ClassJS,class.js,
修复DOM事件里的this,如jQuery,events.js
关于call和apply复用:利用apply和arguments复用方法
关于call和apply的性能问题参考:冗余换性能-从Backbone的triggerEvents说开了去
7.ES5中新增的bind和this
上面6里提到call/apply在JS里体现动态语言特性及动态语言的流行原因,其在JS用途如此广泛。ES5发布时将其采纳,提了一个更高级的方法bind。
varmodal={ message:'ThisisA' } functionshowMsg(){ alert(this.message) } varotherShowMsg=showMsg.bind(modal) otherShowMsg()//'ThisisA'
因为是ES5才加的,低版本的IE不支持,可以修复下Function.prototype。bind只是call/apply的高级版,其它没什么特殊的。
8.ES6箭头函数(arrowfunction)和this
ES6在今年的6月18日正式发布(恰京东店庆日同一天,^_^),它带来的另一种类型的函数-箭头函数。箭头函数的一个重要特征就是颠覆了上面的一句话,再贴一次
判断this指向谁,看执行时而非定义时,只要函数(function)没有绑定在对象上调用,它的this就是window
是的,前面一直用这句话来判断this的指向,在箭头函数里前面半句就失效了。箭头函数的特征就是,定义在哪,this就指向那。即箭头函数定义在一个对象里,那箭头函数里的this就指向该对象。如下
varbook={ author:'JohnResig', init:function(){ document.onclick=ev=>{ alert(this.author);//这里的this不是document了 } } }; book.init()
对象book里有一个属性author,有一个init方法,给document添加了一个点击事件,如果是传统的函数,我们知道this指向应该是document,但箭头函数会指向当前对象book。
箭头函数让JS回归自然和简单,函数定义在哪它this就指向哪,定义在对象里它指向该对象,定义在类的原型上,指向该类的实例,这样更容易理解。
总结:
函数的上下文this是JS里不太好理解的,在于JS函数自身有多种用途。目的是实现各种语言范型(面向对象,函数式,动态)。this本质是和面向对象联系的,和写类,对象关联一起的,和“函数式”没有关系的。如果你采用过程式函数式开发,完全不会用到一个this。但在浏览器端开发时却无可避免的会用到this,这是因为浏览器对象模型(DOM)本身采用面向对象方式开发,Tag实现为一个个的类,类的方法自然会引用类的其它方法,引用方式必然是用this。当你给DOM对象添加事件时,回调函数里引用该对象就只能用this了。
明白了么?
相信看完全文以后,this不再是坑~