前言
在JavaScript中,你可以通过多种方式去定义函数。
第一种常用的方法是使用关键字function:
//函数声明functiongreet(who){return`Hello,${who}!`;}//函数表达式constgreet=function(who){return`Hello,${who}`;}
代码中的函数声明和函数表达式被称为“常规函数”。
从ES2015开始,第二种可用的方法是箭头函数语法:
constgreet=(who)=>{return`Hello,${who}!`;}
虽然两者的语法都能够定义函数,但是在开发时该怎么选择呢?这是个好问题。
在本文中,我将展示两者之间的主要区别,以供你能够根据需要选择正确的语法。
1.this值
1.1常规函数
在常规JavaScript函数内部,this值(即执行上下文)是动态的。
动态上下文意味着this的值取决于如何调用函数。在JavaScript中,有4种调用常规函数的方式。
在简单调用过程中,this的值等于全局对象(如果函数在严格模式下运行,则为undefined):
functionmyFunction(){console.log(this);}//简单调用myFunction();//logsglobalobject(window)
在方法调用过程中,this的值是拥有该方法的对象:
constmyObject={method(){console.log(this);}};//方法调用myObject.method();//logs"myObject"
在使用myFunc.call(context,arg1,...,argN)或myFunc.apply(context,[arg1,...,argN])的间接调用中,this的值等于第一个参数:
functionmyFunction(){console.log(this);}constmyContext={value:'A'};myFunction.call(myContext);//logs{value:'A'}myFunction.apply(myContext);//logs{value:'A'}
在使用关键字new的构造函数调用期间,this等于新创建的实例:
functionMyFunction(){console.log(this);}newMyFunction();//logsaninstanceofMyFunction
1.2箭头函数
箭头函数中this的行为与常规函数的this行为有很大不同。
无论如何执行或在何处执行,箭头函数内部的this值始终等于外部函数的this值。换句话说,箭头函数可按词法解析this,箭头函数没有定义自己的执行上下文。
在以下示例中,myMethod()是箭头函数callback()的外部函数:
constmyObject={myMethod(items){console.log(this);//logs"myObject"constcallback=()=>{console.log(this);//logs"myObject"};items.forEach(callback);}};myObject.myMethod([1,2,3]);
箭头函数callback()中的this值等于外部函数myMethod()的this。
this词法解析是箭头函数的重要功能之一。在方法内部使用回调时,要确保箭头函数没有定义自己的this:不再有constself=this或者callback.bind(this)这种解决方法。
2.构造函数
2.1常规函数
如上一节所述,常规函数可以轻松的构造对象。
例如用Car()函数创建汽车的实例:
functionCar(color){this.color=color;}constredCar=newCar('red');redCarinstanceofCar;//=>true
Car是常规函数,使用关键字new调用时会创建Car类型的新实例。
2.2箭头函数
this此法解决了箭头函数不能用作构造函数。
如果你尝试调用带有new关键字前缀的箭头函数,则JavaScript会引发错误:
constCar=(color)=>{this.color=color;};constredCar=newCar('red');//TypeError:Carisnotaconstructor
调用newCar('red')(其中Car是箭头函数)会抛出TypeError:Carisnotaconstructor。
3.arguments对象
3.1常规函数
在常规函数的主体内部,arguments是一个特殊的类似于数组的对象,其中包含被调用函数的参数列表。
让我们用3个参数调用myFunction函数:
functionmyFunction(){console.log(arguments);}myFunction('a','b');//logs{0:'a',1:'b'}
类似于数组对象的arguments中包含调用参数:'a'和'b'。
3.2箭头函数
另一方面,箭头函数内部未定义arguments特殊关键字。
用词法解析arguments对象:箭头函数从外部函数访问arguments。
让我们试着在箭头函数内部访问arguments:
constgreet=(who)=>{return`Hello,${who}!`;}0
箭头函数myArrowFunction()由参数'c','d'调用。在其主体内部,arguments对象等于调用myRegularFunction()的参数:'a','b'。
如果你想访问箭头函数的直接参数,可以使用剩余参数...args:
constgreet=(who)=>{return`Hello,${who}!`;}1
剩余参数...args接受箭头函数的执行参数:{0:'c',1:'d'}。
4.隐式返回
4.1常规函数
使用returnexpression语句从函数返回结果:
constgreet=(who)=>{return`Hello,${who}!`;}2
如果缺少return语句,或者return语句后面没有表达式,则常规函数隐式返回undefined:
constgreet=(who)=>{return`Hello,${who}!`;}3
4.2箭头函数
可以用与常规函数相同的方式从箭头函数返回值,但有一个有用的例外。
如果箭头函数包含一个表达式,而你省略了该函数的花括号,则将显式返回该表达式。这些是内联箭头函数
constgreet=(who)=>{return`Hello,${who}!`;}4
increment()仅包含一个表达式:num+1。该表达式由箭头函数隐式返回,而无需使用return关键字。
5.方法
5.1常规函数
常规函数是在类上定义方法的常用方式。
在下面Hero类中,用了常规函数定义方法logName():
constgreet=(who)=>{return`Hello,${who}!`;}5
通常把常规函数用作方法。
有时你需要把该方法作为回调提供给setTimeout()或事件监听器。在这种情况下,你可能会很难以访问this的值。
例如用logName()方法作为setTimeout()的回调:
constgreet=(who)=>{return`Hello,${who}!`;}6
1秒钟后,undefined会输出到控制台。setTimeout()执行logName的简单调用(其中this是全局对象)。这时方法会与对象分离。
让我们手动把this值绑定到正确的上下文:
constgreet=(who)=>{return`Hello,${who}!`;}7
batman.logName.bind(batman)将this值绑定到batman实例。现在,你可以确定该方法不会丢失上下文。
手动绑定this需要样板代码,尤其是在你有很多方法的情况下。有一种更好的方法:把箭头函数作为类字段。
5.2箭头函数
感谢“类字段提案”(https://github.com/tc39/proposal-class-fields)(目前在第3阶段),你可以将箭头函数用作类中的方法。
与常规函数相反,现在用箭头定义的方法能够把this词法绑定到类实例。
让我们把箭头函数作为字段:
constgreet=(who)=>{return`Hello,${who}!`;}8
现在,你可以把batman.logName用于回调而无需手动绑定this。logName()方法中this的值始终是类实例:
constgreet=(who)=>{return`Hello,${who}!`;}9
6.总结
了解常规函数和箭头函数之间的差异有助于为特定需求选择正确的语法。
常规函数中的this值是动态的,并取决于调用方式。是箭头函数中的this在词法上是绑定的,等于外部函数的this。
常规函数中的arguments对象包含参数列表。相反,箭头函数未定义arguments(但是你可以用剩余参数...args轻松访问箭头函数参数)。
如果箭头函数有一个表达式,则即使不用return关键字也将隐式返回该表达式。
最后一点,你可以在类内部使用箭头函数语法定义去方法。粗箭头方法将this值绑定到类实例。
不管怎样调用胖箭头方法,this始终等于类实例,在回调这些方法用时非常有用。