相信Javascript中的this会让很多同学在工作学习中感到困惑,笔者也是如此。在看了各种资料和实际应用后,做了下面的整理,主要内容包括长期以来对this和this的绑定规则的误解,箭头函数,以及在实际工作场景中遇到的问题。希望能帮到迷茫的你。一、两个误解1、指向自身this的第一个误解是,很容易理解为this指向函数本身。实际上,这个方向在函数定义阶段是无法确定的。只有在执行函数时才能确定。对谁来说,实际this的最终点是调用它的对象。下面的例子声明了一个函数foo(),当执行foo.count=0时,给函数对象foo增加一个属性count。但是函数foo的内部代码this.count中的this并没有指向函数对象。for循环中foo(i)的对象是window,相当于window.foo(i)。因此,函数foo中的this指向了window。functionfoo(num){this.count++;//记录foo被调用的次数}foo.count=0;window.count=0;for(leti=0;i<10;i++){if(i>5){foo(i);}}console.log(foo.count,window.count);//042.指向函数作用域this的第二个误区是this指向函数作用域下面的代码,在foo中尝试调用bar函数,调用成功与否取决于环境。浏览器:浏览器环境没有问题。全局声明的函数放在window对象下。foo函数中的this指的是window对象。全局环境中没有声明变量a,所以bar函数中this.a中的this.a自然是没有定义的,输出的是undefined。Node.js:在Node.js环境下,声明的函数不会放在global全局对象下,所以在foo函数中调用this.bar函数会报TypeError:this.barisnotafunction错误。要运行不报错,调用bar函数时省略前面的this即可。functionfoo(){vara=2;this.bar();}functionbar(){console.log(this.a);}foo();2.This的四种绑定规则1.默认绑定当函数调用属于Independentcalls(没有函数引用的调用),不能调用其他绑定规则,我们给它起个名字“默认绑定”,绑定到非全局对象严格模式和使用严格模式(usestrict)下界为undefined。严格模式调用:'usestrict'functiondemo(){//TypeError:Cannotreadproperty'a'ofundefinedconsole.log(this.a);}consta=1;demo();非严格模式下的调用:在浏览器环境下将a绑定到window.a,下面的代码对于用var声明的变量a输出1。functiondemo(){console.log(this.a);//1}vara=1;demo();下面的代码使用let或者const来声明变量a,结果会输出undefinedfunctiondemo(){console.log(this.a);//undefined}leta=1;demo();在例子中,我其实是想着重说明一下this的默认绑定关系,但是你会发现上面两段代码是使用了var和let进行声明导致的。它也是不同的。原因是涉及到顶层对象的概念。在Issue:Nodejs-Roadmap/issues/11中,有童鞋提到了这个问题,也是之前的一个疏忽。下面简单说一下顶层对象的概念。服务器环境指的是window,Node.js环境指的是Global)属性和全局变量属性的赋值是等价的,使用var和function来声明顶层对象的属性,而let属于ES6规范,但在ES6规范中let、const和class声明的全局变量不再是顶级对象的属性。2.隐式绑定在函数的调用位置被一个对象包含,并且有上下文,看下面的例子:functionchild(){console.log(this.name);}letparent={name:'zhangsan',child,}parent.child();//zhangsan函数在调用时会使用父对象上下文来引用子函数child,可以理解为调用子函数时父对象拥有或包含它。隐式绑定的隐患:隐式绑定的函数,因为一些粗心的操作会丢失绑定对象,此时会应用开头提到的绑定规则中的默认绑定,见如下代码:functionchild(){console.log(this.name);}letparent={name:'zhangsan',child,}letparentparent2=parent.child;varname='lisi';parent2();将parent.child函数本身赋值给parent2,调用parent2()实际上是一个未修饰的函数调用,因此应用默认绑定。3.显式绑定。显式绑定和隐式绑定从字面上理解。有一个相反的对比。一个更直接,另一个更委婉。再来看看这两条规则的含义:隐式绑定判定:通过对象内部的属性间接引用一个函数,使得this隐式绑定到对象内部属性所指向的函数(例如child上例中对象parent的属性指的是函数functionchild(){})。显示绑定:当需要引用一个对象时,进行强制绑定调用。js提供了call()和apply()方法,ES5也提供了内置方法Function.prototype.bind。call()和apply()这两个函数的第一个参数就是设置这个对象。不同的是apply传递的参数是数组传递的,call的参数是一个一个传递的。functionfruit(...args){console.log(this.name,args);}varapple={name:'apple'}varbanana={name:'banana'}fruit.call(banana,'a','b')//['a','b']fruit.apply(apple,['a','b'])//['a','b']下面是bind绑定的例子,只是put函数的this绑定一个值,返回绑定的函数,只有在执行fruit函数时才输出信息,例如:functionfruit(){console.log(this.name);}varapple={name:'apple'}fruitfruit=fruit.bind(apple);fruit();//除了上面的call,apply,bind,Apple还可以传context上下文,例如:functionfruit(name){console.log(`${this.name}:${name}`);}constobj={name:'这是水果',}constarr=['苹果','香蕉'];arr.forEach(水果,obj);//Thisisfruit:Apple//Thisisthefruit:Banana4.newbindingnewbinding也会影响这个调用,它是一个构造函数,每一个newbinding都会创建一个新的对象。functionFruit(name){this.name=name;}constf1=newFruit('apple');constf2=newFruit('banana');console.log(f1.name,f2.name);//applebanana3.优先如果this的调用位置同时应用多个绑定规则,其优先级为:新绑定->显式绑定->隐式绑定->默认绑定。4.箭头函数箭头函数不是使用function关键字定义的,也不会使用上面解释的this的四个标准规范。箭头函数将从外部函数调用的this绑定继承。fruit.call(apple)执行时,箭头函数this已经绑定,不能再修改。functionfruit(){return()=>{console.log(this.name);}}varapple={name:'apple'}varbanana={name:'banana'}varfruitfruitCall=fruit.call(apple);fruitCall。call(banana);//AppleV,This使用中常见的几个问题1.通过函数和原型链模拟类如下例子,定义函数Fruit,然后在原型链上定义info方法,实例化对象f1并分别定义对象f2调用info方法。functionFruit(name){this.name=name;}Fruit.prototype.info=function(){console.log(this.name);}constf1=newFruit('Apple');f1.info();constf2={name:'Banana'};f2.info=f1.info;f2.info()输出,两个结果不一样,原因是info方法中的this定义的时候没有对应上下文,但是当它被称为上下文,根据我们上面提到的几种绑定规则,对应于隐式绑定规则。苹果香蕉2。在原型链上使用箭头函数如果使用构造函数和原型链来模拟类,则不能在原型链上定义箭头函数,因为箭头函数中的this会继承外层函数调用的this绑定。functionFruit(name){this.name=name;}Fruit.prototype.info=()=>{console.log(this.name);}varname='Banana'constf1=newFruit('Apple');f1.info();3.在事件中使用以Node.js为例。在事件中使用的时候,当我们的监听器被调用时,如果声明的是一个普通的函数,this会指向监听器实例绑定的EventEmitter,如果使用箭头函数方式,则this不会指向EventEmitter实例。constEventEmitter=require('events');classMyEmitterextendsEventEmitter{构造函数(){super();this.name='myEmitter';}}constfunc1=()=>console.log(this.name);constfunc2=function(){console.log(this.name);};constmyEmitter=newMyEmitter();myEmitter.on('event',func1);//undefinedmyEmitter.on('event',func2);//myEmittermyEmitter.emit('event');
