ES6 之 函数的扩展 尾调用以及尾递归

函数参数的默认值

    function log(x, y) {
      y = y || 'world'
      console.log(x + ' ' + y);
    }
    log('hello') // hello world
    log('hello','China') // hello China
    log('hello', '') // hello world
    /*
    * 若是 y 没有赋值,指定默认值为world // log('hello') // hello world
    * 若是 y 有赋值,指定赋值  log('hello','China') // hello China
    * 若是 y 有赋值,可是为 boolean 值 false,则该赋值就不起做用了  log('hello', '') // hello world
    * */

函数的length属性

console.log((function (a) {}).length) // 1
console.log((function (a = 1) {}).length) // 0 
console.log((function (a, b = 1, c) {}).length) //1
console.log((function (a, b , c = 2) {}).length) // 2
// 返回没有赋值形参的个数
// 若是设置了默认值的参数不是尾参数,那么length属性也再也不计入后面的参数

做用域

赋值的形参造成单独的做用域编程

rest参数

ES6 引入rest参数,(形式为“...变量名”),用于获取函数的多余参数,这样就不须要arguments对象了。rest参数搭配的变量是一个数组,该变量将多余的参数放入其中
    function add(...value) {
      let sum = 0
      for(let val of value) {
        sum += val
      }
      return sum
    }
    const NUM = add(2, 5, 8, 90)
    console.log(NUM)

 

箭头函数

var f = v => v;
// 若是见箭头函数不须要参数 或者 须要对个参数,就是用圆括号表明参数部分
var sum = (num1, num2) => num1 + num2
// 若是箭头函数的代码块部分多余一条语句,就要使用大括号将其阔起来,
// 并使用return语句返回
// 若是箭头函数直接返回一个对象,必须在对象外面加上括号
var getTempItem = id => ({ id: id, name: "Temp"})

// 箭头函数 注意事项
/*
* 函数体内的this对象就是定义时所在的对象,而不是使用时所在的队形
* 不能够当作构造函数
* 不能够使用arguments对象
* 不能够使用yield命令,所以箭头函数態用做Generator函数
* 箭头函数可让this指向固定化,这种形式很是有利于封装回调函数
* 箭头函数能够绑定this,大大减小了显示绑定this对象的写法(call,apply,bind)*/

《ES6标准入门》 第三版数组

箭头函数有几个使用注意点。
(1)函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象。
(2)不能够看成构造函数,也就是说,不能够使用new命令,不然会抛出一个错误。
(3)不能够使用arguments对象,该对象在函数体内不存在。若是要用,能够用 rest 参数代替。
(4)不能够使用yield命令,所以箭头函数不能用做 Generator 函数。
上面四点中,第一点尤为值得注意。this对象的指向是可变的,可是在箭头函数中,它是固定的。app

箭头函数可让setTimeout里面的this,绑定定义时所在的做用域,而不是指向运行时所在的做用域。函数式编程

箭头函数可让this指向固定化,这种特性颇有利于封装回调函数。函数

绑定this

箭头函数能够绑定this对象,大大减小了显式绑定this对象的写法(call、apply、bind)。优化

可是,箭头函数并不适用于全部场合,因此如今有一个 提案,提出了“函数绑定”(function bind)运算符,用来取代call、apply、bind调用。this

函数绑定运算符是并排的两个冒号(::),双冒号左边是一个对象,右边是一个函数。该运算符会自动将左边的对象,做为上下文环境(即this对 象),绑定到右边的函数上面。spa

若是双冒号左边为空,右边是一个对象的方法,则等于将该方法绑定在该对象上面。rest

尾调用及优化

就是指某个函数的后一步是调用另外一个函数。code

函数调用会在内存造成一个“调用记录”,又称“调用帧”(call frame),保存调用位置和内部变量等信息。

若是在函数A的内部调用函数B,那 么在A的调用帧上方,还会造成一个B的调用帧。

等到B运行结束,将结果返回到A,B的调用帧才会消失。若是函数B内部还调用函数C,那就还有一 个C的调用帧,

以此类推。全部的调用帧,就造成一个“调用栈”(call stack)。


尾调用因为是函数的后一步操做,因此不须要保留外层函数的调用帧,由于调用位置、内部变量等信息都不会再用到了,只要直接用内层函数的调用 帧,取代外层函数的调用帧就能够了。

 

