ES6中函数内容的新特性
参数设定默认值
ES6允许为函数的参数设置默认值,即直接写在参数定义的后面。
function log(x,y='world'){
console.log(x,y);
}
log('hello') //hello world
log('hello','yivi') //hello yivi
log('hello','') //hello
参数变量是默认声明的,因此不能再用let或const再次声明,也不能用同名的参数。
function foo(x=5){
let x=5 //error
const x=5 //error
}
与解构赋值结合使用
参数默认值可以与解构赋值的默认值结合使用。
function foo({
x,y=5}){
console.log(x,y)
}
foo({
}) //undefined,5
foo() //error: cannot read property 'x' of undefined
foo({
x:1}) // 1,5
foo({
x:1,y:2}) //1,2
来看看下面两种形式:
function f1({
x,y} = {
x:0,y:0}){
return [x,y];
}
function f2({
x=1,y=1} = {
}){
return [x,y];
}
f1() //[0,0]
f2() //[0,0]
f1({
x:3}) //[3,undefined]
f2({
x:3}) //[3,1]
f1({
}) //[undefined,undefined]
f2({
}) //[1,1]
参数默认值的位置
如果定义了默认值的参数在函数的最尾部,则可以省略;如果非尾部的参数设置默认值,实际上这个参数无法省略,必须以undefined填充。
function foo(x,y=1,z){
return [x,y,z]
}
foo(); //[undefined,1,undefined]
foo(1,2,3) //[1,2,3]
foo(1,,2) //error
foo(1,undefined,2) //[1,1,2]
失真的length
指定默认之后,函数的length方法将只返回没有默认值的参数个数,也就是length将失真。这是因为length的本意是该函数预期传入参数的个数。
(function(x,y,z=1){
}).length = 2 //参数个数为3,但返回2
作用域
一旦设置了默认值,函数声明初始化的时候,参数就会形成一个单独的作用域,等到初始化结束,作用域就会失效。
var x=1;
function foo(x,y=x){
return [x,y]
}
f(2) //[2,2]
以上例子中,由于函数设定了默认值,在函数内部会形成单独的作用域,因此函数中的y=x
中的x指的是参数的x,而不是全局变量x。
rest参数
ES6引入了rest参数...rest
来获取函数多余的参数,这样就无需引用arguments对象了。rest参数搭配的变量是一个数组,该变量将多余的参数放入其中。
function add(...num){
let sum = 0;
for(var item in num){
sum += item;
}
return sum;
}
add(1,2,3,4,5) //15
注意:函数的length属性并不包括rest 的参数个数
尾调用优化
啥叫尾调用啊?
尾调用,顾名思义,就是指某个函数的最后一步是调用另一个函数。
function f(x){
return g(x)
}
//以下都不属于尾调用
function f(x){
let y = g(x);
return y;
}
function f(x){
return g(x) + 1;
}
function f(x){
g(x);
}
尾调用的标志是,必须是在最后调用函数,而不是操作调用后的结果。
尾调用优化
我们知道,函数调用会在内存形成一个“调用记录”, 又称“调用帧”(call frame),保存调用位置和内部变量等信息。如果在函数 A 的内部调用函数 B,那么在 A 的调用帧上方还会形成一 个 B 的调用帧。等到 B 运行结束,将结果返回到 A, B 的调用帧才会消失。 如果函数 B 内部还调用函数 c,那就还有一个 c 的调用帧,以此类推。所有的调用帧就形成一个“调用栈”(call stack)。
尾调用由于是函数的最后一步操作,所以不需要保留外层函数的调用帧,因为调用位置、 内部变量等信息都不会再用到了,直接用内层函数的调用帧取代外层函数的即可。
function f(){
let x = 1;
let y = 2;
return g(x+y);
}
//等同于
g(3);
上面的代码中,如果不是尾调用,函数f就需要保存变量x,y的值、g的调用信息等。但由于尾调用,函数f就结束了,所以内存中可以删除f的调用记录,只保留g的调用帧。这就是尾调用优化,可以大量节约内存。
尾递归
递归是一种非常消耗内存的方法,因为其需要保存大量的调用帧,容易发生内存溢出等错误,但尾递归就很好的解决了这个问题,因为他永远只占用一个调用帧。
// 阶乘函数
function factorial(n){
if(n == 1){
return 1;
}else{
return n*factorial(n-1);
}
}
// 我们尾递归真的太厉害了(老财富密码了)
// 普通递归,不好用!尾递归,好用!
function factorial(n,total){
if(n == 1){
return total
}else{
return factorial(n-1,n*total);
}
}
//fibonacci数列
function fibonacci(n){
if(n <= 1){
return 1
}else{
return fibonacci(n-1) + fibonacci(n-2);
}
}
// 普通递归,不好用!尾递归,好用!
function fibonacci(n,ac = 1,ac = 1){
if(n <= 1){
return ac2;
}else{
return fibonacci(n-1,ac2,ac1+ac2);
}
}
尾递归改写
上面的代码似乎不是很合逻辑,为什么我算一个阶乘还要传一个total进去呢,难道就不能直接给我结果吗?答案是可以的。别忘了,ES6给我们提供了默认值操作!
function factorial(n,total = 1){
if(n == 1){
return total
}else{
return factorial(n-1,n*total);
}
}
factorial(5) //120
还有一种尾调用的方法,就是在尾递归函数外再提供一个正常形式的函数来进行尾递归:
function tailFactorial(n,total){
if(n == 1){
return total
}else{
return factorial(n-1,n*total);
}
}
function factorial(n){
return tailFactorial(n,1);
}
factorial(5); //120
这其实是一种柯里化思维。等会儿!啥叫柯里化???
柯里化
函数编程中有一个重要的概念,叫做柯里化,意思是将多参数的函数转化成单参数的形式。
function curry(fn,n){
return function(m){
return fn.call(this,m,n);
} //返回一个可执行函数
}
function tailFactorial(n,total){
if(n == 1){
return total
}else{
return factorial(n-1,n*total);
}
}
const factorial = curry(tailFactorial,1);
factorial(5) //120