函数参数的默认值
基本用法
在ES6之前,不能直接为函数的参数指定默认值,为了避免这个问题,通常需要先判断一下参数y
是否被赋值,如果没有,再等于默认值。
ES6允许为函数的参数设置默认值,即直接写在参数定义的后面。
@H_403_12@function log(x,y = 'World') { console.log(x,y); } log('Hello') // Hello World log('Hello','China') Hello China log('Hello','') Hello
可以看到,ES6的写法比ES5简洁许多,而且非常自然。
ES6的写法还有两个好处:首先,阅读代码的人,可以立刻意识到哪些参数是可以省略的,不用查看函数体或文档;其次,有利于将来的代码优化,即使未来的版本在对外接口中,彻底拿掉这个参数,也不会导致以前的代码无法运行。
参数变量是默认声明的,所以不能用let
或const
再次声明。
function foo(x = 5) { let x = 1; error const x = 2; error }
上面代码中,参数变量x
是默认声明的,在函数体中,不能用let
或const
再次声明,否则会报错。
与解构赋值默认值结合使用
参数默认值可以与解构赋值的默认值,结合起来使用。
function foo({x,y = 5}) { console.log(x,y); } foo({}) undefined,5 foo({x: 1}) 1,5 foo({x: 1,y: 2}) TypeError: Cannot read property 'x' of undefined
上面代码使用了对象的解构赋值默认值,而没有使用函数参数的默认值。只有当函数foo
的参数是一个对象时,变量x
和y
才会通过解构赋值而生成。如果函数foo
调用时参数不是对象,变量x
和y
就不会生成,从而报错。如果参数对象没有y
属性,y
的默认值5才会生效。
参数默认值的位置
通常情况下,定义了默认值的参数,应该是函数的尾参数。因为这样比较容易看出来,到底省略了哪些参数。如果非尾部的参数设置默认值,实际上这个参数是没法省略的。
例一 function f(x = 1,y) { return [x,y]; } f() [1,undefined] f(2) [2,undefined]) f(,1) 报错 f(undefined,1] 例二 function f(x,z) { [undefined,5,undefined] f(1) 报错 f(1,undefined,2]
上面代码中,有默认值的参数都不是尾参数。这时,无法只省略该参数,而不省略它后面的参数,除非显式输入undefined
。
如果传入undefined
,将触发该参数等于默认值,null
则没有这个效果。
function foo(x = 5,y = 6null) 5 null
上面代码中,x
参数对应undefined
,结果触发了默认值,y
参数等于null
,就没有触发默认值。
函数的length属性
指定了默认值以后,函数的length
属性,将返回没有指定默认值的参数个数。也就是说,指定了默认值后,length
属性将失真。
(function (a) {}).length 1 (function (a = 5) {}).length 0 (function (a,b,c = 5) {}).length 2
上面代码中,length
属性的返回值,等于函数的参数个数减去指定了默认值的参数个数。比如,上面最后一个函数,定义了3个参数,其中有一个参数c
指定了默认值,因此length
属性等于3
减去1
,最后得到2
。
这是因为length
属性的含义是,该函数预期传入的参数个数。某个参数指定默认值以后,预期传入的参数个数就不包括这个参数了。同理,rest参数也不会计入length
属性。
(function(...args) {}).length 0
如果设置了默认值的参数不是尾参数,那么length
属性也不再计入后面的参数了。
(function (a = 0,c) {}).length 1
作用域
一个需要注意的地方是,如果参数默认值是一个变量,则该变量所处的作用域,与其他变量的作用域规则是一样的,即先是当前函数的作用域,然后才是全局作用域。
var x = 1; x) { console.log(y); } f(2) 2
上面代码中,参数y
的默认值等于x
。调用时,由于函数作用域内部的变量x
已经生成,所以y
等于参数x
,而不是全局变量x
。
如果调用时,函数作用域内部的变量x
没有生成,结果就会不一样。
let x = 1function f(y = x) { let x = 2; console.log(y); } f() 1
上面代码中,函数调用时,y
的默认值变量x
尚未在函数内部生成,所以x
指向全局变量,结果又不一样。
利用参数默认值,可以指定某一个参数不得省略,如果省略就抛出一个错误。
function throwIfMissing() { throw new Error('Missing parameter'); } function foo(mustBeProvided = throwIfMissing()) { mustBeProvided; } foo() Error: Missing parameter
上面代码的foo
函数,如果调用的时候没有参数,就会调用默认值throwIfMissing
函数,从而抛出一个错误。
从上面代码还可以看到,参数mustBeProvided
的默认值等于throwIfMissing
函数的运行结果(即函数名之后有一对圆括号),这表明参数的默认值不是在定义时执行,而是在运行时执行(即如果参数已经赋值,默认值中的函数就不会运行),这与python语言不一样。
另外,可以将参数默认值设为undefined
,表明这个参数是可以省略的。
function foo(optional = undefined) { ··· }
rest参数
ES6引入rest参数(形式为“...变量名”),用于获取函数的多余参数,这样就不需要使用arguments对象了。rest参数搭配的变量是一个数组,该变量将多余的参数放入数组中。
add(...values) { let sum = 0; for (var val of values) { sum += val; } sum; } add(2,3) 10
上面代码的add函数是一个求和函数,利用rest参数,可以向该函数传入任意数目的参数。
下面是一个rest参数代替arguments变量的例子。
arguments变量的写法 sortNumbers() { Array.prototype.slice.call(arguments).sort(); } rest参数的写法 const sortNumbers = (...numbers) => numbers.sort();
上面代码的两种写法,比较后可以发现,rest参数的写法更自然也更简洁。
注意,rest参数之后不能再有其他参数(即只能是最后一个参数),否则会报错。
报错 f(a,...b,c) { ... }
(function(a) {}).length function(...a) {}).length function(a,...b) {}).length 1
扩展运算符
含义
扩展运算符(spread)是三个点(...
)。它好比rest参数的逆运算,将一个数组转为用逗号分隔的参数序列。
console.log(...[1,2,3]) 1 2 3 console.log(1,...[2,3,4],5 1 2 3 4 5 [...document.querySelectorAll('div')] [<div>,<div>,<div>]
替代数组的apply方法
由于扩展运算符可以展开数组,所以不再需要apply
方法,将数组转为函数的参数了。
ES5的写法 f(x,1)"> ... } var args = [0,1,2]; f.apply( ES6的写法 ]; f(...args);
扩展运算符的应用
(1)合并数组
ES5 [1,1)">].concat(more) ES6 [1,...more]
(2)与解构赋值结合
扩展运算符可以与解构赋值结合起来,用于生成数组。
ES5 a = list[0],rest = list.slice(1 ES6 [a,...rest] = list
如果将扩展运算符用于数组赋值,只能放在参数的最后一位,否则会报错。
(3)函数的返回值
JavaScript的函数只能返回一个值,如果需要返回多个值,只能返回数组或对象。扩展运算符提供了解决这个问题的一种变通方法。
var dateFields = readDateFields(database); var d = new Date(...dateFields);
上面代码从数据库取出一行数据,通过扩展运算符,直接将其传入构造函数Date
。
4)字符串
扩展运算符还可以将字符串转为真正的数组。
[...'hello'] [ "h","e","l","o" ]
上面的写法,有一个重要的好处,那就是能够正确识别32位的Unicode字符。
'x\uD83D\uDE80y'.length 4 [...'x\uD83D\uDE80y'].length 3
(5)实现了Iterator接口的对象
任何Iterator接口的对象,都可以用扩展运算符转为真正的数组。
var nodeList = document.querySelectorAll('div'); var array = [...nodeList];
上面代码中,querySelectorAll
方法返回的是一个nodeList
对象。它不是数组,而是一个类似数组的对象。这时,扩展运算符可以将其转为真正的数组,原因就在于NodeList
对象实现了Iterator接口。
name属性
foo() {} foo.name "foo"
这个属性早就被浏览器广泛支持,但是直到ES6,才将其写入了标准。
需要注意的是,ES6对这个属性的行为做出了一些修改。如果将一个匿名函数赋值给一个变量,ES5的name
属性,会返回空字符串,而ES6的name
属性会返回实际的函数名。
var func1 = () {}; ES5 func1.name "" ES6 func1.name "func1"
上面代码中,变量func1
等于一个匿名函数,ES5和ES6的name
属性返回的值不一样。
bind
返回的函数,name
属性值会加上“bound ”前缀。
foo() {}; foo.bind({}).name "bound foo" (function(){}).bind({}).name "bound "