ES6 针对新的语法特性(解构、参数默认值、箭头语句、块级作用域let),对于函数的属性、参数、作用域进行了扩展,并对递归调导致内存栈溢出用进行了优化。 同时ES6规定,只要函数参数使用了默认值、解构赋值、扩展运算符,函数内部都不可以使用严格模式(ES5可以),否则会报错;
1. 函数参数默认值
ES6之前,函数定义/声名时不能指定默认值;ES6可以在函数定义时进行初始化;同时有以下几点需要注意:
- 参数设置默认值时,参数直接写在默认值后面,如:
function(x,y=1){return x+y}
; - 函数传递参数时,不允许有同名参数,如:
function(x,x,y=true){return x};//报错:参数重复命名
; - 函数传递参数时,不能在函数体内部用
let
、const
再次声明,如:function(x){let x=false;}//报错
; rest
参数,用于获取多余的参数,可替代arguments
,rest参数之后不可再有其他参数,否则会报错:
function sum(...values){
let sum=0;
for(let val of values){
sum+=val;
}
return sum;
}
sum(1,2,3,4);//10
- 传递默认参数时,默认参数需放在无默认值参数后面,如果不在最后,需要显示传入
undefiend
function fn(x,y=5,z){
return [x,y,z];
}
fn(); // [undefined,5,undeifined]
fn(1); //[1,5,undefiend]
fn(1,2);// [1,2,undefined]
fn(1,,3);//报错
fn(1,undefined,3);//[1,5,3]
- 传参与结构赋值结合:
// 写法一
function fn1({x=0,y=0}={}){
return [x,y];
}
//写法二
function fn2({x,y}={x=0,y=0}){
return [x,y];
}
// ① 函数无参数
fn1(); // [0,0]
fn2(); // [0,0]
// ② x、y都无值情况;
fn1({}); // [0,0]
fn2({}); // [undefined,undefined]
// ③x有值,y无值;
fn1({x:3}}); // [3,0]
fn2({x:3}); // [3,undefiend]
2.函数作用域
函数参数设置默认值后,函数初始化时,参数会形成一个单独的作用域;等到初始化完成后,这个作用域消失;不设置默认值时,该机制不生效;
// ① 函数参数独立作用域机制赋值:
let x=1;
function fn1(x,y=x){console.log(y)};
fn1(2);//2
//② 函数参数独立作用域+作用域链
let a=1;
function fn2(b=a){
let a=2;
console.log(b);
};
fn2();//1
// ③ 函数参数单独作用域+作用域链
function fn3(e=d){
console.log(e)
}
fn3();// ReferenceError:e is not defiined
// ④ 参数赋初值+let 暂时性死区 =>报错
let j=2;
function fn4(j=j){
console.log(j)
}
fn4();//ReferenceError:j is not defined
3. 函数新增属性
- name:返回函数名称:
function fn(){};fn.name;//fn
①将匿名函数赋值给变量,ES5中该属性返回空字符串
""
,而ES6返回实际函数名;②Function构造函数返回函数实例,name属性返回anoymous
,bind返回的函数,name属性加上bound前缀:
(new Function).name; //"anoymous"
function fn(){};
fn.bind({}).name; // "bound fn"
(function(){}).bind({}).name; // "bound"
- length:返回函数没有指定默认值的参数的个数,本质是返回该函数预期传入的参数个数,即指定默认值的参数,length属性将失效(忽略该参数)、同理rest参数页不会计入length属性;
(function(a){}).length;//1
(function(a=1){}).length;//0
(fucntion(a,b,c=5){}).length;//2
(function(..args){}).length;//0
(function(a,b=1,c,d){}).length;//1
4. 箭头函数——`()=>
箭头函数不存在单独的作用域,即不存在单独的this、arguments、super、new.target;这四个对象分别指向外层函数对应的变量; 箭头函数中this指向固化并不是箭头函数内部有绑定this的机制,相反,本质原因是箭头函数根本没有自己的this,导致箭头函数内部的this就是外层代码块的this,也正是因为没有this,即没有独自的作用域,所以不能用作构造函数;
1.注意事项
- 不存在
this
:箭头函数体内的this对象指向定义时所在对象,而非调用时所在对象; - 不可做构造函数:箭头函数不可用实例化,即不可用new创建,否则报错;
- 不可使用
arguments
对象:arguments对象在箭头函数体内部存在,可用rest参数替代; - 不可使用
yeild
命令:即箭头函数不能用作Generator函数;
function Timer(){
this.s1=0;
this.s2=0;
setInterval(()=>this.s1++,1000);
setInterval(function(){this.s2++},1000);
}
var timer=new Timer();
setTimeout(()=>console.log(‘s1:‘,timer.s1),3100); // s1:3
setTimeout(()=>console.log(‘s2:‘,timer.s2),3100); // s2:0
2. 箭头函数使用场景
- 简化回调函数;
- 绑定this:自动绑定this到外层函数对象,减少显示绑定this对象写法(call、apply);
- 部署管道机制(pipeline):即前一个函数的输出时后一个函数的输入;
const plus=a=>a+1;
const mult=b=>b*2;
mult(plus(5));//12
const pipeline=((...fns)=>val=>fns.reduce((a,b)=>b(a),val));
const addThenMult=pipeline(plus,mult);
addThenMult(5);//12
5.函数优化
四个概念:调用帧、尾调用函数、尾递归函数、蹦床函数、函数柯里化(currying)
调用帧:
函数调用时会在内存中形成一个调用记录(调用帧-call frame),保存调用位置和内部变量等息息;如果A函数的内部调用B函数,那么在A的调用帧上方会形成一个B函数的调用帧,待B函数运行结束,将结果返回A时,B函数的调用帧占用的内存才会被回收消失;如果B函数内部还调用了C函数,就会生成一个C函数的调用帧,以此类推,所有的调用帧形成一个调用栈(call stack); 递归函数非常消耗内存,因为需要同时保存数量巨大的调用帧,容易造成“栈溢出”错误(stack overflow);
尾调用函数: 尾调用函数(Tail call):即某个函数的最后一步是返回调用另一个函数,如:
function f(x){return g(x)}
;尾调用不一定出现在函数的尾部,但一定是最后一步操作,以下三种均不属于尾调用:
function fn1(){return g()+1;} // 调用后还有其他操作
function fn2(){let res=g();return res;} // 不是最后一步操作
function fn3(){g()} // 返回 undeifend
- 尾递归函数: 尾调用函数的调用函数为函数自身时,成为尾递归函数;
// ① ES6尾递归优化案例:阶乘
function fn1(n){
if(n===1) retrun 1;
return n*fn(n-1);
}
fn1(5; // 120
funcion fn1Optimize(n,tatal){
if(n===1)return 1;
return fn1Optimize(n-1,n*total);
}
fn1Optimize(5,1); //120
// ② ES6尾递归优化案例: 斐波那数列
function fibonacci(n){
if(n<=1) return 1;
return fibonacci(n-1)+fibonacci(n-2);
}
fibonacci(10); //89
fibonacci(100); //堆栈溢出
fibonacci(1000); // 堆栈溢出
function fibonacciOpt(n,a1=1,a2=1){
if(n<=1) return a2;
return fibonacciOpt(n-1,a2,a1+a2)
}
fibonacciOpt(10); //89
fibonacciOpt(100); // 573147844013817200000
fibonacciOpt(1000); // 49 7.0330367711422765e+208
fibonacciOpt(10000);// Infinity 或 Uncaught RangeError: Maximum call stack size exceeded
- 蹦床函数(trampoline):可以将尾递归转换为循环执行;进而避免递归执行,消除调用栈溢出,蹦床函数的实现方法实例;
function trampoline(fn){
while(f&&f instanceof Function){fn=fn()};
return fn;
}
- 函数柯里化:函数式编程中,函数柯里化指 将多参数的函数转换成单个参数形式的函数;
function currying(fn,n){
return function(m){
return fn.call(this,m,n)
}
}
优化方案: ① 外层函数封装多参数的尾递归函数,如:
function fn(n){function g(n,1){return g(n,1) }}
;②函数柯里化只有当调用函数不再需要外层函数的内部变量,内层函数的调用帧才会取代外层函数的调用帧,否则无法进行尾调用优化; 优化方案:尾调用、尾递归由于是函数的最后一步,调用位置、内部变量信息等都不会再用到了,所以不需要保留外层函数的调用帧,直接用内层函数的调用帧取代外层函数即可,这就是尾调用优化(Tail call Optimize)、尾递归函数优化;
由于尾调用优化的重要性,ES6第一次明确规定: 所有ECMAScript的实现都必须部署“尾调用优化”;即ES6中只要使用尾递归,就不会发生内存溢出;
ps:
- ES7 提案函数对象绑定运算符(babel已支持):
::
双冒号左边是一个对象,右边是一个函数,该运算符自动将左边对象作为右边函数的上下文环境(context);