ES6-04:函数的扩展

ES6 针对新的语法特性(解构、参数默认值、箭头语句、块级作用域let),对于函数的属性、参数、作用域进行了扩展,并对递归调导致内存栈溢出用进行了优化。
同时ES6规定,只要函数参数使用了默认值、解构赋值、扩展运算符,函数内部都不可以使用严格模式(ES5可以),否则会报错;

1. 函数参数默认值

ES6之前,函数定义/声名时不能指定默认值;ES6可以在函数定义时进行初始化;同时有以下几点需要注意:

  • 参数设置默认值时,参数直接写在默认值后面,如:function(x,y=1){return x+y};
  • 函数传递参数时,不允许有同名参数,如:function(x,x,y=true){return x};//报错:参数重复命名
  • 函数传递参数时,不能在函数体内部用letconst再次声明,如: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);
原文地址:https://www.cnblogs.com/hbzyin/p/8012299.html