函数的扩展(上)

前端开发
2018年10月19日
324

函数的默认值

基本用法

在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的写法还有两个好处:首先,阅读代码的人可以立刻意识到哪些参数是可以省略的,不用查看函数体或文档;其次,有利于将来的代码优化,即使未来的版本彻底拿掉这个参数,也不会导致以前的代码无法运行。

参数变量是默认声明的,所以不能用letconst再次声明。

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的参数是一个对象时,变量xy才会通过解构赋值而生成。如果函数foo调用时参数不是对象,变量xy就不会生成,从而报错。

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标准入门》