默认值
想必在写函数时,都会有一个困惑:要是在这个函数调用的时候,参数没有传入时,该怎么办?设置默认值。对,所以大部分的函数都会有以下的代码。
function func(param1, param2) {
param1 = param1 || {/*...*/}
/* ... */
}
以上代码相信绝大多数的 javascript
开发者都写过,乍一看是没啥大问题的,但是如果传入的值是 false
呢?那么 param1
就会被替换成默认值。为了避免这个问题就应该将代码改一改:
if( typeof a === 'undefined' ){
a = {/* ... */}
}
这就大大增加了代码量,而且一个痛点莫名其妙的增加了:增加了代码的不可读性。
在 ES6
中允许为函数设置默认值,而且定义明确,代码的可读性大大增加。
function log(x, y = 'World') {
console.log(x, y);
}
log('Hello'); // Hello World
log('Hello', 'China'); // Hello China
log('Hello', ''); // Hello
这让函数体内(也就是 {}
内)的代码专注于函数实现,而不用去处理默认值的逻辑。
当然在构造函数中也可以使用。
function Point(x = 0, y = 0) {
this.x = x;
this.y = y;
}
var p = new Point();
p // { x: 0, y: 0 }
解构赋值与默认值结合使用
如果不理解解构赋值的含义,可以查看。不多说,先上个例子:
function foo({ x, y = 5 }) {
console.log(x, y);
}
foo({}); // undefined, 5
foo({x: 1}); // 1, 5
foo({x: 1, y: 2}); // 1, 2
foo(); // TypeError: Cannot read property 'x' of undefined
上述代码使用的是结构赋值时的默认值,并没有使用函数传参的默认值。
在明白了上面这个例子后,在来看这个例子:
function foo({x, y = 5} = {x: 4, y: 7}) {
console.log(x, y);
}
foo({}); // undefined, 5
foo({x: 1}); // 1, 5
foo({x: 1, y: 2}); // 1, 2
foo(); // 4, 7
唯一的不同就是,在函数参数的后面多了 = {x: 1, y: 2}
,造成的结果就是在 foo
函数调用的第 4
种方式上的结果。很明显,第四种方式,并没有传递参数,就使用了我们规定好的默认值。
那么现在我们就可以用两种方式来规定一个值的默认值了,假设一个场景,我们的函数需要一个含有 x
和 y
的对象,当有参数传入时, y
的默认值是 5
,而没有参数传入时,默认值为 {x: 4, y: 7}
,就可以用上面的函数实现,避免了很多不必要的逻辑判断代码去为函数参数设置默认值。
应用
利用默认值来指定一个参数不可省略:
function throwIfMissing() {
throw new Error('Missing parameter');
}
function foo(mustBeProvided = throwIfMissing()) {
return mustBeProvided;
}
foo();
// Error: Missing parameter
上述的代码如果没有 foo
传递实际的参数,就会去调用默认的函数,就会抛出一个错误。对上面的 throwIfMissing
函数进行进一步的改造,就能抛出更具体的错误信息了。
function throwIfMissing(argName = '') {
throw new Error('Missing '+ argName +' parameter');
}
function foo(arg1 = throwIfMissing('arg1')) {
return arg1;
}
foo();
// Error: Missing arg1 parameter
函数参数的位置
通常情况下,有默认值的参数应该位于函数的参数列表的尾部。因为如果非尾部的参数有默认值的话,该值后面的参数又是必须传递参数的,导致的结果就是这个参数设不设置默认值,都是需要传递参数的。
// 例一
function f(x = 1, y) {
return [x, y];
}
f(); // [1, undefined]
f(2); // [2, undefined])
f(, 1); // 报错
f(undefined, 1); // [1, 1]
// 例二
function f(x, y = 5, z) {
return [x, y, z];
}
f(); // [undefined, 5, undefined]
f(1); // [1, 5, undefined]
f(1, ,2); // 报错
f(1, undefined, 2); // [1, 5, 2]
函数的 length 属性
指定了默认值以后,函数的 length
属性,将返回没有指定默认值的参数个数。
(function (a) {}).length; // 1
(function (a = 5) {}).length; // 0
(function (a, b, c = 5) {}).length; // 2
如果设置了默认值的参数不是尾参数,那之后的参数也不会记录到 length
中
(function (a = 0, b, c) {}).length; // 0
(function (a, b = 1, c) {}).length; // 1
函数参数的作用域
一旦设置了参数的默认值,函数进行声明初始化时,参数会形成一个单独的作用域(context)。等到初始化结束,这个作用域就会消失。
let x = 1;
function func(y = x) {
let x = 2;
console.log(y);
}
f(); // 1
上面的代码中,函数调用时,参数 y = x
形成一个单独的作用域(这个处在参数初始化阶段,而函数的作用域还没出现)。这个作用域里面,变量 x
本身没有定义,所以指向外层的全局变量 x
。
想象一下,如果这个单独的作用域没出现的话,而是函数的作用域,那么其实上面的代码大概可以等价于:
let x = 1;
function func() {
let y = x;
let x = 2;
console.log(y);
}
f(); // Uncaught ReferenceError: x is not defined
那么这个函数是执行不下去的。由于 let
变量不存在变量提升,因此会报错。
当然如果参数的默认值是一个函数,该函数的作用域同样遵守这个规则。
let foo = 'outer';
function bar(func = () => foo) {
let foo = 'inner';
console.log(func());
}
bar(); // outer
REST 参数
ES6
引入了 REST
参数(即 "...参数名" ),用于获取函数的多余参数,在一定情况下减少了对 arguments
对象的使用了。
function add(...values) {
let sum = 0;
for (var val of values) {
sum += val;
}
return sum;
}
add(2, 5, 3); // 10
由于 arguments
是函数体内的一个变量,所以必须写在函数体内(也就是 {}
内),而 REST
参数写在函数参数里,使用起来便捷。以下就是使用 arguments
和 REST
参数实现函数参数排序两种不同的方式。
// arguments变量的写法,必须在函数体内才能获取。
function sortNumbers() {
return Array.prototype.slice.call(arguments).sort();
}
// rest 参数的写法。
const sortNumbers = (...numbers) => numbers.sort();
很明显,结合使用 ES6
的写法,使用 REST
参数方式更加的语义化和便捷。
REST
参数表示剩下的参数,所以在 REST
参数前是可以有别的参数存在的,但是在 REST
参数之后是不可以在有函数参数的。并且 REST
参数代表的是一个数组,故所有的数组的方法都使用于这个变量。
function push(array, ...items) {
items.forEach(function(item) {
array.push(item);
console.log(item);
});
}
var a = [];
push(a, 1, 2, 3);
// 报错
function f(a, ...b, c) {
// ...
}
扩展运算符
扩展运算符是 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>]
一些使用
function push(array, ...items) {
array.push(...items);
}
function add(x, y) {
return x + y;
}
var numbers = [4, 38];
add(...numbers); // 42
扩展运算符的应用
替代apply方法
// ES5的写法
Math.max.apply(null, [14, 3, 77]);
// ES6的写法
Math.max(...[14, 3, 77]);
// 等同于
Math.max(14, 3, 77);
由于 Math.max
需要一系列单独的数字,而不是一个数字,在 ES5
中只能使用 apply
方法来实现,而在 ES6
中使用扩展运算符就方便的多了。
像数组的 push
方法也使用:
// ES5 的写法
var arr1 = [0, 1, 2];
var arr2 = [3, 4, 5];
Array.prototype.push.apply(arr1, arr2);
// ES6 的写法
var arr1 = [0, 1, 2];
var arr2 = [3, 4, 5];
arr1.push(...arr2);
合并数组
// 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);
// [ 'a', 'b', 'c', 'd', 'e' ]
// ES6 的合并数组
[...arr1, ...arr2, ...arr3];
// [ 'a', 'b', 'c', 'd', 'e' ]
与解构赋值结合
// ES5
a = list[0], rest = list.slice(1);
// ES6
[a, ...rest] = list;
a
取 list
数组中的第一个,而 rest
取剩下的所有。 ES5
和 ES6
实现的区别。同样的 REST
之后不可以用别的解构的值,并且只能在数组中使用。
函数的返回值
在 JavaScript
中若需要返回多值时,只能使用对象或是数组,而扩展运算符本质上是数组,所以也可以用着函数的返回值中。
function test(...args){
return [...args];
}
test(["a", "b", "c"]);
// ["a", "b", "c"]
字符串
将字符串转为真正的数组。
[...'hello'];
// [ "h", "e", "l", "l", "o" ]
Iterator
对象
任何 Iterator
接口的对象,都可以用扩展运算符转为真正的数组。
let map = new Map([
[1, 'one'],
[2, 'two'],
[3, 'three'],
]);
let arr = [...map.keys()]; // [1, 2, 3]
箭头函数
感觉这点时 EMCAScript 6
中极其重要的一点,所以单独会有一节。
总结
主要是函数参数的一些注意点,以及一些新特性,由于箭头函数在 ES6
中应该会成为随手就写的编程方式,所以单独开一篇。