深入学习js函数的隐式参数 arguments 和 this
前言
在函数调用时,arguments和this会被静默的传递给函数,并可以在函数体内引用它们,借以访问函数相关的一些信息。
其中arguments是一个类数组结构,它保存了调用时传递给函数的所有实参;this是函数执行时的上下文对象,这个对象有些让人感到困惑的行为。下面分别对他们进行讨论。
1.arguments
1.1背景
JavaScript允许函数在调用时传入的实参个数和函数定义时的形参个数不一致,比如函数在定义时声明了n个参数,在调用函数时不一定非要传入n个参数,例如:
//1.定义有一个形参的函数fn() functionfn(arg){} //2.在调用时传入0个或多个参数,并不会报错 fn();//传入0个参数 fn(1,'a',3);//传入多个参数
1.2arguments与形参的对应关系
arguments是个类数组结构,它存储了函数在调用时传入的所有实参,通过访问它的length属性可以得到其中保存的实参的个数,并可以通过arguments[n]按顺序取出传入的每个参数(n=1,2,..,arguments.length-1)。
参数在arguments中保存的顺序和传入的顺序相同,同时也和形参声明的顺序相同,例如:
functionfn(arg1,arg2,arg3){ console.log(arg1===arguments[0]);//true console.log(arg2===arguments[1]);//true console.log(arg3===arguments[2]);//true } fn(1,2,3);//调用
当传入的实参多于形参个数时,想要获得多余出的实参,就可以用arguments[n]来获取了,例如:
//定义只有一个形参的函数 functionfn(arg1){ console.log('lengthofargumentsis:',arguments.length); console.log('arguments[0]is:',arguments[0]);//获取传入的第一个实参,也就是形参arg1的值 console.log('arguments[1]is:',arguments[1]);//获取第二个实参的值,没有形参与其对应 console.log('arguments[2]is:',arguments[2]);//获取第二个实参的值,没有形参与其对应 } fn(1,2,3);//传入3个实参 //可以得到实际上传入的实参的个数并取出所有实参 //lengthofargumentsis:3 //arguments[0]is:1 //arguments[1]is:2 //arguments[2]is:3
1.3arguments与形参的值相互对应
在非严格模式下,修改arguments中的元素值会修改对应的形参值;同样的,修改形参的值也会修改对应的arguments中保存的值。下面的实验可以说明:
functionfn(arg1,arg2){ //1.修改arguments元素,对应的形参也会被修改 arguments[0]='修改了arguments'; console.log(arg1); //2.修改形参值,对应的arguments也会被修改 arg2='修改了形参值'; console.log(arguments[1]); } fn(1,2); //'修改了arguments' //'修改了形参值'
但是,在严格模式下不存在这种情况,严格模式下的arguments和形参的值之间失去了对应的关系:
'usestrict';//启用严格模式 functionfn(arg1,arg2){ //修改arguments元素,对应的形参也会被修改 arguments[0]='修改了arguments'; console.log(arg1); //修改形参值,对应的arguments也会被修改 arg2='修改了形参值'; console.log(arguments[1]); } fn(1,2); //1 //2
注意:arguments的行为和属性虽然很像数组,但它并不是数组,只是一种类数组结构:
functionfn(){ console.log(typeofarguments);//object console.log(argumentsinstanceofArray);//false } fn();
1.4为什么要了解arguments
在ES6中,可以用灵活性更强的解构的方式(...符号)获得函数调用时传入的实参,而且通过这种方式获得的实参是保存在真正的数组中的,例如:
functionfn(...args){//通过解构的方式得到实参 console.log(argsinstanceofArray);//args是真正的数组 console.log(args);//而且args中也保存了传入的实参 } fn(1,2,3); //true //Array(3)[1,2,3]
那么在有了上面这种更加灵活的方式以后,为什么还要了解arguments呢?原因是在维护老代码的时候可能不得不用到它。
2.函数上下文:this
在函数调用时,函数体内也可以访问到this参数,它代表了和函数调用相关联的对象,被称为函数上下文。
this的指向受到函数调用方式的影响,而函数的调用方式可以分成以下4种:
- 直接调用,例如:fn()
- 作为对象的方法被调用,例如:obj.fn()
- 被当做一个构造函数来使用,例如:newFn()
- 通过函数call()或者apply()调用,例如:obj.apply(fn)/obj.call(fn)
下面分别讨论以上4种调用方式下this的指向.
2.1直接调用一个函数时this的指向
有些资料说在直接调用一个函数时,这个函数的this指向window,这种说法是片面的,只有在非严格模式下而且是浏览器环境下才成立,更准确的说法是:在非严格模式下,this值会指向全局上下文(例如在浏览器中是window,Node.js环境下是global)。而在严格模式下,this的值是undefined。实验代码如下:
//非严格模式 functionfn(){ console.log(this); } fn();//global||Window
严格模式下:
'usestrict'; functionfn(){ console.log(this); } fn();//undefined
总结:在直接调用一个函数时,它的this指向分成两种情况:在非严格模式下指向全局上下文,在严格模式下指向undefined.
2.2被一个对象当做方法调用
当函数被一个对象当成方法调用时,这个函数的this会指向调用它的对象。代码验证如下:
//定义一个对象 letxm={ getThis(){//定义一个函数 returnthis;//这个函数返回自己的this指向 } } letthisOfFunc=xm.getThis();//通过对象调用函数得到函数的this指向 console.log(thisOfFunc===xm);//true,函数的this指向调用它的对象本身
因为这个原因,对象的属性可以通过this来访问,如果给xm加上一个name属性,则通过xm.name可以得到这个属性值,也可以在函数中通过this.name得到属性值,即this.name就是vm.name,进一步,this===xm。实验如下:
letxm={ name:'小明',//给xm加一个属性,可以通过xm.name访问到 getName(){ returnthis.name;//返回this的指向的name属性 } } console.log(xm.name,xm.getName());//小明小明
2.3被作为构造函数来调用时
2.3.1不要像使用普通函数一样使用构造函数
构造函数本质上是函数,只是在被new操作符调用时一个函数才被称为构造函数。然而话虽如此,但是由于写出一个构造函数的目的是用他来创建一个对象,所以还要有一些约定俗成的东西来限制这个概念,避免把构造函数当成普通函数来使用。例如,构造函数虽然能被直接调用,但是不要这样做,因为这是一个普通函数就可以做到的事情,例如:
functionPerson(name){ this.name=name; return1;//不要这样对待构造函数 } letn=Person();//不要这样使用构造函数
2.3.2使用构造函数创建对象时发生了什么
当使用new关键字来调用构造函数的最终结果是产生了一个新对象,而产生新对象的过程如下:
- 创建一个空对象{}
- 将该对象的prototype链接到构造函数的prototype上
- 将这个新对象作为this的指向
- 如果这个构造函数没有返回一个引用类型的值,则将上面构造的新对象返回
上面的内容如果需要完全理解,还需要了解原型相关的内容。这里只需要关注第3、4步就可以了,即:将this绑定到生成到的新对象上,并将这个新对象返回,进一步下结论为:使用构造函数时,this指向生成的对象,实验结果如下:
functionPerson(){ this.getThis=function(){//这个函数返回this returnthis; } } letp1=newPerson();//调用了构造函数并返回了一个新的对象 console.log(p1.getThis()===p1);//true letp2=newPerson(); console.log(p2.getThis()===p2);//true
2.3.3结论
从上面的内容可以得到如下的结论:当函数作为构造函数使用时,this指向返回的新对象
2.4通过call()或者apply()调用时
使用函数call和apply可以在调用一个函数时指定这个函数的this的指向,语法是:
fn.call(targetThis,arg1,arg2,...,argN) fn.apply(targetThis,[arg1,arg2,..,argN]) fn:要调用的函数 targetThis:要把fn的this设置到的目标 argument:要给fn传的实参
例如定义一个对象如下:
letxm={ name:'小明', sayName(){ console.log(this.name); } }; xm.sayName();//对象调用函数输出'小明'
上面定义了一个对象,对象的name属性为'小明';sayName属性是个函数,功能是输出对象的name属性的值。根据2.2部分可知sayName这个函数的this指向xm对象,this.name就是xm.name。下面定义一个新对象,并把xm.sayName这个函数的this指向新定义的对象。
新定义一个对象xh:
letxh={ name:'小红' };
对象xh只有name属性,没有sayName属性,如果想让xh也使用sayName函数来输出自己的名字,那么就要在调用sayName时让它的this指向小红,以达到this.name等于xh.name的目的。这个目的就可以通过call和apply两个函数来实现。以call函数为例来实现这个需求,只需要这样写就可以了:
xm.sayName.call(xh);//小红 xm.sayName.apply(xh);//小红
其中fn为xm.sayName;targetThis为xh,这是因为targetThis的指向就是xh,此结论可以由2.2部分的内容得到。
2.4.1call和apply的区别
call和apply的区别仅仅是要传给fn的参数的形式不同:对于apply,传给fn的参数argument是个数组,数组由所有参数组成;对于call,传给fn的参数argument直接是所有参数的排列,直接一个个写入就可以。
例如要传给函数fn三个参数:1、2、3.则对于call和apply调用的方法分别是:
fn.call(targetThis,1,2,3);//把1,2,3直接传入 fn.apply(targetThis,[1,2,3]);//把1,2,3合成数组后作为参数
2.5箭头函数和bind函数
箭头函数和bind函数对于this的处理与普通函数不同,要单独拿出来说。
2.5.1箭头函数
与传统函数不同,箭头函数本身不包含this,它的this继承自它定义时的作用域链的上一层。而且箭头函数不能作为构造函数,它也没有文章第1部分所说的arguments属性。
下面用一个例子引出箭头函数中this的来源:
functionPerson(){ this.age=24; setTimeout(function(){ console.log(this.age);//undefined console.log(this===window);//true },1000); } varp=newPerson();//创建一个实例的时候就立即执行了定时器
可以看到,在定时器内定义的普通匿名函数无法访问到Person的age属性,这是因为setTimeout是个全局函数,它的内部的this指向的是window,而window上没有age这个属性,所以就得到了undefined。从下面this===window为true也说明了匿名函数中this指向的是window。
将普通的函数换成箭头函数之后可以看到如下结果:
functionPerson(){ this.age=24; setTimeout(()=>{ console.log(this.age);//24 console.log(this===p);//true },1000); } varp=newPerson();
由上面的代码可以看出箭头函数内的this指向实例p,即它的this指向的是定义时候的作用域链的上一层。
说明:这个例子仅用来引出箭头函数的this指向的来源,不要像这样使用构造函数。
2.5.2bind函数
bind函数的作用是根据一个旧函数而创建一个新函数,语法为newFn=oldFn.bind(thisTarget)。它会将旧函数复制一份作为新函数,然后将新函数的this永远绑定到thisTarget指向的上下文中,然后返回这个新函数,以后每次调用这个新函数时,无论用什么方法都无法改变这个新函数的this指向。例如:
//创建一个对象有name和sayName属性 letp1={ name:'P1', sayName(){ console.log(this.name);//访问函数指向的this的name属性 } } p1.sayName();//P1 //创建一个对象p2,并把这个对象作为bind函数绑定的this letp2={ name:'P2' } //将p1的sayName函数的this绑定到p2上,生成新函数sayP2Name并返回 letsayP2Name=p1.sayName.bind(p2); //由于此时sayP2Name的内部this已经绑定了p2, //所以即使是按文章2.1部分所说的直接调用sayP2Name,它的this也是指向p2的,并不是指向全局上下文或者undefined sayP2Name();//P2 //定义新对象,尝试将sayP2Name的this指向到p3上 letp3={ name:'P3' } //尝试使用call和apply函数来将sayP2Name函数的this指向p3, //但是由于sayP2Name函数的this已经被bind函数永远绑定到p2上了,所以this.name仍然是p2.name sayP2Name.call(p3);//P2 sayP2Name.apply(p3);//P2
通过以上内容可知一旦通过bind函数绑定了this,就再也无法改变this的指向了.
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持毛票票。