• 4.Function扩展



    ES6对Function做一些扩展,引入很多新特征,下面来逐个介绍

    1.函数参数默认值


    函数定义时,可以指定默认参数,在未传入参数或参数为undefined时,会使用默认的参数

    function Obj(id = 107, name = 'jack'){
      this.id = id;
      this.name = 'jack'
    }
    const obj = new Obj() // {id: 107, name: 'jack'}
    
    function add(x, y = 1){
      return x + y
    }
    add(1) // 2
    add(1, 3) // 4
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    但是要小心,参数变量是默认声明的,不能用let或const再次声明,因为其中调用时会发生隐式解构,即let [arg1,...,arg2] = [].prototype.slice.call(arguments,0)

    function fn(x = 5){
      // 隐式解构:let [x=5] = [].prototype.slice.call(arguments,0)
      let x = 1 // error
    }
    // 下面这个可以
    function fn(x=7) {
      if (x) {
        let x = 1 // 不在一个语句块里
        console.log(x) // 1
      }
      console.log(x) // 7
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    惰性求值

    let x = 99
    function foo(p = x + 1){
      alert(p)
    }
    foo() // 100
    x = 100
    foo() // 101
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    指定了默认值后,函数的length属性会失真,因为length属性含义为函数预期传入的参数个数

    (function (a, b, c = 5) {}).length // 2
    (function (a, b = 1, c) {}).length // 1 只记录默认值前面的
    
    • 1
    • 2

    2.rest参数


    rest参数形式为...args,会将获取函数的多余参数并将其放进数组中

    function(a,...b){}
    // 等价于
    function(){
      let args = [...arguments]
      let a = args.shift()
      let b = args
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    function add(n, ...values) {
      let sum = 0
      for (let val of values) {
        sum += val
      }
      return n * sum
    }
    add(10, 1, 2, 3) // 60
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    3.name属性


    函数的name属性返回该函数的函数名

    function fn(){ ... }
    fn.name // 'fn'
    
    var bar = function foo(){ ... }
    bar.name // 'foo'
    
    // 特殊的
    var fn = function(){ ... }
    fn.name // 'fn'
    
    // bind()返回的函数会加上‘bound’前缀
    function fn(){ ... }
    fn.bind({}).name // 'bound fn'
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    ☆4.箭头函数


    4.1 箭头函数特征

    箭头函数是ES6对函数进行的最大扩展

    var fn = n => n+1
    // 等价于
    var fn = function(n){
      return n+1
    }
    
    var fn = (a,b)=>a+b
    
    var fn = (a,b)=>{
      return a+b
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    若直接返回一个对象有必要时需要加个括号

    var getId = id =>({id: id,name:'jack'})
    
    • 1

    若箭头函数只有一行语句,且不需要返回值,可以采用void(慎用,会被同事特殊关怀滴~)

    var fn = arr => void arr.push(1)
    var arr = [3,2]
    fn(arr) // [3,2,1]
    
    • 1
    • 2
    • 3
    4.2 箭头函数和参数解构结合
    var obj ={
      id: 107,
      name: 'jack',
      age: 18
    }
    var getIdAndName = ({id, name}) =>name + ':'+ id
    
    getIdAndName(obj) // jack:107
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    4.3 箭头函数与rest参数结合
    let numbersToArr = ...num => num
    numbersToArr(1,2,3) // [1,2,3]
    
    let fn = (head,...end) =>[head, end]
    fn(1,2,3) //[1,[2,3]]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    ☆4.4 箭头函数注意点

    作用域里无this,this实际是来自于作用域链访问到的非箭头函数作用域中的this
    不可以new
    不存在arguments对象
    不可以yield

    详细说明一下箭头函数的this

    //  用Babel编译ES6代码来解释this
    // ES6
    function fn() {
      setTimeout(() => {
        console.log('id:', this.id)
      }, 100)
    }
    
    // ES5
    function fn() {
      var _this = this
      setTimeout(function () {
        console.log('id:', _this.id)
      }, 100)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    由于箭头函数作用域中没有自己的this,故call、apply、bind对其无效

    function fn() {
      return () => {
        return () => {
          return () => {
            console.log('id:', this.id)
          };
        };
      };
    }
    
    var f = fn.call({id: 1}) // f为return 的箭头函数,这时候this为上文所说的_this,故箭头函数没有真正意义上的'this',也意味着bind、call、apply对它无效!
    
    var t1 = f.call({id: 2})()() // id: 1
    var t2 = f().call({id: 3})() // id: 1
    var t3 = f()().call({id: 4}) // id: 1
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    再举个栗子

    function fn() {
      setTimeout(() => {
        console.log('id:', this.id);
      }, 100);
    }
    var id = 21
    fn() // 21
    fn.call({ id: 42 }) // 42
    
    
    function Timer() {
      this.s1 = 0
      this.s2 = 0
      // 箭头函数
      setInterval(() => this.s1++, 1000)
      // 普通函数
      setInterval(function(){this.s2++}, 1000)
    }
    
    var timer = new Timer()
    
    setTimeout(() => console.log(timer.s1), 3100) // 3  this绑定timer
    setTimeout(() => console.log(timer.s2), 3100) // 0  this帮定window
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    下面的例子能帮助你很好的理解箭头函数里的this

    var obj = {
      name: 'jack',
      sayName:()=>alert(this.name)
    }
    
    obj.sayName() // undefined, WTF???
    
    
    // 冷静详细分析一下
    // 箭头函数的this实际上是__this Babel转译
    obj = {
      __this:this // 这里的this指向的是window
      sayName:function(){
        alert(__this.name)
      }
    }
    // 在全局执行obj.sayName()
    // 1.创建全局上下文gContext
    gContext = {
      VO:{
        obj:{ ... },  // 因为obj是gContext中访问的,this是顺作用域链查询为window
        this: window
      }
      Scope:[VO],
    }
    // 2.objContext
    objContext = {
      VO:{
        __this: this, 
        name: 'jack',
        sayName:function(){ alert(_this.name) } 
      },
      Scope: [gContext.VO,VO]  // 在全局时Scope加入gContext.VO
    }
    // 3.sayNameReference
    sayNameReference = {
      base: obj, 
      name: sayNameReference
    }
    sayName.[[scope]] = objContext.Scope
    // 4.sayName执行时创建sayNameContext
    sayNameContext = {
      VO:{
        arguments:{
          length:0
        }
      },
      Scope: [gContext.VO,objContext.VO,VO]
    }
    
    // 卧槽,终于分析完了,这时候函数执行alert(__this.name),就会查找_this
    // 就会顺着sayNameContext.Scope作用域链查询,由于作用域链是栈结构,后进先出,故从数组屁股开始找 
    // sayNameContext.VO --> objContext.VO 找到_this,而_this保存的又是this,继续查找 
    // --> gContext.VO 找到this为window,TMD,终于给老子找到了!!!
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54

    考你一下有没有认真听课

    var button = document.getElementById('press')
    button.addEventListener('click', () => {
      this.classList.toggle('on')
    });
    // babel转译
    button = {
      __this: this, // 这里的_this指向什么?
      onClick:function(){
        __this.classList.toggle('on')
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    5.尾调用优化


    尾调用(tail call)是函数式编程的一个重要概念,是指函数的最后一步是调用另一个函数

    function fn(x){
      return g(x)
    }
    
    • 1
    • 2
    • 3

    函数调用会在内存形成一个调用记录,又称调用帧(call frame),保存调用位置和内部变量等信息。如果在函数A的内部调用函数B,那么在A的调用帧上方,还会形成一个B的调用帧,等到B运行结束,将结果返回到A,B的调用帧才会消失。如果函数B内部还调用函数C,那就还有一个C的调用帧,以此类推。所有的调用帧,就形成一个调用栈(call stack)
    而由于尾调用是函数的最后一步,故不需保留外层函数的调用帧,直接访问内层函数的调用帧即可。
    这就是尾调用优化,即必要时(闭包除外),删除外部函数的调用帧,只保留内层函数的调用帧。

    function f() {
      let m = 1
      let n = 2
      return g(m + n)
    }
    f()
    
    // 等同于
    function f() {
      return g(3)
    }
    f()
    
    // 等同于
    g(3)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    5.1 尾递归

    在尾部调用自身即为尾递归,由于递归需要同时保存成千上百个调用帧,很耗费内存,很容易栈溢出(stack overflow),而尾递归由于只会存在下一个内部函数的调用帧,所以不会栈溢出

    function factorial(n) {
      if (n === 1) return 1
      return n * factorial(n - 1) // 每次都还保留n这个变量,故调用帧保存下来
    }
    
    factorial(5) // 120
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    最多(即递归到最后一次函数执行时,调用帧叠加最多)需要保存n个调用帧,因为外层调用帧不能删除优化,还要用到n这个外部变量,空间复杂度O(n),现在做尾调优化

    function factorial(n, total) {
      if (n === 1) return total
      return factorial(n - 1, n * total) // 不保留其他变量,调用帧在尾调开始时就结束了
    }
    
    factorial(5, 1) // 120
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    这时候,每次递归执行时只需保存当前的调用帧即可,外部函数的调用帧已经优化删除了,空间复杂度O(1)

    5.2 尾递归优化改写

    头发还多的话去找找看吧

  • 相关阅读:
    状态模式和策略模式对比
    Bash openldap同步AD组织数据
    从零开始学React--环境搭建
    Shell Bad substitution的解决方法
    Linux命令之tree(3)
    代码随想录刷题记录:DP系列
    Unity TrailRenderer实现拖尾
    聊聊“JVM 调优&JVM 性能优化”是怎么个事?
    php配合fiddler批量下载淘宝天猫商品数据分享
    Java HttpClient-Restful 工具各种请求高度封装提炼总结及案例
  • 原文地址:https://blog.csdn.net/Xiaoyc7/article/details/125516575