JavaScript ES6 - 函数扩展

本章节的主要内容是: ES6 函数扩展

: 函数扩展
    1. 函数新增特性:
       1. 参数默认值
       2. rest 参数
       3. 扩展运算符
       4. 箭头函数
       5. this 绑定
       6. 尾调用

如图所示:
在这里插入图片描述

1. ES6 函数参数默认值

/**
1. 函数参数默认值
   1. ES6 设置默认值, 直接写在参数定义的后面 。
   2. 细节知识点:
      1. ES6 参数变量是默认声明的, 所以在函数体中不能用 let / const 再次声明, 否则会报错 。
      2. ES6 默认值不是传值, 而是每次都重新计算默认值, 也就是说默认值是惰性求值 。
      3. ES6 参数设置默认值, 后面不能再有没有默认值的参数 。
         1. 如果此时将默认参数设置为 undefiend, 将触发设置的默认值, null 没有这个效果 。
      4. 函数的 length 属性:
         1. length 属性的含义是这个函数预期传入参数的个数 。
            1. 某个参数指定默认值后, 预期传入参数的个数就不包含这个参数了 。
      5. 作用域的概念:
         1. 函数一旦设置了参数的默认值, 函数进行初始化时, 参数会形成一个单独的作用域; 等待初始化结束, 这个作用域就会消失 。这种语法行为在不设置参数的默认值时是不会出现的 。
 */

// ES6 写法
{
  function testES61(x, y='默认值') {
    console.log('ES6 设置默认值方法: ', x, y);
    // ES6 验证在函数体中不能用 let / const 再次声明, 已经设置了默认值的参数为变量 。
    // let y = 10 // 这里会报错 。
  }

  // 正常调用-1
  testES61('Hello') // ES6 设置默认值方法:  Hello 默认值
  // 正常调用-2
  testES61('Hello', '小明') // ES6 设置默认值方法:  Hello 小明
  // 正常调用-3
  testES61('Hello', '') //ES6 设置默认值方法:  Hello
  // 正常调用-4
  testES61('Hello', false) // ES6 设置默认值方法:  Hello false
}

// ES6 默认值是惰性求值
{
  /**
   * testES62 函数的参数 res 的默认值是 num1 + 1, 每次调用 testES62 都会重新计算 num1 + 1, 而不是默认 res = 10
   */
  let num1 = 9
  function testES62(res = num1 + 1) {
    console.log('ES6 默认值 -- 惰性求值: ', res);
  }
  testES62() // ES6 默认值 -- 惰性求值:  10

  num1 = 100
  testES62() // ES6 默认值 -- 惰性求值:  101
}

// ES6 参数设置默认值, 后面不能再有没有默认值的参数
{
  function testES63(x, y='默认值', z) {
    console.log('ES6 参数设置默认值, 后面不能再有没有默认值的参数: ', x, y, z);
  }
  testES63() // ES6 参数设置默认值, 后面不能再有没有默认值的参数:  undefined 默认值 undefined
  testES63('Hello') // ES6 参数设置默认值, 后面不能再有没有默认值的参数:  Hello 默认值 undefined
  testES63('Hello', '覆盖默认值-小明') // ES6 参数设置默认值, 后面不能再有没有默认值的参数:  Hello 覆盖默认值-小明 undefined
  // testES63('Hello', ,'第三个参数') // 报错, 无法设置
  testES63('Hello', undefined,'第三个参数') // ES6 参数设置默认值, 后面不能再有没有默认值的参数:  Hello 默认值 第三个参数
}

// 函数的 length 属性
{
  // 没有设置默认值
  function testES64(x, y, z) {
    console.log('函数的 length 属性');
  }
  console.log('函数的 length 属性 - 没有设置默认值: ', testES64.length); // 函数的 length 属性 - 没有设置默认值:  3

  // 设置默认值
  function testES65(x, y, z='位数参数-设置默认值') {
    console.log('函数的 length 属性');
  }
  console.log('函数的 length 属性 - 设置默认值: ', testES65.length); // 函数的 length 属性 - 设置默认值:  2
}

