ES6详解(四)
函数的扩展
函数参数的默认值
ES6 允许为函数的参数设置默认值,即直接写在参数定义的后面。
1 | function log(x, y = 'World') { |
1 | function Point(x = 0, y = 0) { |
好处:
①阅读代码的人,可以立刻意识到哪些参数是可以省略的,不用查看函数体或文档;
②其次,有利于将来的代码优化,即使未来的版本在对外接口中,彻底拿掉这个参数,也不会导致以前的代码无法运行。
参数变量是默认声明的,所以不能用let
或const
再次声明。
1 | function foo(x = 5) { |
使用参数默认值时,函数不能有同名参数。
1 | // 不报错 |
参数默认值可以与解构赋值的默认值,结合起来使用。
1 | function foo({x, y = 5}) { |
上面代码只使用了对象的解构赋值默认值,没有使用函数参数的默认值。只有当函数foo()
的参数是一个对象时,变量x
和y
才会通过解构赋值生成。如果函数foo()
调用时没提供参数,变量x
和y
就不会生成,从而报错。
通过提供函数参数的默认值,就可以避免这种情况。
1 | function foo({x, y = 5} = {}) { |
下面两种函数写法有什么差别?
1 | // 写法一 |
写法一:函数
m1
1
2
3function m1({x = 0, y = 0} = {}) {
return [x, y];
}这个写法使用了解构赋值和默认参数。在没有传递参数时,默认为一个空对象
{}
,然后再解构这个空对象。这确保了即使没有传递参数,函数也会正常执行并返回带有默认值的数组。写法二:函数
m2
1
2
3function m2({x, y} = { x: 0, y: 0 }) {
return [x, y];
}这个写法也使用了解构赋值和默认参数。不同之处在于,它的默认参数是
{ x: 0, y: 0 }
,而不是空对象。因此,在没有传递参数时,默认值就是这个对象,而不是空对象。如果没有传递参数,它仍然会正常执行并返回带有默认值的数组。
在实际使用中,这两种写法的差异可能在于默认参数的选择。写法一使用了空对象,而写法二使用了具有默认值的对象。在某些情况下,你可能更倾向于使用空对象作为默认参数,因为这样可以更灵活地适应传递的参数形式。
参数默认值的位置
1 | function f(x, y = 5, z) { |
上面代码中,有默认值的参数都不是尾参数。这时,无法只省略该参数,而不省略它后面的参数,除非显式输入undefined
。
如果传入undefined
,将触发该参数等于默认值,null
则没有这个效果。
1 | function foo(x = 5, y = 6) { |
上面代码中,x
参数对应undefined
,结果触发了默认值,y
参数等于null
,就没有触发默认值。
函数的 length 属性
指定了默认值以后,函数的length
属性,将返回没有指定默认值的参数个数。也就是说,指定了默认值后,length
属性将失真。
1 | (function (a) {}).length // 1 |
因为length
属性的含义是,该函数预期传入的参数个数。
如果设置了默认值的参数不是尾参数,那么length
属性也不再计入后面的参数了。
1 | (function (a = 0, b, c) {}).length // 0 |
作用域
一旦设置了参数的默认值,函数进行声明初始化时,参数会形成一个单独的作用域(context)。等到初始化结束,这个作用域就会消失。
1 | let x = 1; |
上面代码中,函数f
调用时,参数y = x
形成一个单独的作用域。这个作用域里面,变量x
本身没有定义,所以指向外层的全局变量x
。函数调用时,函数体内部的局部变量x
影响不到默认值变量x
。
如果参数的默认值是一个函数,该函数的作用域也遵守这个规则。请看下面的例子。
1 | let foo = 'outer'; |
上面代码中,函数bar
的参数func
的默认值是一个匿名函数,返回值为变量foo
。函数参数形成的单独作用域里面,并没有定义变量foo
,所以foo
指向外层的全局变量foo
,因此输出outer
。
应用
利用参数默认值,可以指定某一个参数不得省略,如果省略就抛出一个错误。
1 | function throwIfMissing() { |
上面代码的foo
函数,如果调用的时候没有参数,就会调用默认值throwIfMissing
函数,从而抛出一个错误。
另外,可以将参数默认值设为undefined
,表明这个参数是可以省略的。
1 | function foo(optional = undefined) { ··· } |
rest 参数(…数组)
ES6 引入 rest 参数(形式为...变量名
),用于获取函数的多余参数,这样就不需要使用arguments
对象了。
rest 参数搭配的变量是一个数组,该变量将多余的参数放入数组中。
1 | function add(...values) { |
上面代码的add
函数是一个求和函数,利用 rest 参数,可以向该函数传入任意数目的参数。
1 | // arguments变量的写法 |
上面代码的两种写法,比较后可以发现,rest 参数的写法更自然也更简洁。
arguments
对象不是数组,而是一个类似数组的对象。所以为了使用数组的方法,必须使用Array.from
先将其转为数组。rest 参数就不存在这个问题,它就是一个真正的数组,数组特有的方法都可以使用。
rest 参数之后不能再有其他参数(即只能是最后一个参数),否则会报错。
1 | // 报错 |
函数的length
属性,不包括 rest 参数。
1 | (function(a) {}).length // 1 |
name 属性
函数的name
属性,返回该函数的函数名。
1 | function foo() {} |
Function
构造函数返回的函数实例,name
属性的值为anonymous
。
1 | (new Function).name // "anonymous" |
bind
返回的函数,name
属性值会加上bound
前缀。
1 | function foo() {}; |
箭头函数
基本用法
1 | var f = v => v; |
如果箭头函数不需要参数或需要多个参数,就使用一个圆括号代表参数部分。
1 | var f = () => 5; |
如果箭头函数的代码块部分多于一条语句,就要使用大括号将它们括起来,并且使用return
语句返回。
1 | var sum = (num1, num2) => { return num1 + num2; } |
由于大括号被解释为代码块,所以如果箭头函数直接返回一个对象,必须在对象外面加上括号,否则会报错。
1 | // 报错 |
箭头函数可以与变量解构结合使用。
1 | const full = ({ first, last }) => first + ' ' + last; |
下面是 rest 参数与箭头函数结合的例子。
1 | const numbers = (...nums) => nums; |
使用注意点
(1)箭头函数没有自己的this
对象(详见下文)。
(2)不可以当作构造函数,也就是说,不可以对箭头函数使用new
命令,否则会抛出一个错误。
(3)不可以使用arguments
对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。
(4)不可以使用yield
命令,因此箭头函数不能用作 Generator 函数。
它没有自己的this
对象,内部的this
就是定义时上层作用域中的this
。也就是说,箭头函数内部的this
指向是固定的,相比之下,普通函数的this
指向是可变的。
1 | function foo() { |
上面代码中,setTimeout()
的参数是一个箭头函数,这个箭头函数的定义生效是在foo
函数生成时,而它的真正执行要等到 100 毫秒后。如果是普通函数,执行时this
应该指向全局对象window
,这时应该输出21
。但是,箭头函数导致this
总是指向函数定义生效时所在的对象(本例是{id: 42}
),所以打印出来的是42
。
下面例子是回调函数分别为箭头函数和普通函数,对比它们内部的this
指向。
1 | function Timer() { |
上面代码中,Timer
函数内部设置了两个定时器,分别使用了箭头函数和普通函数。前者的this
绑定定义时所在的作用域(即Timer
函数),后者的this
指向运行时所在的作用域(即全局对象)。所以,3100 毫秒之后,timer.s1
被更新了 3 次,而timer.s2
一次都没更新。
下面是一个例子,DOM 事件的回调函数封装在一个对象里面。
1 | var handler = { |
上面代码的init()
方法中,使用了箭头函数,这导致这个箭头函数里面的this
,总是指向handler
对象。如果回调函数是普通函数,那么运行this.doSomething()
这一行会报错,因为此时this
指向document
对象。
总之,箭头函数根本没有自己的this
,导致内部的this
就是外层代码块的this
。正是因为它没有this
,所以也就不能用作构造函数。
以下三个变量在箭头函数之中也是不存在的,指向外层函数的对应变量:arguments
、super
、new.target
。
由于箭头函数没有自己的this
,所以当然也就不能用call()
、apply()
、bind()
这些方法去改变this
的指向。
不适用场合
由于箭头函数使得this
从“动态”变成“静态”,下面两个场合不应该使用箭头函数。
第一个场合是定义对象的方法,且该方法内部包括this
。
1 | const cat = { |
上面代码中,cat.jumps()
方法是一个箭头函数,这是错误的。调用cat.jumps()
时,如果是普通函数,该方法内部的this
指向cat
;如果写成上面那样的箭头函数,使得this
指向全局对象,因此不会得到预期结果。这是因为对象不构成单独的作用域,导致jumps
箭头函数定义时的作用域就是全局作用域。
对象的属性建议使用传统的写法定义,不要用箭头函数定义。
第二个场合是需要动态this
的时候,也不应使用箭头函数。
1 | var button = document.getElementById('press'); |
上面代码运行时,点击按钮会报错,因为button
的监听函数是一个箭头函数,导致里面的this
就是全局对象。如果改成普通函数,this
就会动态指向被点击的按钮对象。
尾调用优化
尾调用(Tail Call)是函数式编程的一个重要概念,本身非常简单,一句话就能说清楚,就是指某个函数的最后一步是调用另一个函数。
1 | function f(x){ |
上面代码中,函数f
的最后一步是调用函数g
,这就叫尾调用。
以下三种情况,都不属于尾调用。
1 | // 情况一 |
上面代码中,情况一是调用函数g
之后,还有赋值操作,所以不属于尾调用,即使语义完全一样。情况二也属于调用后还有操作,即使写在一行内。情况三等同于下面的代码。
1 | function f(x){ |
尾调用不一定出现在函数尾部,只要是最后一步操作即可。
1 | function f(x) { |
函数调用自身,称为递归。如果尾调用自身,就称为尾递归。
1 | function factorial(n) { |
“尾调用优化”对递归操作意义重大,所以一些函数式编程语言将其写入了语言规格
ES6 中只要使用尾递归,就不会发生栈溢出(或者层层递归造成的超时),相对节省内存。
catch 命令的参数省略
以前明确要求catch
命令后面必须跟参数,接受try
代码块抛出的错误对象。
1 | try { |
ES6 允许catch
语句省略参数。
1 | try { |