函数的扩展(上)
函数的默认值
基本用法
在ES6之前,不能直接为函数的参数指定默认值,只能采用变通的方法。
js
function log (x, y) {
y = y || 'World';
console.log(x, y);
}
log('Hello') // Hello World
log('Hello', 'China') // Hello China
log('Hello', '') // Hello World
上面的代码检查函数log
的参数y
有没有赋值,如果没有,则指定默认值为World
。这种写法的缺点在于,如果参数y
赋值了,但是对应的布尔值为false
,则该赋值不起作用。就像以上代码的最后一行,参数y
等于空字符,结果被改为默认值。
为了避免这个问题,通常需要先判断一下参数y
是否被赋值,如果没有,再等于默认值。这有两种写法。
js
// 写法一
if (typeof y === 'undefined') {
y = 'World';
}
// 写法二
if (arguments.length === 1) {
y = 'World'
}
ES6允许为函数的参数设置默认值。
js
function log (x, y = 'World') {
console.log(x, y);
}
log('Hello') // Hello World
log('Hello', 'China') // Hello China
log('Hello', '') // Hello
可以看到,ES6的写法比ES5简洁许多,而且非常自然。
js
function Point(x = 0, y = 0) {
this.x = x;
this.y = y;
}
var p = new Point();
p // {x: 0, y: 0}
除了简洁,ES6的写法还有两个好处:首先,阅读代码的人可以立刻意识到哪些参数是可以省略的,不用查看函数体或文档;其次,有利于将来的代码优化,即使未来的版本彻底拿掉这个参数,也不会导致以前的代码无法运行。
参数变量是默认声明的,所以不能用let
或const
再次声明。
js
function foo (x = 5) {
let x = 1; // error
const x = 1; // error
}
与解构赋值默认值结合使用
参数默认值可以与解构赋值的默认值结合起来使用。
js
function foo ({x, y = 5}) {
console.log(x, y);
}
foo({}) // undefined, 5
foo({x: 1}) // 1, 5
foo({x: 1, y: 5}) // 1, 2
foo() // TypeError: Cannot read property 'x' of undefined
上面的代码使用了对象的解构赋值
默认值,而没有使用函数参数的默认值。只有当函数foo
的参数是一个对象时,变量x
和y
才会通过解构赋值而生成。如果函数foo
调用时参数不是对象,变量x
和y
就不会生成,从而报错。
js
function fetch(url, { body = '', method = 'GET', headers = {} }) {
console.log(method);
}
fetch('http://example.com', {}) // 'GET'
fetch('http://example.com') // 报错
上面的代码中,如果函数fetch的第二个参数是一个对象,就可以为它的3个属性设置默认值。上面的写法不能省略第二个参数,如果结合函数参数的默认值,就可以省略第二个参数。这时,就出现了双重默认值。
js
function fetch(url, { method = 'GET' } = {}) {
console.log(method)
}
fetch('http://example.com') // 'GET'
上面的代码中,函数fetch没有第二个参数时,函数参数的默认值就会生效,然后才是解构赋值的默认值生效,变量method取到默认值GET。
js
// 写法一
function m1 ({x = 0, y = 0} = {}) {
return [x, y];
}
// 写法二
function m2 ({x, y} = {x: 0, y: 0}) {
return [x, y];
}
上面两种写法都对函数的参数设定了默认值,区别是:写法一中函数参数的默认值是空对象,但是设置了对象解构赋值的默认值;写法二中函数参数的默认值是一个有具体属性的对象,但是没有设置对象解构赋值的默认值。
js
// 函数没有参数的情况
m1() // [0, 0]
m2() // [0, 0]
// x和y都有值的情况
m1({x: 3, y: 8}) // [3, 8]
m2({x: 3, y: 8}) // [3, 8]
// x有值,y无值的情况
m1({x: 3}) // [3, 0]
m2({x: 3}) // [3, undefined]
// x和y都无值的情况
m1() // [0, 0]
m2() // [undefined, undefined]
m1({z: 3}) // [0, 0]
m2({z: 3}) // [undefined, undefined]
参数默认值的位置
通常情况下,定义了默认的参数应该是函数的尾参数。因为这样比较容易看出,到底省略了哪些参数。如果非尾部参数设置默认值,实际上这个参数是无法省略的。
js
// example 1
function f (x = 1, y) {
return [x, y];
}
f() // [1, undefined]
f(2) // [2, undefined]
f(, 1) // 报错
// example 2
function f(x, y = 5, z) {
return [x, y, z];
}
f() // [undefined, 5, undefined]
f(2) // [2, 5, undefined]
f(1, , 2) // 报错
f(1, undefined, 2) // [1, 5, 2]
上面的代码中,有默认值的参数都不是尾参数。这时,无法只省略该参数而不省略其后的参数。如果传入undefined
,将触发该参数等于默认值,null
则没有这个效果。
函数的legth属性
指定了默认值以后,函数的length属性
将返回没有指定默认的参数的个数。也就是说,指定默认值后,length属性
将失真。
js
(function (a) {}).length // 1
(function (a = 5) {}).length // 0
(function (a, b, c = 5) {}).length // 2
上面的代码中,length属性
的返回值等于函数的参数个数减去指定了默认值的参数个数。这是因为length属性的
含义是:**该函数预期传入的参数个数。**某个参数指定默认值后,预期传入的参数个数就不包括这个参数了。同理rest参数
也不会计入length属性
。
js
(function (...args) {}).length // 0
作用域
一个需要注意的地方是,如果参数默认值是一个变量,则该变量所处的作用域与其他变量的作用域规则是一样的,即先是当前函数的作用域,然后才是全局作用域。
js
var x = 1;
function f (x, y = x) {
console.log(y)
}
f(2) // 2
上面的代码中,参数y
的默认值等于x
。调用时,由于函数作用域内部的变量x
已经生成,所以y
等于参数x
而不是全局变量x
。
如果调用时函数作用域内部的变量x
没有生成,结果就会不一样。
js
let x = 1;
function f (y = x) {
let x = 2;
console.log(y);
}
f() // 1
上面的代码中,函数调用时y
的默认值变量x
尚未在函数内部生成,所以x
指向全局变量。如果此时全局变量x
不存在就会报错。
js
// let x = 1;
function f (y = x) {
let x = 2;
console.log(y);
}
f() // ReferenceError: x is not defined
如果函数A的参数默认值是函数B,那么由于函数的作用域是其声明时所在的作用域,函数B作用域就不是函数A,而是全局作用域。请看下面的例子:
js
let foo = 'outer';
function bar (func = x => foo) {
let foo = 'inner';
console.log(func());
}
bar(); // outer
上面的代码中,函数bar
的参数func
默认是一个匿名函数,返回值为变量foo
。这个匿名函数的作用域就不是bar
。这个匿名函数声明时是处在外层作用域,所以内部的foo
指向函数体外的声明,输出“outer”。它实际上等同于下面的代码:
js
let foo = 'outer';
let f = x => foo;
function bar (func = f) {
let foo = 'inner';
console.log(func());
}
bar(); // outer
如果写成下面这样,就会报错。
js
function bar (func = () => foo) {
let foo = 'inner';
console.log(func());
}
bar(); // ReferenceError: foo is not defined
应用
利用参数默认值,可以指定某一个参数不得省略,如果省略就抛出一个错误。
js
function throwIfMissing () {
throw new Error('Missing parameter');
}
function foo (mustBeProvided = throwIfMissing()) {
return mustBeProvided;
}
foo() // Missing parameter
上面的代码中的foo函数
,如果调用的时候没有参数,就会调用默认值throwIfMissing函数
,从而抛出一个错误。
从上面的代码还可以看到,参数mustBeProvided
的默认值等于throwIfMissing函数的运行结果
(即函数名之后有一对圆括号),这表明参数的默认值不是在定义时执行,而是在运行时执行(即如果参数已经赋值,默认值中的函数就不会运行),这与Python语言
不一样。
另外,可以将参数默认值设置为undefined
,表明这个参数是可以省略的。
rest参数
ES6引入了rest参数
(形式为...变量名
),用于获取函数的多余参数,这样就不需要用arguments对象
了。rest参数
搭配的变量是一个数组,该变量将多余的参数放入其中。
js
function add (...valus) {
let sum = 0;
for (var val of values) {
sum += val;
}
return sum;
}
add(2, 5, 3) // 10
以上代码中的add函数
是一个求和函数,利用rest参数
可以向该函数传入任意数目的参数。
下面是一个rest参数
代替arguments变量
的例子。
js
// arguments变量的写法
const sortNumbers = () => {
Array.prototype.slice.call(arguments).sort();
}
// rest参数的写法
const sortNumbers = (...numbers) => number.sort();
rest参数
中的变量代表一个数组,所以数组特有的方法都可以用于这个变量。下面是一个利用rest参数
改定数组的push方法
的例子。
js
function push(array, ...items) {
item.forEach(function (item) {
array.push(item);
console.log(item);
})
}
var a = [];
push(a, 1, 2, 3)
注意,rest参数
之后不能再有其他参数(即只能是最后一个参数),否则会报错。
js
// 报错
function f (a, ...b, c) {
// ...
}
函数的length属性
不包括rest参数
。
扩展运算符
含义
扩展运算符(spread)是三个点(...
)。它好比rest参数
的逆运算,将一个数组转为用逗号分隔的参数序列。
js
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>]
该运算符主要用于函数调用。
js
function push(array, ...items) {
array.push(...items);
}
function add (x, y) {
return x + y;
}
var numbers = [4, 38];
add(...numbers) // 42
上面的代码中,array.push(...items)
和add(...numbers)
这两行都是函数的调用,它们都使用扩展运算符。该运算符将一个数组变为参数序列。
扩展运算符与正常的函数参数可以结合使用,非常灵活
js
function f (v, w, x, y, z) {}
var args = [0, 1];
f(-1, ...args, 2, ...[3]);
替代数组的apply方法
由于扩展运算可以展开数组,所以不再需要apply方法
将数组转为函数的参数了。
js
function f (x, y, z) {}
var args = [0, 1, 2];
// ES5的写法
f.apply(null, args);
// ES6的写法
f(...args);
扩展运算符的应用
合并数组
扩展运算符提供了数组合并的新写法。
js
// ES5
[1, 2].concat(more)
// ES6
[1, 2, ...more]
var arr1 = ['a', 'b'];
var arr2 = ['c'];
var arr3 = ['d', 'e'];
// ES5的合并数组
arr1.concat(arr2, arr3);
// ES6的合并数组
[...arr1, ...arr2, ...arr3];
与解构赋值结合
扩展运算符可以与解构赋值结合起来用于生成数组。
js
// ES5
a = list[0], rest = list.slice(1)
// ES6
[a, ...rest] = list
下面是另外一些例子。
js
const [first, ...rest] = [1, 2, 3, 4, 5];
first // 1
rest [2, 3, 4, 5]
const [first, ...rest] = []
first // undefined
rest // []
const [first, ...rest] = ['foo'];
first // 'foo'
rest // []
如果将扩展运算符用于数组赋值,只能放在参数的最后一位,否则会报错。
js
// 报错
const [...butLast, last] = [1, 2, 3, 4, 5]
const [first, ...middle, last] = [1, 2, 3, 4, 5]
函数的返回值
JavaScript的函数只能返回一个值,如果需要返回多个值,只能返回数组或对象。扩展运算符提供了解决这个问题的一种变通方法。
js
var dataFields = readDateFields(database);
var d = new Date(...dataFields);
字符串
扩展运算符还可以将字符串转为真正的数组。
js
[...'hello'] // ['h', 'e', 'l', 'l', 'o']
上面的写法有一个重要的好处,那就是能够正确地识别32位的Unicode字符。
js
'x\uD83D\uDE80y'.length // 4
[...'x\uD83D\uDE80y'].length // 3
上面的第一种写法,JavaScript会将32位的Unicode字符识别为2个字符,采用扩展运算符就没有这个问题。凡是涉及操作32位Unicode字符的最好使用扩展运算符改写。
类似数组的对象
任何类似数组的对象都可以用扩展运算符转为真正的数组。
js
var nodeList = document.querySelectorAll('div');
var array = [...nodeList];
Map和Set结构,Generator函数
扩展运算符内部调用的是数据结构的Interator接口
,因此只要具有Interator接口
的对象,都可以使用扩展运算符,比如Map结构
。
js
let map = new Map([
[1, 'one'],
[2, 'two'],
[3, 'three'],
]);
let arr = [...map.keys()]; // [1, 2, 3]
Generator函数
运行后返回一个遍历器对象,因此也可以使用扩展运算符。
js
var go = function* () {
yield 1;
yield 2;
yield 3;
}
[...go()] // [1, 2, 3]
如果没有Interator接口
的对象使用扩展运算符,将会报错。
js
var obj = {a: 1, b: 2};
let arr = [...obj]; // TypeError: Cannot spread non-interable object
name属性
函数的name属性
返回该函数的函数名。
js
function foo () {}
foo.name // 'foo'
这个属性早就被浏览器广泛支持,但是直到ES6才写入了标准。
需要注意的是,ES6对这个属性的行为作了一些修改。如果将一个匿名函数赋值给一个变量,ES5的name属性
会返回空字符串,而ES6的name属性
会返回实际的函数名。
js
var fun1 = function () {};
// ES5
func1.name // ''
// ES6
func1.name // 'func1'
上面的代码中,变量func1
等于一个匿名函数,ES5和ES6的name属性
返回的值不一样。
如果将一个具名函数赋值给一个变量,则ES5和ES6的name属性
都返回这个具名函数原本的名字
js
const bar = function baz () {};
// ES5
bar.name // 'baz'
// ES6
bar.name // 'baz'
Function构造函数
返回的函数实例,name属性
的值为“anonymous”。
js
(new Function).name // 'anonymous'
// bind返回的函数,name属性会加上“bound”前缀
function foo () {};
foo.bind({}).name // 'bound foo'
(function () {}).bind({}).name // 'bound '
以上,摘抄自阮一峰老师的《ES6标准入门》