zoukankan      html  css  js  c++  java
  • 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对象的指向是可变的,但是在箭头函数中,它是固定的。

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

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

    绑定this

    箭头函数可以绑定this对象,大大减少了显式绑定this对象的写法(call、apply、bind)。

    但是,箭头函数并不适用于所有场合,所以现在有一个 提案,提出了“函数绑定”(function bind)运算符,用来取代call、apply、bind调用。

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

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

    尾调用及优化

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

    函数调用会在内存形成一个“调用记录”,又称“调用帧”(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
  • 相关阅读:
    OCP 062【中文】考试题库(cuug内部资料)第13题
    OCP 062【中文】考试题库(cuug内部资料)第12题
    560. 和为K的子数组 力扣(中等) 字节面试题,不会,前缀和,hash,有尺取法的味道
    863. 二叉树中所有距离为 K 的结点 力扣(中等) bfs
    671. 二叉树中第二小的节点 力扣(简单) auto循环set
    1713. 得到子序列的最少操作次数 力扣(困难) 最长公共子序列->最长上升子序列 lower(vector) 得迭代器
    47. 全排列 II 力扣(中等) 手写练习
    328. 奇偶链表 力扣(中等) 链表练习
    21. 合并两个有序链表 力扣(简单) 链表练习
    1743. 从相邻元素对还原数组 力扣(中等) 需要思考但可以做,hash
  • 原文地址:https://www.cnblogs.com/houfee/p/10090850.html
Copyright © 2011-2022 走看看