1.函数参数的默认值
1.1 基本用法
ES5函数参数没有默认值,通常这样传默认值
function log(x,y) {
if(typeof y === 'undefined') {
y = 'world';
}
console.log(x,y);
};
log('hello'); // hello world
log('hello',false); //hello false
log('hello',''); //hello
ES6引入了函数默认值,使函数在需要使用默认值的情况下代码更加简洁。
function log(x,y = 'world') {
console.log(x,y);
}
log('hello'); // hello world
log('hello',false); //hello false
log('hello',''); //hello
这里需要注意两点
1)函数参数在函数体内不能用let 或者const再次声明导致重复声明变量
function foo(x = 5) {
let x = 1; // error
const x = 2; // error
}
2)如果函数参数的传的默认值是一个表达式,表达式每次需要重新计算
let x = 99;
function foo(p = x + 1) {
console.log(p);
}
foo() // 100
x = 100;
foo() // 101
另一个例子
function point(x = 0, y = 0) {
this.x = x;
this.y = y;
}
const p = new point(3,4);
console.log(p); //point {x: 3, y: 4}
const q = new point();
console.log(q); //point {x: 0, y: 0}
ES6 引入函数默认值的好处在于:首先,阅读代码的人,可以立刻意识到哪些参数是可以省略的,不用查看函数体或文档;其次,有利于将来的代码优化,即使未来的版本在对外接口中,彻底拿掉这个参数,也不会导致以前的代码无法运行。
1.2 与解构赋值默认值结合使用
code one
function foo ({x, y = 3}) {
console.log(x,y);
}
foo(); //Cannot destructure property `x` of 'undefined' or 'null'.
foo({}); //undefined 3
foo({x:1}) //1 3
foo({x:4,y:5}); //4 5
同样与也可以与数组解构赋值结合使用
code two
function foo ([x,y]=[1,3]) {
console.log(x,y);
}
foo([]); //undefined undefined
foo([3,4]); // 3 4
code one中上只使用了对象的解构赋值默认值,没有使用函数参数的默认值。只有当函数foo的参数是一个对象时,变量x和y才会通过解构赋值生成。如果函数foo调用时没提供参数,变量x和y就不会生成,从而报错。通过提供函数参数的默认值,如何提供函数的参数默认值?只需等于一个空对象即可。
function foo ({x, y = 3} = {}) {
console.log(x,y);
}
foo(); //undefined 3
下面是另一个解构赋值默认值的例子。
function fetch(url,{body = '', method = 'GET', headers = {} }) {
console.log(method);
};
fetch('http://www.baidu.com',{}); //GET
fetch('http://www.baidu.com'); // Cannot destructure property `body` of 'undefined' or 'null'.
给第二个参数传入默认值,这样在给函数传值时可以省略第二个参数而不会报错。
function fetch(url,{body = '', method = 'GET', headers = {} } = {}) {
console.log(method);
};
fetch('http://www.baidu.com'); //GET
考虑下面两种函数参数的区别
//函数参数的默认值是空对象,但是设置了对象解构赋值的默认值;
function fun1({x=0,y=0} = {}) {
console.log ([x,y]);
}
//函数参数的默认值是一个有具体属性的对象,但没有设置对象解构赋值的默认值
function fun2 ({x,y} = {x: 0, y: 0}) {
console.log ([x,y]);
}
//函数没有传参数
fun1(); //[0,0]
fun2(); //[0,0]
//函数传参数且x,y都有值
fun1({x:1, y:3}); //[1, 3]
fun2({x:1, y:3}); //[1, 3]
//x 有值,y 无值的情况
fun1({x:1}); //[1,0]
fun2({x:1}); //[1,undefined]
//函数有参数,但x,y都无值
fun1({}); //[0,0]
fun2({}); //[undefined, undefined]
1.2 有默认值参数的位置
引入函数参数默认值,是为了可以省略参数,所以通常情况下,定义了默认值的参数,应该是函数的尾参数。因为这样比较容易看出来,到底省略了哪些参数。如果非尾部的参数设置默认值,实际上这个参数是没法省略的。
```
function f1(x=1,y) {
console.log([x,y]);
}
f1(2); //[2, undefined]
function f2(y,x=1) {
console.log([[x,y]]);
}
f2(2); //[1,2]
```
所以如果设置参数默认值,应该将参数默认值写在后面,没有默认值的参数写在前面。
1.3函数的length属性
指定了默认值以后,函数的length属性,将返回没有指定默认值的参数个数。也就是说,指定了默认值后,length属性将失真。
function f1(x, y, z = 3) {
console.log(f1.length);
};
f1(); //2
function f2(x=1, y=2, z = 3) {
console.log(f2.length);
};
f2(); //0
//如果函数的参数有默认值且不是尾参数,那么含有默认值的参数后面的参数不计入length
function f3(x=1, y=2, z) {
console.log(f3.length);
};
f3(); //0
1.4 作用域
一旦设置了参数的默认值,函数进行声明初始化时,参数会形成一个单独的作用域(context)。等到初始化结束,这个作用域就会消失。这种语法行为,在不设置参数默认值时,是不会出现的。
var x = 1;
function f(x,y = x) {
console.log(y);
}
f(2); //2
上面代码中,参数y的默认值等于变量x。调用函数f时,参数形成一个单独的作用域。在这个作用域里面,默认值变量x指向第一个参数x,而不是全局变量x,所以输出是2。
let x = 1;
function f(y=x) {
let x = 2;
console.log(y);
}
f(); //1
f(3); //3, y值为3,默认值失效
上面代码中,函数f调用时,参数y = x形成一个单独的作用域。这个作用域里面,变量x本身没有定义,所以指向外层的全局变量x。函数调用时,函数体内部的局部变量x影响不到默认值变量x。
这里特别说明一点:在ES6中 let x = x;无论在声明变量,还是在函数给定默认参数都会报错。
var x = x; //不报错,x为undefined
let x = x; //报错,x is not defined
再看
let x = 3;
function f(x = x) {
console.log(x + 1);
};
f(); // ReferenceError: x is not defined
f(3); //4
所以在项目中尽量避免let x = x;这种写法。
函数的参数也可以是一个默认值,同样遵守作用域规则,这种情况在实际中很少用到,这里就不作讨论,想了解更多细节请参考ES6入门。
1.5 应用
利用参数默认值,可以指定某一个参数不得省略,如果省略就抛出一个错误。
function throwIfMissing() {
throw new Error('Missing parameter');
}
function foo(mustBeProvided = throwIfMissing()) {
console.log(mustBeProvided+1);
}
foo(); // Missing parameter
foo(1); //2
2.rest参数
ES6 引入 rest 参数(形式为…变量名),用于获取函数的多余参数,这样就不需要使用arguments对象了。rest 参数搭配的变量是一个数组,该变量将多余的参数放入数组中。
function add(...values) {
let sum = 0;
for(let item of values) {
sum += item ;
}
console.log(sum);
}
add(1,2,3);
上面代码的add函数是一个求和函数,利用 rest 参数,可以向该函数传入任意数目的参数。
注意,这里let item
不能写成let var
,ES6入门中是let var of values
会报错:Unexpected token var
。var是一个关键词,不能用作变量名。
rest 参数代替arguments变量
// arguments变量的写法
function sortNumbers() {
return Array.prototype.slice.call(arguments).sort();
}
// rest参数的写法
const sortNumbers = (...numbers) => numbers.sort();
arguments对象不是数组,而是一个类似数组的对象。所以为了使用数组的方法,必须使用Array.prototype.slice.call先将其转为数组。rest 参数就不存在这个问题,它就是一个真正的数组,数组特有的方法都可以使用。
rest 参数之后不能再有其他参数(即只能是最后一个参数),否则会报错。
function f1(...items,a) {
return;
}
f1(); //Rest parameter must be last formal parameter
函数的length属性不包括rest参数
function f1(a,...items) {
return;
}
function f2(a=3,...items) {
return;
}
console.log(f1.length); //1
console.log(f2.length); //0
3.严格模式
从 ES5 开始,函数内部可以设定为严格模式。
function doSomething(a, b) {
'use strict';
// code
}
ES2016 做了一点修改,规定只要函数参数使用了默认值、解构赋值、或者扩展运算符,那么函数内部就不能显式设定为严格模式,否则会报错。
// 报错
function doSomething(a, b = a) {
'use strict';
// code
}
// 报错
const doSomething = function ({a, b}) {
'use strict';
// code
};
// 报错
const doSomething = (...a) => {
'use strict';
// code
};
const obj = {
// 报错
doSomething({a, b}) {
'use strict';
// code
}
};
两种方法可以规避这种限制。第一种是设定全局性的严格模式,这是合法的。
1)设定全局性的严格模式
'use strict';
function doSomething(a, b = a) {
// code
}
2)函数包在一个无参数的立即执行函数里面
const doSomething = (function () {
'use strict';
return function(value = 42) {
return value;
};
}());
4.name属性
函数的name属性,返回该函数的函数名。
function foo() {}
foo.name // "foo"
如果将一个匿名函数赋值给一个变量,ES5 的name属性,会返回空字符串,而 ES6 的name属性会返回实际的函数名。
var f = function () {};
// ES5
f.name // ""
// ES6
f.name // "f"
如果将一个具名函数赋值给一个变量,则 ES5 和 ES6 的name属性都返回这个具名函数原本的名字。
const bar = function baz() {};
// ES5
bar.name // "baz"
// ES6
bar.name // "baz"
Function构造函数返回的函数实例,name属性的值为anonymous。
function foo() {};
foo.bind({}).name // "bound foo"
(function(){}).bind({}).name // "bound "
5.箭头函数
5.1 基本用法
ES6 允许使用“箭头”(=>)定义函数。一个参数可以省略圆括号
var f = v => v;
<=>
var f = function(v) {
return v;
}
如果箭头函数不需要参数或需要多个参数,就使用一个圆括号代表参数部分。
//一个参数
var f = () => 6;
<=>
var f = function() {
return 6;
}
console.log(f()); //6
----------
//多个参数,参数用圆括号包括起来
var f = (num1,num2) => num1 + num2;
<=>
var f = function(num1, num2) {
return num1 + num2;
}
console.log
如果箭头函数的代码块部分多于一条语句,就要使用大括号将它们括起来,并且使用return语句返回。
var f = (num1,num2) => {
let num3 = num1 + 1;
let num4 = num2 + 2;;
return [num3,num4];
}
console.log(f(1,2)); //[2,4]
如果箭头函数返回一个对象,需要在对象外面加上圆括号否则会报错。
var f = (x,y) => ({x:1,y:2});
console.log(f()); //{x: 1, y: 2}
如果箭头函数只有一行语句,且不需要返回值,可以采用下面的写法,就不用写大括号了。
let fn = () => void doesNotReturn();
箭头函数可以与变量解构结合使用。
const full = ({first,last}) => first + ' ' + last;
<=>
function full(person) {
return person.first + ' ' + person.last;
}
full({first:'harry',last:'potter'}) //harry potter
箭头函数使得表达更加简洁。
const isEven = n => n % 2 == 0;
const square = n => n * n;
console.log(isEven(3)); //false
console.log(square(2)); //4
箭头函数的一个用处是简化回调函数。
// 正常函数写法
[1,2,3].map(function (x) {
return x * x;
});
// 箭头函数写法
[1,2,3].map(x => x * x);
另一个例子是
// 正常函数写法
var result = values.sort(function (a, b) {
return a - b;
});
// 箭头函数写法
var result = values.sort((a, b) => a - b);
rest 参数与箭头函数结合
const numbers = (...nums) => nums;
numbers(1, 2, 3, 4, 5)
// [1,2,3,4,5]
const headAndTail = (head, ...tail) => [head, tail];
headAndTail(1, 2, 3, 4, 5)
// [1,[2,3,4,5]]
5.2 使用注意点
(1)函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象。
(2)不可以当作构造函数,也就是说,不可以使用new命令,否则会抛出一个错误。
(3)不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。
(4)不可以使用yield命令,因此箭头函数不能用作 Generator 函数。
上面四点中,第一点尤其值得注意。this对象的指向是可变的,但是在箭头函数中,它是固定的。
this在JS中是一个比较复杂的问题,我会在另一篇文章中介绍传统函数的this和箭头函数this的区别。