// 作用域的概念
{
  // 作用域 - 示例一
  let x = 'text'
  function testES66(x, y = x) {
    console.log('参数作用域的概念 -- 与函数默认值有关联: ', x, y);
  }
  // 思考: 当我们只传入一个参数, 即形参 x 等于 11; 此时的 y 参数取值是什么?
  // 此时参数 y 取值是 变量 x ? 还是形参 x ?
  // 此时参数 y 取值就是形参 x 的值; 看执行结果 。
  // 执行情况 1:
  // testES66(11) // 参数作用域的概念 -- 与函数默认值有关联:  11 11
  // 执行情况 2:
  testES66() // 参数作用域的概念 -- 与函数默认值有关联:  undefined undefined


  // 作用域 - 示例二
  let str = 'text'
  function testES67(a, y = str) {
    console.log('参数作用域的概念 -- 与函数默认值有关联: ', a, y);
  }
  // 思考此时的执行结果
  // 执行情况 1:
  testES67(11) // 参数作用域的概念 -- 与函数默认值有关联:  11 text
  // 执行情况 2:
  testES67() // 参数作用域的概念 -- 与函数默认值有关联:  undefined text


  // // 作用域 - 示例三
  // // 如果此时默认值 num 不存在, 就会报错 。
  // function testES67(a, y = num) {
  //   console.log('参数作用域的概念 -- 与函数默认值有关联: ', a, y);
  // }
  // testES67() // num is not defined
}





// ES5 写法
{
  function testEs51(x, y) {
    // 设置默认值
    y = y || '默认值'
    console.log('ES5 设置默认值: ', x, y);
  }

  // 正常调用-1
  testEs51('Hello') // ES5 设置默认值:  Hello 默认值

  // 正常调用-2
  testEs51('Hello', '传入参数') // 设置默认值:  Hello 传入参数

  // 问题调用--1
  testEs51('Hello', '') // 设置默认值:  Hello 默认值

  // 问题调用--2
  testEs51('Hello', false) // 设置默认值:  Hello 默认值


  
  /**
   * 问题总结:
   * 1. testEs51 函数这种写法的缺点在于如果赋值为 ''空 或者 false, 赋值是不起作用的; 就如  问题调用--1 与 问题调用--2 的调用方式 。
   *
   * 2. 解决办法: 如下代码
   *    1. 为了避免这个问题, 我们需要先判断需要设置默认值的参数是否被赋值, 如果没有再使其等于默认值 。
   */
  function testEs52(x, y) {
    // 设置默认值
    if(typeof y === 'undefined') {
      y = '默认值'
    }
    console.log('ES5 设置默认值--避免上述问题: ', x, y);
  }
  // 问题调用-解决办法--1
  testEs52('Hello', '') // 设置默认值--避免上述问题:  Hello 默认值

  // 问题调用-解决办法--2
  testEs52('Hello', false) // 设置默认值--避免上述问题:  Hello 默认值
}

2. ES6 rest 参数

/**
2. rest 参数
   1. 形式是 ...变量名
   2. 作用是在不确定参数数目时, 将一些列参数转换为一个数组 。
   3. 细节知识点:
      1. rest 参数后面不能再有其余的参数, 否则会报错 。
         1. 如果 后面还有其它的参数时, 在转换数组时它不知道在什么地方截止 。
 */
{
  // 示例一:
  function restES61(...arg) {
    for (const val of arg) {
      console.log('ES6 -- rest 参数: ', val);
    }
  }
  restES61(1,3,5)
  // 打印结果:
  // ES6 -- rest 参数:  1
  // ES6 -- rest 参数:  3
  // ES6 -- rest 参数:  5

  // 示例二:
  function pushRewrite(array, ...arg) {
    arg.forEach(function(item) {
      array.push(item)
    })
  }
  var arr = []
  pushRewrite(arr, 1,2,3,4,5)
  console.log('改写 js 的 push 方法: ', arr); // 改写 js 的 push 方法:  (5) [1, 2, 3, 4, 5]
}

3. ES6 函数扩展运算符

/**
3. 扩展运算符
   1. 形式: ...[] 或者 ...变量名 或者 ...()
   2. 它可以理解为 rest 参数 的逆运算, 将一个数组转换为用逗号隔开的参数序列 。
   3. 细节知识点:
      1. 扩展运算符后面可以放置表达式 。
      2. 如果扩展运算符后面是一个空数组, 则不产生任何效果 。
 */

{
  console.log('ES6 扩展运算符: ', ...[1,2,4]); // ES6 扩展运算符 1 2 4

  let arr1 = [1,2,4]
  console.log('ES6 扩展运算符: ', 'a', ...arr1, 6); // ES6 扩展运算符:  a 1 2 4 6

  let conditi = 0
  let num = 8
  let arr = [
    ...(conditi > 0 ? ['a'] : []),
    num,
  ]
  console.log('ES6 扩展运算符后面可以放置表达式: ', arr); // ES6 扩展运算符后面可以放置表达式:   [8]
}

4. ES6 箭头函数

