当前位置: 首页 > 科技观察

JavaScript中thisbinding详解

时间:2023-03-13 19:15:00 科技观察

这可以说是javascript中最耐人寻味的特性了,就像高中英语中的各种时态一样,被动时态,过去时态,现在时态,过去进行时态,无论你错过了多少次,下一次可能还是错的。受《你不知道的JavaScript上卷》的启发,本文在javasript中对此进行了总结。了解this的第一步是了解this既不指函数本身,也不指函数的范围。this其实就是调用函数时发生的绑定,它指向的位置完全取决于调用函数的位置。javascript中的默认绑定是最常用的函数调用类型是独立函数调用,因此当没有其他规则适用时,将此规则视为默认值。如果函数在没有任何修饰的情况下被调用,即“裸”调用,那么将应用默认绑定规则,默认绑定指向全局范围。functionsayLocation(){console.log(this.atWhere)}varatWhere="Iaminglobal"sayLocation()//默认绑定,this绑定到全局对象,输出"Iaminglobal"再看一个例子varname="global"functionperson(){console.log(this.name)//(1)"global"person.name='inside'functionsayName(){console.log(this.name)//(2)"global"不是"inside"}sayName()//执行person函数内部的sayName函数,this指向同一个全局对象to是固定在全局对象上的(在浏览器中是window,在node中是global),所以第一句输出的是一个全局对象的name属性,当然是“global”了。person函数内部调用了sayName函数,即使第(2)句中的this仍然引用全局对象,即使person函数设置了name属性。这是默认的绑定规则,这是javascript中最常见的函数调用方式。this的绑定规则也是四种绑定规则中最简单的,绑定到全局范围。默认绑定中的严格模式在javascript中,如果使用严格模式,则无法将this绑定到全局对象。还是用第一个例子,不过这次加上严格模式语句'usestrict'functionsayLocation(){console.log(this.atWhere)}varatWhere="Iaminglobal"sayLocation()//UncaughtTypeError:Cannotreadproperty'atWhere'ofundefined就可以了可以看出,在严格模式下,当this绑定到全局对象时,实际上绑定的是undefined,所以上面的代码会报错。隐式绑定当一个函数被调用时,如果函数有一个所谓的“立足点”,即有上下文对象时,隐式绑定规则会将函数中的this绑定到this上下文对象上。如果你觉得上面这段话还不够直白,我们再来看代码。functionsay(){console.log(this.name)}varobj1={name:"zxt",say:say}varobj2={name:"zxt1",say:say}obj1.say()//zxtobj2.say()//zxt1很简单,不是吗。上面代码中,obj1和obj2就是所谓say函数的落脚点。用更专业的方式来说,它是上下文对象。当为函数指定上下文对象时,函数内部的this自然指向上下文对象。这也是一种很常见的函数调用模式。隐式绑定函数时会丢失上下文)alias()//函数调用输出“global”(2)可以看到这里输出的是“global”。为什么和上面的例子不一样?我们只是更改了obj.say的名称?首先,我们看上面第(1)句的代码,因为在javascript中,函数是对象,对象是按引用传递的,不是按值传递的。所以,第(1)句的代码只是alias=obj.say=say,即alias=say,obj.say只是起到了桥梁的作用,alias最终指向了say函数的地址,什么都没有用对象obj来做。这被称为“失去上下文”。最后,alias函数被执行,但say函数被简单地执行并输出了“global”。显式绑定显式绑定,顾名思义,将this显式绑定到上下文。在javascript中,提供了三种显式绑定方法,apply、call和bind。apply和call的用法基本类似,区别在于:apply(obj,[arg1,arg2,arg3,...]以数组的形式给出被调用函数的参数call(obj,arg1,arg2,arg3,...)依次给出被调用函数的参数,bind函数执行后,返回一个新的函数,下面代码解释。//无参函数peak(){console.log(this.name)}varname="global"varobj1={name:'obj1'}varobj2={name:'obj2'}speak()//global相当于speak.call(window)speak.call(window)speak.call(obj1)//obj1speak.call(obj2)//obj2由此可见apply,call的作用是给函数绑定一个执行上下文,而且是显式绑定的。所以,函数中的this自然而然的绑定到了call或者apply所调用的对象上。//带有参数functioncount(num1,num2){console.log(this.a*num1+num2)}varobj1={a:2}varobj2={a:3}count.call(obj1,1,2)//4count.apply(obj1,[1,2])//4count.call(obj2,1,2)//5count.apply(obj2,[1,2])//5上面的例子说明了apply和call的区别用法。另一方面,绑定函数返回绑定到指定执行上下文的新函数。还是以上面的代码为例//withparametersfunctioncount(num1,num2){console.log(this.a*num1+num2)}varobj1={a:2}varbound1=count.bind(obj1)//not指定参数bound1(1,2)//4varbound2=count.bind(obj1,1)//指定一个参数bound2(2)//4varbound3=count.bind(obj1,1,2)//指定两个参数bound3()//4varbound4=count.bind(obj1,1,2,3)//指定冗余参数,冗余参数会被忽略bound4()//4所以,bind方法只是返回一个新的Function,里面的this这个函数指定了执行上下文,这个新函数可以接受参数。newbinding最后要讲的this绑定规则是指通过new操作符调用构造函数时发生的this绑定。首先要明确的是,javascript中没有其他语言中类的概念。构造函数只是一个普通的函数,只是构造函数的函数名以大写字母开头,可以通过new操作符调用。functionPerson(name,age){this.name=namethis.age=ageconsole.log("我只是一个普通函数")}Person("zxt",22)//"我只是一个普通函数"console.log(name)//"zxt"console.log(age)//22varzxt=newPerson("zxt",22)//"我只是一个普通函数"console.log(zxt.name)//"zxt"console.log(zxt.age)//22上面的例子,首先定义了一个Person函数,可以正常调用也可以通过构造函数的形式调用。正常调用时,会按照正常的函数执行,输出一个字符串。如果是通过new操作符,则构造一个新的对象。然后,我们再来看一下这两个调用方法,this都绑定到哪里了。首先,在正常调用的时候,如前所述,此时会应用默认的绑定规则,将this绑定到全局对象上。这时候会在全局对象中添加name和age两个属性。当被new运算符调用时,该函数返回一个对象,从输出结果来看,this对象绑定到返回的对象。所以,所谓new绑定就是当一个函数通过new操作符被调用时,会产生一个新的对象,构造函数中的this会绑定到这个对象上。其实在javascript中,使用new调用一个函数,会自动执行下面的操作。创建一个全新的对象这个新对象会被执行prototypeconnection这个新对象会绑定到函数调用的this上如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象四bindingsPriority上面介绍了javascript中this的四种绑定规则,这四种绑定规则基本涵盖了所有的函数调用情况。但是,如果同时应用这四个规则中的两个或更多,或者这四个绑定的优先级顺序是什么?首先,很容易理解默认绑定的优先级最低。这是因为只有在没有其他this绑定规则可以应用时才会调用默认绑定。隐式绑定和显式绑定呢?让我们看看代码,代码从不说谎。functionspeak(){console.log(this.name)}varobj1={name:'obj1',speak:speak}varobj2={name:'obj2'}obj1.speak()//obj1(1)obj1.speak.call(obj2)//obj2(2)所以在上面的代码中,obj1.speak()被执行了,speak函数里面的this指向obj1,所以当然(1)处的代码输出的是obj1,但是当explicitlybound在speak函数中设置this为obj2,输出结果变为obj2。从这个结果可以看出显式绑定的优先级高于隐式绑定。其实我们可以这样理解obj1.speak.call(obj2)这行代码。obj1.speak只是间接获取了speak函数的引用,有点像上面说的隐式绑定,失去了上下文。好吧,现在显式绑定优先于隐式绑定,让我们比较一下新绑定和显式绑定。functionfoo(something){this.a=something}varobj1={}varbar=foo.bind(obj1)//返回一个新的函数bar,这个新函数中的this指向obj1(1)bar(2)//这个它绑定了Obj1,所以obj1.a===2console.log(obj1.a)varbaz=newbar(3)//调用new操作符后,bar函数的this指向返回的新实例baz(2)console.log(obj1.a)console.log(baz.a)我们可以看到,在(1)处,bar函数内部的this原本指向obj1,但是在(2)处,由于新的操作符号调用,但是thisbar函数内部指向返回的实例,可见new绑定的优先级高于显式绑定。至此,得到了四种绑定规则的优先级顺序,分别是newbinding>explicitbinding>implicitbinding>defaultbinding箭头函数。ES6中的this绑定箭头函数是一个重要的特性。箭头函数的this是根据外部(函数或全局)作用域来确定的。函数体中的this对象是指定义时的对象,不是前面介绍的调用时绑定的对象。举个例子vara=1varfoo=()=>{console.log(this.a)//定义在全局对象中,所以this在全局范围内绑定}varobj={a:2}foo()//1、在全局对象中调用foo.call(obj)//1,显示绑定,通过obj对象调用,但完全不影响结果。从上面的例子可以看出,箭头函数的this绑定到箭头函数定义的作用域,不能通过显式绑定修改,比如apply、call方法。我们看下面的例子//定义一个构造函数Person(name,age){this.name=namethis.age=agethis.speak=function(){console.log(this.name)//普通函数(非箭头Function),调用时this绑定的范围}this.bornYear=()=>{//这篇文章是2016年写的,所以newDate().getFullYear()得到2016//箭头函数,this绑定在实例控制台里面.log(newDate().getFullYear()-this.age)}}}varzxt=newPerson("zxt",22)zxt.speak()//"zxt"zxt.bornYear()//1994//应该有这里对大家没问题varxiaoMing={name:"xiaoming",age:18//Xiaoming永远是18岁}zxt.speak.call(xiaoMing)//"xiaoming"this绑定到对象xiaoMingzxt.bornYear.call(xiaoMing)//1994而不是1998,这是因为this总是绑定到zxt的实例上,所以ES6箭头函数没有使用标准的四种绑定规则,而是根据当前词法作用域来确定this,具体来说,箭头函数会继承t的this绑定外部函数调用,无论外部函数的this绑定在哪里。总结以上就是javascript中所有this绑定的情况。在es6之前,上面提到的四种绑定规则可以覆盖任何函数调用的情况。es6标准实施后,在函数的扩展中加入了箭头函数,这一点与之前有所不同。不同之处在于箭头函数的作用域在箭头函数被定义的作用域内。对于前面的四种绑定规则,掌握每条规则的调用条件,可以很好的理解this绑定到哪个范围。