ES6中的函数
函数对于任何编程语言都是不可或缺的,在 ES6 出现之前,函数存在诸多问题,最典型的要数 this的指向问题,不同的调用方式、不同的写法,函数内部的 this 也会相应不同。用代码来说明下:
function Sum() { |
通过上面的代码,你会发现全局函数直接调用 this 会返回 window对象,而全局函数的构造函数则返回它的 实例对象,另外如果你使用对象属性的方式来定义函数,并在这个对象上调用时,则 this 又变成对象本身。但是如果遇到 setInterval
或者 setTimeout
这样的内置函数,其内部的 this 又变成了 window对象。是不是很乱?更不要说还可以使用 apply、call改变this了。
再者,我们在写插件时,为了防止用户不设置相关属性值,通常都会给一些参数设置默认值,但是需要使用 ||
或符号,有点麻烦:
function sum(num1, num2) { |
倘若函数有多个参数,那就需要把所有的参数以及默认值都写一遍,那岂不是更麻烦?如果能够直接把参数默认值塞到函数名后面的括号里,是不是会更好一些?
接下来,看看 ES6 对函数部分作了哪些调整和改进。
一、默认参数
针对前面提到的参数问题,ES6 新增了 默认参数 的概念,旨在更高效的设置参数默认值,只需要给对应参数赋值即可:
function sum(num1 = 1, num2 = 2) { |
如上面代码,sum()
没有传递一个参数,使用两个参数的默认值,而 sum(5)
则只使用了 num2 的默认值,但 sum(5, 5)
则是不用任何一个参数默认值,全部重新定义。
通过设置参数默认值,这也就表明,sum
函数中的两个参数都是可选的。
值得一提的是,如果传入的参数为 null
或者 undefined
,则情况会有些不同:
function sum(num1 = 1, num2 = 2) { |
null
会替换掉默认值,它自身被隐式转化为 0,所以结果为 2。
而 undefined
则直接使用默认值,因此,结果为 3。
另外,空字符串可能会出乎你的所料(你一定觉得它会使用默认值),但实际上,它却替换默认值,所以,结果为 '' + 2
,即字符串 "2"
。
1.1 参数表达式
参数的默认值不仅仅只能用一个静态值来表示,它也可以是一个变量,一个表达式。你只需要把变量或表达式赋值给参数,作为参数的默认值即可。比如,后面的参数默认值可以是前面参数的值:
function sum(num1 = 1, num2 = num1) { |
当然,这个参数表达式也可以是一个函数:
var a = 1; |
或许你会好奇,为什么同样都是打印出 sum(2)
,但每次结果都是不一样呢?
这里就需要注意,para()
这个表达式(函数)只会在 sum(2)
调用时才会执行。因为 a 是全局变量,para()
内部又每次对 a 进行加 1,从而导致参数 num2 的默认值不同,最终打印出的结果也不同。
1.2 作用域
如果参数是一个变量,那它最终返回的值会受函数的作用域影响。
var num1 = 1; |
函数中参数 num1 的值是来自函数内部,而非全局变量。
如果参数是一个函数,则直接读取全局的值:
var num1 = 1; |
1.3 函数的length
我们都知道,函数有一个length属性,它的值是函数形参的个数,即:
function sum() {} |
但 ES6 对这个属性有所调整,它的值变成了没有指定参数默认值的形参个数。换句话说就是,如果某个参数已经指定了默认值,那么它就不能被包含在 length 属性中。还是用代码来说明:
function sum(num1) {} |
sum4
和 sum5
的结果可能让你有点意外。它反映了一个特殊情况,即:如果有默认值的参数不在参数列表的最后一位,则函数 length 的值都为 0 。
二、rest参数
之前,我们在处理多位参数时,都是基于 arguments 对象进行操作。但为了更方便的处理不确定的多位参数,ES6 引入了 rest参数 ,它是由三个点加变量名组成,即 ...变量名
,表示剩余参数。先来看下它的用法:
function sum(...Args) { |
或许你会觉得,用 arguments.length
也能正常获取形参长度。的确如此,但是 arguments 只是一个类似数组的对象,而 rest参数 却是一个数组。它可以使用数组的各种方法和属性,这样在函数内部操作时更方便,就拿参数拼接来说吧:
function join(...Args) { |
使用 rest参数 来操作,秒秒钟就能输出结果。但是,使用 arguments ,势必需要逐个循环,然后再进行字符串拼接。
你也可以用它排序:
function ascendSort(num1, num2) { |
join
函数的第一个参数是 sort
,而其他参数就是 ...Args
。
不过,在使用 rest参数 时,也有一些需要注意的地方。
- rest参数 必须是参数列表的最后一个
- rest参数 不包含在函数的length
用代码来说明下:
function sum(...Args, last) { |
三、扩展运算符
与 rest参数 相反的一个概念,是 扩展运算符。扩展运算符 指定一个数组并把这个数组拆分开来逐个作为参数传递给函数。
它也是用三个点加变量名组成,即 ...变量名
。
先来看下它的一些简单应用:
var arr = [3, 2, 6, 9, -3]; |
以上代码是求所有数的最大值。无论是只传入 扩展运算符,还是传入 扩展运算符 和其他参数。它都是将所有的数字拆分后,然后进行比较,最后返回最大值的。
用于函数调用:
function sum(num1, num2, num3) { |
用于数组与单项、以及多个数组之间的合并:
console.log([1, 2, ...['a', 'b'], null, 5]); // [1, 2, "a", "b", null, 5] |
四、name属性
对于函数而言,除了之前提到的 length属性 外,它还有 name属性。表示当前函数的名称,这个属性或许对于代码的调试有一定的帮助。
无论你使用哪种方式去定义一个函数,它都会有 name属性,就像下面:
function sum1() {} |
值得注意的是,如果是用变量声明的匿名函数,那么它返回的函数名会有所不同。在支持ES6的浏览器(chrome54 测)返回的是 变量名,而ES5浏览器(firefox49 测)则返回 空字符串 。
倘若你用变量声明的是一个具名函数,那么无论是否支持ES6,都会返回具名函数的名称。
var sum = function spe() {}; |
由于函数的变种(写法)很多,所以可能会出现一些特殊的name属性,比如,对象上定义的方法:
var number = { |
比如,函数上调用 bind()
方法后,返回的name值是带 bound
前缀的:
function sum() {} |
再比如,构造函数实例的name值是 anonymous
:
function sum() {} |
五、箭头函数
ES6 中新增了一种函数的写法,即 箭头函数。箭头函数就是个简写形式的函数表达式,如它的名称所描述的,它用一个 =>
符号来定义一个函数。
先来看下它的语法:
// 多个参数 |
由这几段语法可以知道,箭头函数式主要由三部分组成。首先是 函数的参数、后面跟着 =>
符号,最后是 函数体。
从上面的代码也可以看出,箭头函数的不同写法,关键在于参数的个数。
当函数有参数多个时,左边必须是多个参数包含在一个圆括号中:
var sum = (num1, num2) => num1 + num2; |
当函数只有一个参数时,左边的圆括号可以省略(也可以加上):
var increase = num => num + 1; |
当函数没有一个参数时,则左边必须使用空的圆括号:
var getThis = () => this; |
除了参数以外,右边的函数体写法也有可能不同。如果你的函数体内有零个或多个表达式,你必须使用花括号把它们包含起来。
像定义一个空函数,你可以这样:
var noop = () => {}; |
又比如,函数体内多个表达式:
var sqrtRoot = (num1, num2) => { |
如果你函数返回的是一个对象,你不能把对象直接写在 =>
符号后面,这样会导致报错。你要么使用 一对圆括号 把返回的对象包含起来,要么使用 花括号 再包一层:
var getObj1 = () => {name: 'yix'}; |
即: 如果 =>
符号后面是 ()
圆括号,则表示函数执行后返回圆括号的内容。 如果 =>
符号后面是 {}
花括号,则表示函数执行后会运行花括号里的内容。
值得注意的是,虽然箭头函数很好用,但是它和传统的函数还是有很多的区别。
5.1 不绑定 this
由文章开头我们知道,this 在函数调用中可以说是千变万化,它返回的值取决它的执行环境(context)。但是在箭头函数却不会有这样的问题,它返回的就是定义时所在的对象。
不使用箭头函数:
function Increase() { |
使用箭头函数:
function Increase() { |
在上面的代码中,不使用箭头函数,setTimeout 函数内部的this默认是 window,但全局中根本没有 num 这个变量,于是就变成了 window.num + 1 ,最后得到的结果是 NaN。
但在使用了箭头函数后,它可以让 this 不绑定运行的环境,而是把 this 指向了定义所在的环境。因此可以找到 num 的值为 1,递增后的结果为 2 。
5.2 不绑定 arguments
在普通函数中,我们可以轻松的访问到 参数对象-arguments,但是你却不能在箭头函数里访问它,因为箭头函数没有 arguments对象:
var sum1 = function(num1, num2) { |
但如果箭头函数是在普通函数里访问 arguments对象,则又能正常访问,因为此时访问到的 arguments对象 是普通函数的,而非箭头函数的:
var sum3 = function(num1, num2) { |
5.3 new 操作符
箭头函数不能用作构造器,若尝试用 new 来构造一个函数,则会报错:
var Sum = (num1, num2) => num1 + num2; |
总结起来,箭头函数有这么几个特点:
- 没有
this
、arguments
、new.target
的绑定 - 不能用
new
来构造函数。若强行使用,则会抛出一个错误 - 没有
prototype
。构造函数都没有了,要prototype
干嘛 - 函数内部没有
arguments
对象。你可以用 Rest参数 代替它 - 不能有同名的参数
六、尾调用优化
首先,必须说明的是,尾调用优化 是 ES6 针对一些性能消耗比较高的代码(比如递归函数)而引入的js引擎优化,它只是一种概念,一个优化手段,而并非是代码或者语法的某种特殊写法。
其他语言早就已经有 尾调用优化 的概念和应用了,我相信,ES6 肯定是借鉴它们的。
要明白什么是尾调用优化,你当然首先需要知道 尾调用。
所谓的 尾调用,就是一个函数在另外一个函数内部的最后一行进行调用。换言之,尾调用是发生在当一个函数尾部返回另一个函数调用时。比如:
function sum() { |
如果最后返回的调用函数是本身,则这种调用,简称 尾递归。如:
function factorial(num) { |
但是,以下的情况并不是尾调用:
function sum() { |
sum
函数里最后一行虽然返回了一个函数调用,但是它是先做了加性操作符运算,然后再返回的,所以它不算。而 factorial
函数最后一行返回的根本不是直接调用的函数,只是达到同等的效果而已,因此它也不能算尾调用。
虽然普通函数也能用到尾调用优化,但为了更深入的阐述尾调用优化的作用,我们就来说说最具有代表性的尾递归,阶层求值:
function f(n) { |
当我们运行 f(5)
时,会执行下列过程:
f(5) |
这种递归的写法并不能使用尾调用优化,因为 f
函数的最后行为不是返回单纯的函数调用。而且因为需要引用前面的关系,就会加入一层一层的堆栈帧,当 n
很大时,很有可能导致堆栈溢出。
我们把上面的函数改动一下:
function f2(n, m) { |
此时,f2
最后返回了一个函数调用,并且使用参数 m
保存了每次计算后的值。运行 f2(5)
它就转化为:
f2(5) |
这样,就变成了一个接一个、无需引用前面的堆栈帧,并且是相互独立的函数调用了。
最后,值得注意的是,你必须在严格模式下,尾调用优化才能生效!