{
  // ES6 声明函数方法
  /**
   * arrow1: 函数名
   * v: 函数参数
   * =>: 使用箭头链接
   * v*2: 函数返回值
   */
  let arrow1 = v => v*2
  console.log('ES6 声明函数方法 -- 箭头函数: ', arrow1(2)); // ES6 声明函数方法 -- 箭头函数:  4
  // 等价于
  function arrow1Equivalence(v) {
    return v * 2
  }


  /**
   * 1. 如果没有参数, 我们使用 ()圆括号 表示
   * 2. 这里返回一个固定值
   */
  let arrow2 = () => 5
  console.log('没有参数, 我们使用 ()空括号 表示 -- 箭头函数: ', arrow2()); // 没有参数, 我们使用 ()空括号 表示 -- 箭头函数:  5
  // 等价于
  function arrow2Equivalence() {
    return 5
  }


  /**
   * 1. 如果箭头函数的 '代码块' 大于一条语句, 我们就需要使用 {}大括号 包裹起来, 并且使用 return 语句返回 。
   * 2. 如果箭头函数直接返回一个对象, 必须在外面加上 ()括号, 这是因为 {}大括号被解析为代码块的原因 。
   */
  let arrow3 = (num1, num2) => {
    return num1 + num2
  }
  console.log('箭头函数的 "代码块" 大于一条语句: ', arrow3(1, 3)); // 箭头函数的 "代码块" 大于一条语句:  4
  let arrow4 = (age) => ({ageNum: age, name: '小明'})
  console.log('箭头函数直接返回一个对象: ',arrow4(12)); // 箭头函数直接返回一个对象:  {ageNum: 12, name: '小明'}


  
  /**
   * 箭头函数简化回调函数
   */
  // 箭头函数写法
  let arr1 = [1, 2, 3, 4]
  let resArr1 = arr1.map(x => x + x)
  console.log('箭头函数简化回调函数: ', 'arr1: ', arr1, 'resArr1: ', resArr1); 
  // 箭头函数简化回调函数:  arr1: [1, 2, 3, 4] resArr1: [2, 4, 6, 8]
  
  // 常规写法
  let arr2 = [1, 2, 3, 4]
  let resArr2 = arr2.map(function(item) {
    return item + item
  })
  console.log('回调函数--常规写法: ', 'arr2: ', arr2, 'resArr2: ', resArr2); 
  // 回调函数--常规写法:  arr2: [1, 2, 3, 4] resArr2: [2, 4, 6, 8]

  // 箭头函数写法
  let resSort1 = [2,1,5,3,7].sort((a, b) => a - b)
  console.log('resSort1: ', resSort1); // resSort1: [1, 2, 3, 5, 7]
  // 常规写法
  var resSort2 = [2,1,5,3,7].sort(function(a, b) {
    return a - b
  })
  console.log('resSort2: ', resSort2); // resSort2: [1, 2, 3, 5, 7]



  /**
   * rest 参数 与 回调函数的结合
   */
  let fun3 = (num, ...arr3) => [num, arr3]
  let res3 = fun3(1, 2, 3, 4)
  console.log('rest 参数 与 回调函数的结合: ', res3); // rest 参数 与 回调函数的结合: [1, [2, 3, 4]]



  /**
   * 1. 箭头函数 this 指向
   *    1. 在箭头函数中 this 指向是固定的 。
   *       1. 箭头函数体内的 this 对象就是定义时所在的对象, 而不是使用时所在的对象 。
   */
  // 示例一:
  function funThis1() {
    // 箭头函数可以让 setTimeout 里面的 this 绑定--定义时所在的作用域, 而不是指向运行时所在的作用域 。
    setTimeout(() => {
      console.log("箭头函数 this 指向 -- 参数 id:", this.id);
      // 打印结果:
      // 箭头函数 this 指向 -- 参数 id: 99
    }, 1000);
  }
  let id = 12
  funThis1.call({id: 99})
  // 示例二:
  function FunThis2() {
    this.num1 = 0
    this.num2 = 0
    // 箭头函数: 此 this 绑定定义时所在的作用域 即 FunThis2 。
    setInterval(() => this.num1++, 1000);
    // 常规函数: 此 this 指向运行时所在的作用域 即 全局对象 。
    setInterval(function() {
      this.num2++
    }, 1000)
  }
  var funThis2 = new FunThis2()
  setTimeout(() => {
    console.log('num1: ', funThis2.num1); // num1:  3
  }, 3100);
  setTimeout(() => {
    console.log('num2: ', funThis2.num2); // num1:  0
  }, 3100);
}

5. ES6 函数尾调用