只保留内层函数的调用帧。若是全部函数都是尾调用,那么彻底能够作到每次执行时,调用帧只有 一项,这将大大节省内存。这就是“尾调用优化”的意义。
注意,只有再也不用到外层函数的内部变量,内层函数的调用帧才会取代外层函数的调用帧,不然就没法进行“尾调用优化”。

尾递归及优化

函数调用自身,称为递归。若是尾调用自身,就称为尾递归。

递归很是耗费内存,由于须要同时保存成千上百个调用帧,很容易发生“栈溢出”错误(stack overflow)。但对于尾递归来讲,因为只存在一个调用帧, 因此永远不会发生“栈溢出”错误。

/* ----------- 普通递归 ------------------ */
function factorial(n) {
  if (n === 1) return 1;
  return n * factorial(n - 1);
}

let num1 = factorial(5)
console.time(1); //设置时间起点
console.log(num1); // 120
console.timeEnd(1); // 3.450ms
/* 上面代码是一个阶乘函数,计算n的阶乘,多须要保存n个调用记录,复杂度 O(n) 。 */


/* ----------- 尾递归 ------------------ */
/* 若是改写成尾递归,只保留一个调用记录,复杂度 O(1) 。 */
function factorial(n, total) {
  if (n === 1) return total;
  return factorial(n - 1, n * total);
}

let num2 = factorial(5, 1) // 120
console.time(1); //设置时间起点
console.log(num2); // 120
console.timeEnd(1); // 0.275ms

ES6 中只要使用尾递归,就不会发生栈溢出,相对节省内存。

/* 非尾递归的 费氏数列 Fibonacci */
function Fibonacci(n) {
  if (n <= 1) {
    return 1
  };
  return Fibonacci(n - 1) + Fibonacci(n - 2);
}

// let num21 = Fibonacci(10) // 573147844013817200000
// console.time(1); //设置时间起点
// console.log(num21); // 89
// console.timeEnd(1); // 4.030ms


// let num22 = Fibonacci(100) // 573147844013817200000
// console.time(2); //设置时间起点
// console.log(num22); // 堆栈溢出 
// console.timeEnd(2); //  4.堆栈溢出 

// let num23 = Fibonacci2(1000) // 7.0330367711422765e+208
// console.time(3); //设置时间起点
// console.log(num23); // 堆栈溢出 
// console.timeEnd(3); //  堆栈溢出 


/* 尾递归优化过的 Fibonacci 数列实现以下 */
function Fibonacci2(n, ac1 = 1, ac2 = 1) {
  if (n <= 1) {
    return ac2
  };
  return Fibonacci2(n - 1, ac2, ac1 + ac2);
}

// let num21 = Fibonacci2(10) // 573147844013817200000
// console.time(1); //设置时间起点
// console.log(num21); // 89
// console.timeEnd(1); // 3.469ms


// let num22 = Fibonacci2(100) // 573147844013817200000
// console.time(2); //设置时间起点
// console.log(num22); // 573147844013817200000
// console.timeEnd(2); //  4.511ms

// let num23 = Fibonacci2(1000) // 7.0330367711422765e+208
// console.time(3); //设置时间起点
// console.log(num23); // 7.0330367711422765e+208
// console.timeEnd(3); //  4.053ms

/* ES6 是如此,第一次明确规定,全部 ECMAScript 的实 现,都必须部署“尾调用优化”。这就是说,ES6 中只要使用尾递归,就不会发生栈溢出,相对节省内存。 */

 

/* 尾递归的实现,每每须要改写递归函数,确保后一步只调用自身。作到这一点的方法,就是把全部用到的内部变量改写成函数的参数。 */

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

// 上面代码经过一个正常形式的阶乘函数factorial,调用尾递归函数tailFactorial,看起来就正常多了。
// 函数式编程有一个概念,叫作柯里化(currying),意思是将多参数的函数转换成单参数的形式。这里也能够使用柯里化。
function currying(fn, n) {
  return function (m) {
    return fn.call(this, m, n);
  };
}

function tailFactorial(n, total) {
  if (n === 1) return total;
  return tailFactorial(n - 1, n * total);
}

const factorial = currying(tailFactorial, 1);

factorial(5) // 120
相关文章
相关标签/搜索