详解JavaScript的this指向和绑定
注意:本文属于基础篇,请大神绕路。如果你不够了解,或者了解的还不完整,那么可以通过本文来复习一下。
this指向的类型
刚开始学习JavaScript的时候,this总是最能让人迷惑,下面我们一起看一下在JavaScript中应该如何确定this的指向。
this是在函数被调用时确定的,它的指向完全取决于函数调用的地方,而不是它被声明的地方(除箭头函数外)。当一个函数被调用时,会创建一个执行上下文,它包含函数在哪里被调用(调用栈)、函数的调用方式、传入的参数等信息,this就是这个记录的一个属性,它会在函数执行的过程中被用到。
this在函数的指向有以下几种场景:
- 作为构造函数被new调用
- 作为对象的方法使用
- 作为函数直接调用
- 被call、apply、bind调用
- 箭头函数中的this
下面我们分别来讨论一下这些场景中this的指向。
1.new绑定
函数如果作为构造函数使用new调用时,this绑定的是新创建的构造函数的实例。
functionFoo(){ console.log(this) } varbar=newFoo()//输出:Foo实例,this就是bar
实际上使用new调用构造函数时,会依次执行下面的操作:
- 创建一个新对象
- 构造函数的prototype被赋值给这个新对象的__proto__
- 将新对象赋给当前的this
- 执行构造函数
- 如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象,如果返回的不是对象将被忽略
2.显式绑定
通过call、apply、bind我们可以修改函数绑定的this,使其成为我们指定的对象。通过这些方法的第一个参数我们可以显式地绑定this。
functionfoo(name,price){ this.name=name this.price=price } functionFood(category,name,price){ foo.call(this,name,price)//call方式调用 //foo.apply(this,[name,price])//apply方式调用 this.category=category } newFood('食品','汉堡','5块钱') //浏览器中输出:{name:"汉堡",price:"5块钱",category:"食品"}
call和apply的区别是call方法接受的是参数列表,而apply方法接受的是一个参数数组。
func.call(thisArg,arg1,arg2,...)//call用法 func.apply(thisArg,[arg1,arg2,...])//apply用法
而bind方法是设置this为给定的值,并返回一个新的函数,且在调用新函数时,将给定参数列表作为原函数的参数序列的前若干项。
func.bind(thisArg[,arg1[,arg2[,...]]])//bind用法
举个例子:
varfood={ name:'汉堡', price:'5块钱', getPrice:function(place){ console.log(place+this.price) }, } food.getPrice('KFC')//浏览器中输出:"KFC5块钱" vargetPrice1=food.getPrice.bind({name:'鸡腿',price:'7块钱'},'肯打鸡') getPrice1()//浏览器中输出:"肯打鸡7块钱"
关于bind的原理,我们可以使用apply方法自己实现一个bind看一下:
//ES5方式 Function.prototype.bind= Function.prototype.bind|| function(){ varself=this varrest1=Array.prototype.slice.call(arguments) varcontext=rest1.shift() returnfunction(){ varrest2=Array.prototype.slice.call(arguments) returnself.apply(context,rest1.concat(rest2)) } } //ES6方式 Function.prototype.bind= Function.prototype.bind|| function(...rest1){ constself=this constcontext=rest1.shift() returnfunction(...rest2){ returnself.apply(context,[...rest1,...rest2]) } }
ES6方式用了一些ES6的知识比如rest参数、数组解构。
注意:如果你把null或undefined作为this的绑定对象传入call、apply、bind,这些值在调用时会被忽略,实际应用的是默认绑定规则。
vara='hello' functionfoo(){ console.log(this.a) } foo.call(null)//浏览器中输出:"hello"
3.隐式绑定
函数是否在某个上下文对象中调用,如果是的话this绑定的是那个上下文对象。
vara='hello' varobj={ a:'world', foo:function(){ console.log(this.a) }, } obj.foo()//浏览器中输出:"world"
上面代码中,foo方法是作为对象的属性调用的,那么此时foo方法执行时,this指向obj对象。也就是说,此时this指向调用这个方法的对象,如果嵌套了多个对象,那么指向最后一个调用这个方法的对象:
vara='hello' varobj={ a:'world', b:{ a:'China', foo:function(){ console.log(this.a) }, }, } obj.b.foo()//浏览器中输出:"China"
最后一个对象是obj上的b,那么此时foo方法执行时,其中的this指向的就是b对象。
4.默认绑定
函数独立调用,直接使用不带任何修饰的函数引用进行调用,也是上面几种绑定途径之外的方式。非严格模式下this绑定到全局对象(浏览器下是winodw,node环境是global),严格模式下this绑定到undefined(因为严格模式不允许this指向全局对象)。
vara='hello' functionfoo(){ vara='world' console.log(this.a) console.log(this) } foo()//相当于执行window.foo() //浏览器中输出:"hello" //浏览器中输出:Window对象
上面代码中,变量a被声明在全局作用域,成为全局对象window的一个同名属性。函数foo被执行时,this此时指向的是全局对象,因此打印出来的a是全局对象的属性。
注意有一种情况:
vara='hello' varobj={ a:'world', foo:function(){ console.log(this.a) }, } varbar=obj.foo bar()//浏览器中输出:"hello"
此时bar函数,也就是obj上的foo方法为什么又指向了全局对象呢,是因为bar方法此时是作为函数独立调用的,所以此时的场景属于默认绑定,而不是隐式绑定。这种情况和把方法作为回调函数的场景类似:
vara='hello' varobj={ a:'world', foo:function(){ console.log(this.a) }, } functionfunc(fn){ fn() } func(obj.foo)//浏览器中输出:"hello"
参数传递实际上也是一种隐式的赋值,只不过这里obj.foo方法是被隐式赋值给了函数func的形参fn,而之前的情景是自己赋值,两种情景实际上类似。这种场景我们遇到的比较多的是setTimeout和setInterval,如果回调函数不是箭头函数,那么其中的this指向的就是全局对象.
其实我们可以把默认绑定当作是隐式绑定的特殊情况,比如上面的bar(),我们可以当作是使用window.bar()的方式调用的,此时bar中的this根据隐式绑定的情景指向的就是window。
this绑定的优先级
this存在多个使用场景,那么多个场景同时出现的时候,this到底应该如何指向呢。这里存在一个优先级的概念,this根据优先级来确定指向。优先级:new绑定>显示绑定>隐式绑定>默认绑定
所以this的判断顺序:
- new绑定:函数是否在new中调用?如果是的话this绑定的是新创建的对象;
- 显式绑定:函数是否是通过bind、call、apply调用?如果是的话,this绑定的是指定的对象;
- 隐式绑定:函数是否在某个上下文对象中调用?如果是的话,this绑定的是那个上下文对象;
- 如果都不是的话,使用默认绑定。如果在严格模式下,就绑定到undefined,否则绑定到全局对象。
箭头函数中的this
箭头函数是根据其声明的地方来决定this的,它是ES6的知识点。
箭头函数的this绑定是无法通过call、apply、bind被修改的,且因为箭头函数没有构造函数constructor,所以也不可以使用new调用,即不能作为构造函数,否则会报错。
vara='hello' varobj={ a:'world', foo:()=>{ console.log(this.a) }, } obj.foo()//浏览器中输出:"hello"
我们可以看看ECMAScript标准中对箭头函数的描述。
原文:
AnArrowFunctiondoesnotdefinelocalbindingsforarguments,super,this,ornew.target.Anyreferencetoarguments,super,this,ornew.targetwithinanArrowFunctionmustresolvetoabindinginalexicallyenclosingenvironment.TypicallythiswillbetheFunctionEnvironmentofanimmediatelyenclosingfunction.
翻译:
箭头函数不为arguments、super、this或new.target定义本地绑定。箭头函数中对arguments、super、this或new.target的任何引用都解析为当前所在词法作用域中的绑定。通常,这是箭头函数所在的函数作用域。
—ECMAScriptLanguageSpecification-ArrowFunction|ECMA标准-箭头函数
一个this的小练习
用一个小练习来实战一下:
vara=20 varobj={ a:40, foo:()=>{ console.log(this.a) functionfunc(){ this.a=60 console.log(this.a) } func.prototype.a=50 returnfunc }, } varbar=obj.foo()//浏览器中输出:20 bar()//浏览器中输出:60 newbar()//浏览器中输出:60
稍微解释一下:
1)vara=20这句在全局变量window上创建了个属性a并赋值为20;
2)首先执行的是obj.foo(),这是一个箭头函数,箭头函数不创建新的函数作用域直接沿用语句外部的作用域,因此obj.foo()执行时箭头函数中this是全局window,首先打印出window上的属性a的值20,箭头函数返回了一个原型上有个值为50的属性a的函数对象func给bar;
3)继续执行的是bar(),这里执行的是刚刚箭头函数返回的闭包func,其内部的this指向window,因此this.a修改了window.a的值为60并打印出来;
4)然后执行的是newbar(),根据之前的表述,new操作符会在func函数中创建一个继承了func原型的实例对象并用this指向它,随后this.a=60又在实例对象上创建了一个属性a,在之后的打印中已经在实例上找到了属性a,因此就不继续往对象原型上查找了,所以打印出第三个60。
如果把上面例子的箭头函数换成普通函数呢,结果会是什么样?
vara=20 varobj={ a:40, foo:function(){ console.log(this.a) functionfunc(){ this.a=60 console.log(this.a) } func.prototype.a=50 returnfunc }, } varbar=obj.foo()//浏览器中输出:40 bar()//浏览器中输出:60 newbar()//浏览器中输出:60
这个例子就不详细讲解了。
如果把上面两个例子弄懂原理,基本上this的指向就掌握的差不多啦~
以上就是详解JavaScript的this指向和绑定的详细内容,更多关于JavaScript的this指向和绑定的资料请关注毛票票其它相关文章!
声明:本文内容来源于网络,版权归原作者所有,内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:czq8825#qq.com(发邮件时,请将#更换为@)进行举报,并提供相关证据,一经查实,本站将立刻删除涉嫌侵权内容。