函数的扩展
函数参数的默认值
基本用法
在ES6之前,不能直接为函数的参数指定默认值,只能采用变通的方法。
上面代码检查函数log
的参数y
有没有赋值,如果没有,则指定默认值为World
。这种写法的缺点在于,如果参数y
赋值了,但是对应的布尔值为false
,则该赋值不起作用。就像上面代码的最后一行,参数y
等于空字符,结果被改为默认值。
为了避免这个问题,通常需要先判断一下参数y是否被赋值,如果没有,再等于默认值。
ES6允许为函数的参数设置默认值,即直接写在参数定义的后面。
可以看到,ES6的写法比ES5简洁许多,而且非常自然。除了简洁,ES6的写法还有两个好处
:首先,阅读代码的人,可以立刻意识到哪些参数是可以省略的,不用查看函数体或文档;其次,有利于将来的代码优化,即使未来的版本在对外接口中,彻底拿掉这个参数,也不会导致以前的代码无法运行。
与解构赋值默认值结合使用
参数默认值可以与解构赋值的默认值,结合起来使用。
接下来我们来看看下面两种写法有什么差别?
上面两种写法都对函数的参数设定了默认值,区别是写法一函数参数的默认值是空对象,但是设置了对象解构赋值的默认值;写法二函数参数的默认值是一个有具体属性的对象,但是没有设置对象解构赋值的默认值。
参数默认值的位置
通常情况下,定义了默认值的参数,应该是函数的尾参数。因为这样比较容易看出来,到底省略了哪些参数。如果非尾部的参数设置默认值,实际上这个参数是没法省略的。
上面代码中,有默认值的参数都不是尾参数。这时,无法只省略该参数,而不省略它后面的参数,除非显式输入undefined
。如果传入undefined,将触发该参数等于默认值,null则没有这个效果
。
函数的length属性
指定了默认值以后,函数的length
属性,将返回没有指定默认值的参数个数。也就是说,指定了默认值后,length
属性将失真。
如果设置了默认值的参数不是尾参数,那么length属性也不再计入后面的参数了。
作用域
一个需要注意的地方是,如果参数默认值是一个变量,则该变量所处的作用域,与其他变量的作用域规则是一样的,即先是当前函数的作用域,然后才是全局作用域。
如果调用时,函数作用域内部的变量x没有生成,结果就会不一样。
如果此时,全局变量x不存在,就会报错。
如果函数A
的参数默认值是函数B
,由于函数的作用域是其声明时所在的作用域
,那么函数B
的作用域不是函数A
,而是全局作用域。请看下面的例子。
上面代码中,函数bar
的参数func
,默认是一个匿名函数,返回值为变量foo
。这个匿名函数的作用域就不是bar
。这个匿名函数声明时,是处在外层作用域,所以内部的foo
指向函数体外的声明,输出outer
。它实际上等同于下面的代码。
rest参数
ES6
引入rest
参数(形式为...变量名
),用于获取函数的多余参数,这样就不需要使用arguments
对象了。rest
参数搭配的变量是一个数组
,该变量将多余的参数放入数组中。
上面代码的add
函数是一个求和函数,利用rest
参数,可以向该函数传入任意数目的参数。下面是一个rest
参数代替arguments
变量的例子。
rest参数中的变量代表一个数组,所以数组特有的方法都可以用于这个变量。下面是一个利用rest参数改写数组push方法的例子。
注意,rest参数之后不能再有其他参数(即只能是最后一个参数),否则会报错,函数的length属性,不包括rest参数。
扩展运算符
含义
扩展运算符spread
是三个点...
。它好比rest
参数的逆运算,将一个数组转为用逗号分隔的参数序列。
该运算符主要用于函数调用。
替代数组的apply方法
由于扩展运算符可以展开数组,所以不再需要apply
方法,将数组转为函数的参数了。
一个例子是通过push函数,将一个数组添加到另一个数组的尾部
扩展运算符的应用
- (1)合并数组
|
|
- (2)与解构赋值结合
|
|
- (3)函数的返回值
JavaScript的函数只能返回一个值,如果需要返回多个值,只能返回数组或对象。扩展运算符提供了解决这个问题的一种变通方法。
- (4)字符串
扩展运算符还可以将字符串转为真正的数组
上面代码中,如果不用扩展运算符,字符串的reverse操作就不正确。
- (5)实现了Iterator接口的对象
任何Iterator接口的对象,都可以用扩展运算符转为真正的数组。
上面代码中,arrayLike
是一个类似数组的对象,但是没有部署Iterator
接口,扩展运算符就会报错。这时,可以改为使用Array.from
方法将arrayLike
转为真正的数组。
- (6)Map和Set结构,Generator函数
扩展运算符内部调用的是数据结构的Iterator接口,因此只要具有Iterator接口的对象,都可以使用扩展运算符,比如Map结构。
Generator函数运行后,返回一个遍历器对象,因此也可以使用扩展运算符。
箭头函数
基本用法
ES6允许使用“箭头”=>
定义函数。
如果箭头函数的代码块部分多于一条语句,就要使用大括号将它们括起来,并且使用return语句返回。
由于大括号被解释为代码块,所以如果箭头函数直接返回一个对象,必须在对象外面加上括号。
箭头函数的一个用处是简化回调函数
下面是rest参数与箭头函数结合的例子。
使用注意点
箭头函数有几个使用注意点
(1)函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象。
(2)不可以当作构造函数,也就是说,不可以使用new命令,否则会抛出一个错误。
(3)不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用Rest参数代替。
(4)不可以使用yield命令,因此箭头函数不能用作Generator函数。
上面四点中,第一点尤其值得注意。this对象的指向是可变的,但是在箭头函数中,它是固定的。
上面代码中,setTimeout
的参数是一个箭头函数,这个箭头函数的定义生效是在foo
函数生成时,而它的真正执行要等到100
毫秒后。如果是普通函数,执行时this
应该指向全局对象window
,这时应该输出21
。但是,箭头函数导致this
总是指向函数定义生效时所在的对象(本例是{id: 42}
),所以输出的是42
。
箭头函数可以让setTimeout里面的this,绑定定义时所在的作用域,而不是指向运行时所在的作用域。下面是另一个例子。
上面代码中,Timer
函数内部设置了两个定时器,分别使用了箭头函数和普通函数。前者的this
绑定定义时所在的作用域(即Timer
函数),后者的this
指向运行时所在的作用域(即全局对象)。所以,3100
毫秒之后,timer.s1
被更新了3次,而timer.s2
一次都没更新。
箭头函数可以让this指向固定化,这种特性很有利于封装回调函数。下面是一个例子,DOM事件的回调函数封装在一个对象里面。
上面代码的init
方法中,使用了箭头函数,这导致这个箭头函数里面的this
,总是指向handler
对象。否则,回调函数运行时,this.doSomething
这一行会报错,因为此时this
指向document
对象。
this
指向的固定化,并不是因为箭头函数内部有绑定this
的机制,实际原因是箭头函数根本没有自己的this
,导致内部的this
就是外层代码块的this
。正是因为它没有this
,所以也就不能用作构造函数。所以,箭头函数转成ES5的代码如下。
在看一个例子
上面代码之中,只有一个this
,就是函数foo
的this
,所以t1、t2、t3
都输出同样的结果。因为所有的内层函数都是箭头函数,都没有自己的this
,它们的this
其实都是最外层foo
函数的this
。
除了this
,以下三个变量在箭头函数之中也是不存在的,指向外层函数的对应变量:arguments
、super
、new.target
。
上面代码中,箭头函数内部的变量arguments
,其实是函数foo
的arguments
变量。另外,由于箭头函数没有自己的this
,所以当然也就不能用call()
、apply()
、bind()
这些方法去改变this的指向。
嵌套的箭头函数
箭头函数内部,还可以再使用箭头函数。下面是一个ES5语法的多重嵌套函数。
上面这个函数,可以使用箭头函数改写。
下面是一个部署管道机制(pipeline)的例子,即前一个函数的输出是后一个函数的输入。
如果觉得上面的写法可读性比较差,也可以采用下面的写法。
函数绑定
箭头函数可以绑定this
对象,大大减少了显式绑定this
对象的写法call、apply、bind
。但是,箭头函数并不适用于所有场合,所以ES7
提出了函数绑定
(function bind)运算符,用来取代call、apply、bind
调用。虽然该语法还是ES7
的一个提案,但是Babel
转码器已经支持。
函数绑定运算符是并排的两个双冒号::
,双冒号左边是一个对象,右边是一个函数。该运算符会自动将左边的对象,作为上下文环境(即this
对象),绑定到右边的函数上面。
|
|
如果双冒号左边为空,右边是一个对象的方法,则等于将该方法绑定在该对象上面。
由于双冒号运算符返回的还是原对象,因此可以采用链式写法。
文章参考ECMAScript 6 入门