函数的扩展
1.函数参数的默认值
基本用法
ES6允许为函数参数设置默认值,直接写在参数定义后面。
function log(x,y='World'){
console.log(x,y);
}
log('Hello') // Hello World
log('Hello', 'China') // Hello China
log('Hello', '') // Hello
此方法除了简洁,有两个好处。一是,方便阅读代码,不用看函数体,意识到哪些参数可以省略。二是,有利于优化代码,即使拿掉参数,也不会导致代码无法运行。
函数参数变量是默认声明的,不能用let或const再次声明。函数不能有同名参数。参数默认值不是传值,每次都重新计算表达式的值。
function foo(x=5){
let x = 1; //error
const x = 2; //error
}
//不报错
function foo(x,x,y){
//...
}
//报错
function foo(x,x,y=1){
//...
}
log('Hello') // Hello World
log('Hello', 'China') // Hello China
log('Hello', '') // Hello
与解构赋值默认值结合使用
有两种情况,一是对象解构赋值默认值,二是函数参数的默认值。
function fetch(url, { body = '', method = 'GET', headers = {} } = {}) {
console.log(method);
}
fetch('http://example.com')
// "GET"
函数fetch没有第二个参数时,函数参数的默认值就会生效,然后才是解构赋值的默认值生效,变量method才会取到默认值GET,出现了双重默认值。
练习:
// 写法一
function m1({x = 0, y = 0} = {}) {
return [x, y];
}
// 写法二
function m2({x, y} = { x: 0, y: 0 }) {
return [x, y];
}
上面两种写法都对函数的参数设定了默认值,区别是写法一函数参数的默认值是空对象,但是设置了对象解构赋值(x,y)的默认值;写法二函数参数的默认值是一个有具体属性的对象,但是没有设置对象解构赋值(x,y)的默认值。
// 函数没有参数的情况
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]
参数默认值的位置
一般情况下,定义了默认值的参数应该在尾部,因为赋值时容易看出省略哪个参数,如果在非尾部,无法省略,除非显示的输入undefined,null没有这个效果。
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属性返回没有指定默认值的参数个数。该属性的含义是该函数预期传入的参数的个数,指定默认值后就不包括这个参数。如果设置了默认值的参数不是尾参数,那么length属性不计入后面的参数。
(function (a) {}).length // 1
(function (a = 5) {}).length // 0
(function (a, b, c = 5) {}).length // 2
(function (a = 0, b, c) {}).length // 0
(function (a, b = 1, c) {}).length // 1
作用域
设置参数的默认值后,参数会形成一个单独的作用域,等到初始化结束,作用域消失。
例1:
var x = 1;
function f(x, y = x) {
console.log(y);
}
f(2) // 2
let x = 1;
function f(y = x) { //相当于let y=x
let x = 2;
console.log(y);
}
f() // 1 如果没有let x = 1;会报错
例2:
var x = 1;
function foo(x, y = function() { x = 2; }) {
var x = 3;
y();
console.log(x);
}
foo() // 3
x // 1
var x = 1;
function foo(x, y = function() { x = 2; }) {
x = 3;
y();
console.log(x);
}
foo() // 2
x // 1
上面代码中,函数foo的参数形成一个单独的作用域,声明了变量x和y,y的默认值是一个匿名函数,内部变量x指向第一个参数x。函数foo内部又声明了一个内部变量x,该变量与第一个参数x不在一个作用域,执行y后内部变量和全局变量x的值都没变。去掉var后,函数foo的内部变量x就指向第一个参数x,但全局变量依然不受影响。
应用
利用参数默认值,可以指定某一个参数不得省略,如果省略就抛出一个错误。也可以将参数默认值设为undefined,表明可省略。
function throwIfMissing() { //不可省略
throw new Error('Missing parameter');
}
function foo(mustBeProvided = throwIfMissing()) {
return mustBeProvided;
}
foo() //如果无参数,就会调用默认值 throwIfMissing函数,抛出错误
// Error: Missing parameter
function foo(optional = undefined) { ··· } //可省略
2.rest函数
形式为…变量名,用于获取函数多余的参数,rest参数搭配的变量是一个数组,将多余的参数放入数组中。rest参数之后不能再有其他参数,否则报错,不包括length属性。
function push(array, ...items) {
items.forEach(function(item) {
array.push(item);
console.log(item);
});
}
var a = [];
push(a, 1, 2, 3)
3.严格模式
ES2016中规定只要函数参数使用了默认值、解构赋值、或扩展运算符,那么函数内部就不能显式的设定为严格模式,否则报错。因为函数内部的严格模式同时适用于函数体和函数参数,但参数先于函数体执行。有两种方法可以规避这种限制,第一种是设定全局性严格模式,二是把函数包在一个无参数的立即执行函数里面。
1.
'use strict';
function doSomething(a, b = a) {
// code
}
2.
const doSomething = (function () {
'use strict';
return function(value = 42) {
return value;
};
}());
4.name属性
函数name属性,返回该函数的函数名。如果将一个匿名函数赋给一个变量,name属性也会返回实际函数名。Function构造函数返回的函数实例,name属性的值为anonymous。bind返回的函数,name属性值会加上bound前缀。
(new Function).name //"anonymous"
function foo() {};
foo.bind({}).name //"bound foo"
(function(){}).bind({}).name //"bound"
5.箭头函数
基本用法
ES6允许使用“箭头”定义函数。如果箭头函数不需要参数或多个参数,就使用圆括号代替参数部分。如果代码块部分多于一条语句,就要用大括号将他们括起来,并用return语句返回。如果返回一个对象,必须在对象外面加上括号,否则会报错。
var f=v=>v;
//等同于
var f=function (v){
return v;
};
箭头函数可以使表达式更简洁
const full = ({ first, last }) => first + ' ' + last;
// 等同于
function full(person) {
return person.first + ' ' + person.last;
}
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]]
使用注意
(1)函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象。
(2)不可以当作构造函数,也就是说,不可以使用new命令,否则会抛出一个错误。
(3)不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。
(4)不可以使用yield命令,因此箭头函数不能用作 Generator 函数。
不适用场合
因为箭头函数使this从“动态”变成“静态”,所以有两种情况下,不能使用箭头函数。
(1)定义对象的方法,且该方法内部包括this。
(2)需要动态this的时候,也不应使用箭头函数。
箭头函数的嵌套
箭头函数内允许嵌套,可能可读性存在问题,可以采用下面的写法。
const plus1 = a => a + 1;
const mult2 = a => a * 2;
mult2(plus1(5))
// 12
6.尾调用优化(某个函数的最后一步是调用另一个函数)
注意:这几种情况不属于尾调用
// 情况一
function f(x){
let y = g(x);
return y;
}
// 情况二
function f(x){
return g(x) + 1;
}
// 情况三
function f(x){
g(x);
}
//情况三同下
function f(x){
g(x);
return undefined;
}
尾调用优化(只保留内层函数的调用帧)
只有不再用到外层函数的内部变量,内层函数的调用帧才会取代外层函数的调用帧,否则就无法进行“尾调用优化”。
function addOne(a){
var one = 1;
function inner(b){
return b + one;
}
return inner(a);
}
尾递归
函数调用自身,称为递归。如果尾调用自身,就称为尾递归。防止“栈溢出”和超时。
function factorial(n) {
if (n === 1) return 1;
return n * factorial(n - 1);
}
factorial(5) // 120
上面的代码计算n的阶乘,复杂度 O(n)。
如果改成尾递归,复杂度O(1)。
递归函数的改写
(1)在尾递归函数之外,再提供一个正常形式的函数。
function tailFactorial(n, total) {
if (n === 1) return total;
return tailFactorial(n - 1, n * total);
}
function factorial(n) {
return tailFactorial(n, 1);
}
factorial(5) // 120
(2)采用ES6默认值
function factorial(n, total = 1) {
if (n === 1) return total;
return factorial(n - 1, n * total);
}
factorial(5) // 120
7.函数参数的尾逗号
允许函数的最后一个参数有尾逗号。
8.Function.prototype.toString()
明确要求返回一模一样的原始代码
function /* foo comment */ foo () {}
foo.toString()
// "function /* foo comment */ foo () {}"
9.catch 命令的参数省略
try {
// ...
} catch {
// ...
}
允许省略参数