本章将专门讨论与执行上下文创建阶段直接相关的最后一个细节——这是什么?以及它到底指向什么。理解这一点也许你已经在其他面向对象的编程语言中看到过这一点,并且你知道它指向由构造函数创建的对象。但实际上,在JavaScript中,this代表的不仅仅是创建的对象。我们来看看ECMAScript标准规范中对this的定义:“this关键字求值为当前执行上下文的ThisBinding的值。”“this关键字代表的值是当前执行上下文的ThisBinding。”MDN对this的定义:“在大多数情况下,this的值取决于函数的调用方式。”“在大多数情况下,this的值取决于函数的调用方式。”好了,如果上面两行你能看懂,那你就不用再看了,恭喜!……我不这么认为,至少光看这两行我还是不明白。我们先来看一个例子:vargetGender=function(){returnpeople1.gender;};varpeople1={gender:'female',getGender:getGender};varpeople2={gender:'male',getGender:getGender};console.log(people1.getGender());//femaleconsole.log(people2.getGender());//女什么?为什么people2会变性?这不是我想要的结果,为什么?因为getGender()返回(return)硬编码的people1.gender的关系,结果自然是'female'。所以,如果我们稍微改变一下getGender:vargetGender=function(){returnthis.gender;};这时候应该会分别得到female和male两个结果。那么回到上一点,从这个例子我们可以看出,people1和people2的getGender方法即使引用了同一个getGender函数,但是由于调用对象不同,执行结果也会不同。现在我们知道了第一个重要的点,这其实就是调用函数时发生的绑定,它指向什么完全取决于调用函数的方式。这个怎么区分呢?看了上面的例子,这是谁,还是有点模糊吧?那我们来看看不同的调用方式对这个值的影响。情况一:全局对象&调用普通函数在全局环境下,this指向全局对象,在浏览器中,就是window对象。在下面的示例中,无论是否在严格模式下,this都指的是全局对象。varx=1console.log(this.x)//1console.log(this.x===x)//trueconsole.log(this===window)//如果在全局环境中调用普通函数则为真,在非严格模式下,this也指向普通函数中的全局对象;如果它处于严格模式,则这将是未定义的。ES5加入了严格模式,让JavaScript在更严格的环境中运行。为了消除安全隐患,严格模式禁止this关键字指向全局对象。varx=1functionfn(){console.log(this);//窗口全局对象console.log(this.x);//1}fn();使用严格模式后:"usestrict"//使用严格模式varx=1functionfn(){console.log(this);//未定义的console.log(this.x);//错误“无法读取未定义的属性‘x’”,因为此时未定义}fn();Case2:作为对象方法调用,我们知道如果对象中的值是原始值(primitivetype;例如string、value、Booleanvalue),我们会把这个新创建的东西称为“属性(property)”;如果对象中的值是一个函数(function),我们就称这个新创建的东西为“方法(method)”。如果函数作为对象的方法,作为对象的方法被调用,则函数中的this指向上层对象。varx=1varobj={x:2,fn:function(){console.log(this);控制台日志(this.x);}}obj.fn()//obj.fn()打印出来;//Object{x:2,fn:function}//2vara=obj.fna()//a()的结果打印出来://窗口全局对象//1在上面的例子中,直接运行obj。fn(),调用这个函数的上层对象是obj,所以this指向obj,this.x的值为2;然后我们先把fn方法赋值给变量a,a在全局环境中运行,所以此时this指向全局对象Window,所以this.x为1。我们再看一个例子,this会指向什么如果函数被多个对象嵌套调用。varx=1varobj={x:2,y:{x:3,fn:function(){console.log(this);//对象{x:3,fn:function}console.log(this.x);//3}}}obj.y.fn();为什么结果不是2,因为在这种情况下,记住一句话:this会一直指向直接调用函数的上层对象,也就是y,上面的例子实际执行的是下面的代码。vary={x:3,fn:function(){console.log(this);//对象{x:3,fn:function}console.log(this.x);//3}}varx=1varobj={x:2,y:y}obj.y.fn();对象和函数可以嵌套。如果函数是嵌套的,这会改变吗?让我们用下面的代码来探索它。varobj={y:function(){console.log(this===obj);//真的console.log(this);//对象{y:函数}fn();函数fn(){控制台。日志(这===对象);//falseconsole.log(this);//窗口全局对象}}}obj.y();在函数y中,this指向调用它的上层对象obj,这个是没有问题的。但是在嵌套函数fn中,this并没有指向obj。嵌套函数不会从调用它的函数继承它。嵌套函数作为函数调用时,其this值在非严格模式下指向全局对象,在严格模式下指向undefined,所以上面例子的实际实现代码如下。函数fn(){console.log(this===obj);//falseconsole.log(this);//窗口全局对象}varobj={y:function(){console.log(this===obj);//真的console.log(this);//对象{y:函数}fn();}}obj.y();情况三:我们可以使用new关键字作为构造函数调用,通过构造函数生成一个实例对象。此时,this指向新对象。varx=1;functionFn(){ this.x=2;控制台日志(这个);//Fn{x:2}}varobj=newFn();//objandFn(..)thisinthecallisboundconsole.log(obj.x)//2当使用new调用Fn(..)时,会构造一个新的对象,它(obj)会是在调用中绑定到Fn(..)this。另外值得一提的是,如果构造函数返回非引用类型(string、number、boolean、null、undefined),this仍然指向实例化的new对象。varx=1functionFn(){this.x=2return{x:3}}vara=newFn()console.log(a.x)//3因为Fn()返回(return)一个对象(引用类型),这将指向返回对象。如果返回的是非引用类型的值怎么办?varx=1functionFn(){this.x=2return3}vara=newFn()console.log(a.x)//2情况4:call和apply方法调用如果要改变this的方向,您可以使用调用或应用方法。它们的第一个参数是指定函数运行时的this点。如果第一个参数不传(参数为空)或null或undefined,则默认this指向全局对象(非严格模式)或undefined(严格模式)。varx=1;varobj={x:2}functionfn(){console.log(this);console.log(this.x);}fn.call(obj)//Object{x:2}//2fn.apply(obj)//Object{x:2}//2fn.call()//窗口globalobject//1fn.apply(null)//Windowglobalobject//1fn.call(undefined)//Windowglobalobject//1使用call和apply时,如果传给this的对象不是对象,JavaScript会使用相关的构造函数将其转化为对象,比如传递number类型,就会进行newNumber()操作,比如传递string类型,就会进行newString()操作。如果传递的是boolean类型,则执行newBoolean()操作。functionfn(){console.log(Object.prototype.toString.call(this))}fn.call('love')//[对象字符串]fn.apply(1)//[对象编号]fn.call(true)//[objectBoolean]call和apply的区别在于call的第二个及后面的参数是一个参数列表,而apply的第二个参数是一个数组。参数列表和参数数组都将作为函数的参数执行。varx=1varobj={x:2}functionSum(y,z){console.log(this.x+y+z)}Sum.call(obj,3,4)//9Sum.apply(obj,[3,4])//9case5:bind方法调用f.bind(someObject)会创建一个与f具有相同函数体和作用域的函数,但是在这个新函数中,新函数的this会是永久点传递给绑定的第一个参数,无论函数如何调用。varx=1varobj1={x:2};varobj2={x:3};函数fn(){console.log(this);console.log(this.x);};vara=fn.bind(obj1);varb=a.bind(obj2);fn();//窗口全局对象//1a();//对象{x:2}//2b();//Object{x:2}//2a.call(obj2);//Object{x:2}//2在上面的例子中,虽然我们试图将它重新赋值给functiona,它仍然指向Object,即使使用call或apply方法也无法改变它永久指向bind传入的第一个参数的事实。情况六:箭头函数中的this点值得一提的是,箭头函数是从ES6开始加入的。首先我们看一下MDN上对箭头函数的描述。箭头函数表达式的语法比函数表达式更短,并且不绑定自己的this、arguments、super或new.target。箭头函数总是匿名的。这些函数表达式最适合非方法函数,它们不能用作构造函数。这里已经明确说明,箭头函数是没有自己的this绑定的。箭头函数中使用的this实际上是直接包含它的函数或函数表达式中的this。在前面情况2的函数嵌套函数的例子中,嵌套的函数不会继承上层函数的this。如果使用箭头函数,会发生什么?varobj={y:function(){console.log(this===obj);//真的console.log(this);//对象{y:函数}varfn=()=>{console.log(this===obj);//真的console.log(this);//对象{y:函数}}fn();}}obj.y()不同于普通函数,箭头函数中的this指向obj,因为它继承了上一层函数的this,可以理解为箭头函数修正了this的指向。所以箭头函数的this在调用的时候并没有确定,而是在定义的时候所在的对象就是它的this。也就是说,箭头函数的this取决于外层是否有函数。如果有,则外层函数的this就是内层箭头函数的this。如果没有,这就是窗口。varobj={y:()=>{console.log(this===obj);//falseconsole.log(this);//窗口全局对象varfn=()=>{console.log(this===obj);//falseconsole.log(this);//窗口全局对象}fn();}}obj.y()在上面的例子中,虽然有两个箭头函数,但是this其实依赖于最外层的箭头函数,由于obj是对象而不是函数,所以this指向的是Window全局对象.和bind一样,箭头函数也很“顽固”,我们不能通过call和apply来改变this的方向,也就是忽略传入的第一个参数。varx=1varobj={x:2}vara=()=>{console.log(this.x)console.log(this)}a.call(obj)//1//窗口全局对象a.apply(obj)//1//对Window全局对象的文字说明太多,可能会有点干,看下面的流程图。我认为这个数字很好地概括了这一点。图中的流程仅针对单条规则。小结本文介绍了this指向的几种情况,不同的运行环境和调用方式都会对此产生影响。一般来说,函数this的指向取决于当前调用该函数的对象,即执行时的对象。本节需要掌握:this指向全局对象的情况;严格模式和非严格模式的区别;this指向函数作为对象方法调用的几种情况;this用作构造函数时,与是否返回的区别;使用call和apply改变调用函数的对象;this在bind创建的函数中的要点;this在箭头函数中的要点。