/**
5. 尾调用
   1. 尾调用它是存在于函数式编程的一个概念 。
   2. 函数的最后一步是调用另一个函数 。
   3. 尾调用的好处:
      1. 提升性能
      2. 尾调用之所以与其它调用不同, 就在于它特殊的调用位置 。
      3. 调用函数会在内存中生成一个 '调用记录'(或者是 '调用帧'), 保存调用位置和内部变量等信息 。
      4. 如果有三个函数分别是 A 、 B 、 C;
         1. A 调用 B, B 调用 C;
         2. 那么在 A 的调用帧上方会生成一个 B 的调用帧, 在 B 的调用帧的上方还会生成一个 C 的调用帧;
         3. 此时运行函数, 等到 B 运行结束, 将结果返回 A, B 的调用帧才会结束;
         4. 同理类推 C 运行结束, 将结果返回 B, C 的调用帧才会结束 。
         5. 所有的调用帧会形成一个 '调用栈' 。
      5. 尾调用由于是函数的最后一步操作, 所以不需要保留外层函数的调用帧, 因为调用位置、内部变量信息都不会再用到, 直接用内层函数的调用帧取代外层函数的调用帧即可 。
      6. 如果所有的函数都是尾调用, 那么可以做到每次执行时调用帧只有一项, 这将节省很大的内存空间; 这也是 尾调用优化 的意义 。
 */
{
  // 尾调用--示例一:
  function fir(x) {
    console.log('fir 函数 -- 尾调用: ', x);
  }
  function sec(x) {
    return fir(x)
  }
  sec(1111) // fir 函数 -- 尾调用:  1111

  // 尾调用--示例二:
  function fir1(x) {
    console.log('尾调用--fir1 函数');
  }
  function fir2(x) {
    console.log('尾调用--fir2 函数');
  }
  function sec1(x) {
    if(x > 1) {
      return fir1(x)
    }
    return fir2(x)
  }

  // 尾调用--示例三:
  function a1() {
    let m = 1
    let n = 2
    return g(m + n)
  }

  // 尾调用--示例四: 只有不再用到外层函数的内部变量, 内层函数的调用帧才会取代外层函数的调用帧, 否则无法进行 尾调用优化 。
  function  b(num1) {
    var m1 = 2
    function inner(num2) {
      // 此处就不能实现 尾调用优化, 因为它使用到了外层函数的内部变量, 外层函数调用帧无法被取代 。
      return m1 + num2
    }
    return inner(a)
  }



  /**
   * 下面不属于尾调用
   * 1. 调用函数之后还有赋值操作
   * 2. 调用之后还有操作
   */
  function g(x) {
    console.log('尾调用函数');
  }

  // 示例一:
  function f1(x) {
    let y =  g(x)
    return y
  }

  // 示例二:
  function f2(x) {
    return g(x) + 1
  }

  // 示例三:
  function f3(x) {
    g(x)
  }
  // 相当于
  function f4(x) {
    g(x)
    return undefined
  }
}

6. ES6 函数尾递归

/**
 * 尾递归:
 * 1. 函数调用自身称为递归, 如果尾调用自身就称为尾递归 。
 * 2. 尾递归非常消耗内存, 因为需要同时保存很多调用帧, 很容易发生 '栈溢出' 错误;
 * 3. 但对于尾递归来说, 由于只存在一个调用帧, 所以永远不会发生 '栈溢出' 错误 。
 * 4. 改写:
 *    1. 改写递归函数为尾递归函数, 就是确保最后一步只调用自身;
 *    2. 要做到这一点的方法就是把所有用到的内部变量改写为函数参数 。
 */
{
  /**
   * 1. 阶乘示例:
   */
  // 递归函数
  function fact1(n) {
    if(n === 1) {
      return 1
    } else {
      return n * fact1(n - 1)
    }
  }
  fact1(5) // 120
  // 尾递归
  function fact2(n, total) {
    if(n === 1) return total
    return fact2(n - 1, n * total)
  }
  fact2(5, 1) // 120




  /**
   * 2. Fibonacci 数列:
   */
  // 递归函数
  function fib1(n) {
    if( n <= 1) {return 1}
    return fib1(n - 1) + fib1(n - 2)
  }
  console.log(fib1(10)); // 89
  // fib1(100) // 堆栈溢出(转呀转转不动, 电脑嗡嗡要炸裂)
  // fib1(500) // 堆栈溢出(转呀转转不动, 电脑嗡嗡要炸裂)

  // 尾递归
  function fib2(n, ac1 = 1, ac2 = 1) {
    if(n <= 1) {return ac2}
    return fib2(n-1, ac2, ac1 + ac2)
  }
  console.log(fib2(10)); // 89
  console.log(fib2(100)); // 573147844013817200000
  console.log(fib2(500)); // 2.2559151616193602e+104
  // console.log(fib2(10000)); // 报错:  Maximum call stack size exceeded
}

以上代码执行结果, 如图所示:
在这里插入图片描述
在这里插入图片描述

之前有整理过部分知识点, 现在将整理的相关内容, 验证之后慢慢分享给大家; 这个专题是 “前端ES6基础” 的相关专栏; 不积跬步,无以至千里, 戒焦戒躁 。

如果对大家有所帮助,可以点个关注、点个赞; 文章会持续打磨 。
有什么想要了解的前端知识, 可以在评论区留言, 会及时分享所相关内容 。
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

黑木令

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值