Javascript中神奇的this
Javascript当中的this与其他语言是完全不同的机制,很有可能会让一些编写其他语言的工程师迷惑。
1.误以为this指向函数自身
根据this的英语语法,很容易将函数中出现的this理解为函数自身。在javascript当中函数作为一等公民,确实可以在调用的时候将属性值存储起来。但是如果使用方法不对,就会发生与实际预期不一致的情况。具体情况,请看下面代码
functionfn(num){ this.count++; } fn.count=0; for(vari=0;i<3;i++){ fn(i); } console.log(fn.count);//0
如果fn函数里面的this指向自身函数,那么count属性的属性值就应该产生变化,但实际上却是纹丝不动。对于这个问题,有些人会利用作用域来解决,比如这么写
vardata={ count:0 }; functionfn(num){ data.count++; } for(vari=0;i<3;i++){ fn(i); } console.log(data.count);//3
又或者更直接的这么写
functionfn(num){ fn.count++; } fn.count=0; for(vari=0;i<3;i++){ fn(i); } console.log(fn.count);//3
虽然这两种方式都输出了正确的结果,但是却避开了this到底绑定在哪里的问题。如果对一个事物的工作原理不清晰,就往往会产生头痛治头,脚痛治脚的问题,从而导致代码变得的丑陋,而且维护性也会变得很差。
2.this神奇的绑定规则
2.1默认绑定规则
第一种是最常见的this的绑定,看一下下面的代码
functionfn(){ console.log(window===this);//浏览器环境 } fn();//true
函数fn是直接在全局作用域下调用的,没有带其他任何修饰,这种情况下,函数调用的时候使用了this的默认绑定,指向了全局对象。
这样就清楚了第一个例子中的this指向,fn函数中的this指向了全局变量,所以this.count++相当于window.count++(浏览器环境下),当然不会对fn函数的count属性产生影响。
有一点要说明的是,上面种情况只能在非严格模式(strictmode)下才能发生,在严格模式下,会将this默认绑定为undefined。以避免全局变量的污染。
2.2隐式绑定规则
如果函数在以对象为上下文进行调用,那么this的绑定就会产生变化。this会绑定到调用这个函数的对象,查看下面代码:
varobj={ a:1, fn:function(){ console.log(this.a); } } obj.fn();//1
即使函数声明不在对象当中,this指向仍会产生变化
functionfn(){ console.log(this.a); } varobj={ a:1, fn:fn } obj.fn();//1
由此可见,this的绑定,不与函数定义的位置有关,而是与调用者和调用方式有关。
在隐式的绑定规则下,有一些特殊的地方,需要注意。
2.2.1多层对象调用this的指向
functionfn(){ console.log(this.a); } varobj={ a:1, obj2:obj2 } varobj2={ a:2, obj3:obj3 } varobj3={ a:3, fn:fn } obj.obj2.obj3.fn();//3
在多层对象引用下,this指向的是调用的函数的那个对象。
2.2.2隐式赋值可能存在丢失现象
查看下面代码
functionfn(){ console.log(this); } varobj={ fn:fn } varfun=obj.fn; fun();//window
虽然fn引用了obj.fun,但是函数的调用方式,仍是不带任何修饰的,所以this还是绑定在了window上。
还有一种情况,容易让大家忽略,那就是传参的时候,其实会进行隐式赋值。
functionfn(){ console.log(this); } functiondoFn(fn){ fn(); } varobj={ fn:fn } doFn(obj.fn);//window
隐式绑定this不是一种很推荐的方式,因为很有可能就发生丢失的情况,如果业务当中对this的绑定有要求,建议还是使用显示绑定的方式。
2.3显式绑定规则
显示绑定就是利用函数原型上的apply与call方法来对this进行绑定。用法就是把想要绑定的对象作为第一个参数传进去。
functionfn(){ console.log(this); } varobj={}; fn.call(obj);//{}
有些时候会想将函数的this绑定在某个对象上,但是不需要立即调用,这样的话,直接利用call或者apply是无法做的。
functionfn(){ console.log(this); } functionbind(fn){ fn(); } varobj={ fn:fn } bind.call(obj,fn);//window
上面这个例子,看似好像可以,但实际上是bind函数的this绑定到了obj这个对象,但是fn仍然是没有任何修饰的调用,所以fn仍然是默认的绑定方式。
functionfn(){ console.log(this); } functionbind(fn,obj){ returnfunction(){ fn.apply(obj,arguments); } } varobj={ fn:fn } varfun=bind(fn,obj); fun();//obj
这样调用,就可以将灵活多变的this,牢牢的控制住了,因为fn的调用方式为apply调用。所以,this就被绑定在传入的obj对象上,在ES5当中,函数的原型方法上多了一个bind。效果与上面的函数基本一致,具体用法限于篇幅就不多说了。
2.4new绑定
new是一个被很多人误解的一个关键字,但实际上javascript的new与传统面向对象的语言完全不同。
个人把new理解为一种特殊的函数调用,当使用new关键字来调用函数的时候,会执行下面操作,
- 创建一个全新的对象
- 将空对象的__proto__指向构造函数的prototype
- 将新对象的this绑定到调用的函数上
- 如果函数返回值为基本类型或者为this又或者不返回任何值,那么将会返回这个创建的新对象,如果返回了一个对象,那么则会返回这个对象,而不会返回创建的新对象。
functionfn(a){ this.a=a; } fn.prototype.hi=function(){ console.log('hi') } varobj=newfn(2); console.log(obj); functionfn(a){ this.a=a; return{}; } varobj=newfn(2); console.log(obj);//{}
2.5特殊的传参
null和undefined也是可以作为this的绑定对象的,但是实际上应用的是默认的绑定。
但是这种传参的实际效用是什么呢?
常见的用法是将一个数组展开,作为参数传入参数。比如
functionfn(a,b){ console.log('a:',a,'b:',b); } fn.apply(null,[1,2]);//a:1b:2
但是这种用法会有一个坑,那就是如果函数存在了this,那么就会应用默认的绑定规则,将this绑定在全局对象上,发生于预期不一致的情况。为了代码更加稳健,可以使创建一个比空对象更空的对象。
varobj=Object.create(null); console.log(obj.__proto__);//undefined varobj2={} console.log(obj2.__proto__);//Object{}
Object原型上有一个create方法,这个方法会创建一个对象,然后将对象的原型指向传入的参数,所以传入null的话,产生一个没有prototype的对象,所以会比空对象更加"空"。
所以传入这个对象,会比传入null更加安全。
varobj=Object.create(null); fn.apply(obj,[1,2]);
2.6根据作用域来决定this的绑定
在ES6当中,出现了一个新的函数类型,箭头函数。
如果使用箭头函数,那么就不会使用上面提到的四种this绑定方式,而是根据作用域来决定
比较常见的是用于事件函数和定时器的情况。
下面是比较常见的传统this写法
functionfn(){ var_this=this; setTimeout(function(){ console.log(_this.a); },100) } varobj={ a:2 } fn.call(obj);//2
如果使用箭头函数则可以这么写
functionfn(){ setTimeout(()=>{ //this来源于fn函数的作用域 console.log(this.a); },100) } varobj={ a:2 } fn.call(obj);//2
2.7事件函数当中this的绑定机制
如果是在事件函数当中,this的绑定是指向触发事件的DOM元素的,
$('body')[0].addEventListener('click',function(){ console.log(this); },false);
点击body元素之后,控制台则会显示body元素
3.小结
如果想判断一个函数的this绑定在哪里,首先是找到函数的调用位置,之后是按照规则来判断。
- 如果函数调用时没有任何修饰条件,那么在严格模式下则会绑定到undefined,非严格模式下会绑定到全局。
- 如果是用对象做上下文,来对函数进行调用,那么则会绑定到调用的这个对象上。
- 如果是用call或者apply方法来进行调用的,则会绑定到第一个传入参数上。
- 如果是使用new关键字来调用函数的,则会绑定到新创建的那个对象上.
- 如果是在事件函数内,则会绑定到触发事件的那个DOM元素上。
以上就是关于Javascript中神奇的this的相关介绍,希望对大家的学习有所帮助。