函数的预解析,作用域,作用域链,作用域链的赋值规则,递归函数

98 阅读8分钟
函数的预解析
           预解析的一个表现就是 声明式函数再定义前可以被调用
      
           预解析是什么?
               JS 在执行代码的时候, 会有一个 所谓的 解析阶段
                   解析阶段, 做了一件事, 就是 函数提升, 就是将 声明式 函数的定义, 提升到当前 作用域的最顶端
      
           作用域的最顶端:
               暂时理解为 当前页面的最开始的位置
    

    fn()
    function fn() {
        console.log('我是 fn 函数, 我被调用了')
    }

    
      
       一道面试题: 函数的预解析是什么?
      
       正常书写的 代码
           fn()
           function fn() {
               console.log('我是 fn 函数, 我被调用了')
           }
      
       浏览器会对我们的 JS 代码, 做一个 预解析, 预解析的时候, 会将函数提升到 当前作用域的最顶端, (暂时理解为 当前页面最开始的位置)
      
           fn()    ->  这行代码是函数调用, 所以不需要提升
           function fn() {                                 ->      这是一个声明式定义的函数, 所以需要提升
               console.log('我是 fn 函数, 我被调用了')
           }
      
      
       预解析之后的代码长什么样(执行顺序)?
           function fn() {
               console.log('我是 fn 函数, 我被调用了')
           }
      
           fn()    // 所以此时调用的时候, 因为 fn 函数已经定义完成了, 所以这里能够正常执行函数
作用域
           什么是 作用域?          (这是一道面试题)
               就是变量可以起作用的范围
      
           作用域分为两个
               1. 全局作用域(直接在 script 内书写的代码)
                       再此作用域创建的变量, 我们叫做全局变量, 在当前 script 标签内的哪里都能使用
                       在 JS 中, 全局作用域中有一个 提前给我们准备好的 对象(一种数据格式)
                               这个 对象叫做 window
                       我们创建的全局变量, 会被自动添加到 window 对象中
      
               2. 局部作用域(在 JS 中, 只有函数能够创建局部作用域)
                       在此作用域创建的变量, 只能在当前作用域使用, 超出这个作用域(也就是在函数外边)去使用, 就会找不到变量
    

     var num = 100

     // 假设 间隔 500 行
     console.log(num)


    function fn() {
        var sum = '我是在函数 fn 内部创建的变量, 我是局部变量, 所以我只能在当前函数内使用'

        var abc123 = '我是在 fn 函数内部创建的局部变量'

        console.log(sum)
    }
    fn()
     console.log(sum) // 这里因为超出了这个变量的使用区间, 所以会 报错


    var abc = '我是一个全局变量 abc'    // 创建一个全局变量 abc
    console.log(window)
作用域链 (这是一个纯概念性的东西, 面试也可能会问)
           作用域链就是在访问一个变量的时候, 如果当前作用域内没有
      
           会去自己的父级作用域, 也就是上一层作用域内查找, 如果找到就直接使用, 如果没有找到继续向上层查找
      
           直到查找到 最顶层的全局作用域, 如果找到了直接使用, 如果没找到 报错提示变量不存在(未定义)
      
           我们将这个一层一层向上查找的规律, 叫做作用域链
    

     var num = 999
     function fn1() {
         var num = 100

         function fn2() {
             console.log(num)                 
                //打印的值 为 100
                   // 1. 先在当前作用域内, 也就是 fn2 函数内部开始查找变量 num, 然后发现 当前作用域内 没有这个变量
                        所以会去自己的父级的作用域内查找(也就是 fn1 这个函数内部)
                   // 2. 来到了自己父级内部查找, 此时找到了一个变量 num 他的值 为 100, 然后直接使用这个变量 并停止查找        
         }
         fn2()
     }
     fn1()


     var num = 999
     function fn1() {
         function fn2() {
             console.log(num)                 
               // 打印的值 为 999
                   // 1. 先在当前作用域内, 也就是 fn2 函数内部开始查找变量 num, 然后发现 当前作用域内 没有这个变量
                   //    所以会去自己的父级的作用域内查找(也就是 fn1 这个函数内部)
                   // 2. 来到了自己父级内部查找, 发现并没有一个叫做 num 的变量, 然后继续向上层查找, 也就是 全局作用域 内
               
                   // 3. 来到全局作用域内查找的时候 发现了一个叫做 num 的变量, 值为 999, 然后停止查找, 直接使用该变量                 
         }
         fn2()
     }
     fn1()


     function fn1() {
         function fn2() {
             console.log(num)
             /**
              *  num 找不到, 所以会报错
              * 
              *      1. 先在当前作用域内查找, 也就是 fn2 内部, 发现没有, 去自己的父级查找, 也就是 fn1 内部
              *      2. 来到了 fn1 内部查找, 发现没有, 去自己的父级查找, 也就是 全局作用域
              *      3. 来到了全局作用域内查找, 发现还是没有, 然后停止查找, 返回一个 num 未定义的报错
              * 
              *      4. 虽然 fn2 作用域内的子级作用域内(fn3函数内部) 有一个变量叫做 num 但是根据 作用域链的访问规则
              *          我们并不会去这个 作用域内查找, 因为 作用域只会逐层向上查找, 并不会向下查找
             */
             function fn3() {
                 var num = 666
             }
             fn3()
         }
         fn2()
     }
     fn1()


    
作用域链的赋值规则
           在给变量赋值的时候, 首先会去当前作用域查找, 如果有直接赋值, 并停止查找
      
           如果没有, 会去自己的父级查找, 在父级找到直接修改值然后停止查找, 如果没有继续向自己的父级查找, 直到找到全局作用域
      
           在全局作用域内, 找到直接赋值修改他的值, 如果没有找到, 那么会在全局作用域创建一个变量, 并赋值
    

     function fn1() {
         function fn2() {
             num = 100
         }
         fn2()
     }
     fn1()
     console.log(num)    // 100

     function fn1() {
         var num = 999
         function fn2() {
             num = 100
             /**
              *  在当前作用域内查找 num 发现没有, 会去自己的父级作用域内查找, 也就是 fn1 函数内部
              * 
              *  在 fn1 函数内部发现一个变量 num 然后值为 999    我们会对这个变量做一个重新赋值的操作
              * 
              *  也就是将他的值 重新修改为 100
             */
         }
         fn2()
         console.log(num)    // 100
     }
     fn1()
     console.log(num)    // 未定义


    var num = 666
    function fn1() {
        var num = 999
        function fn2() {
            num = 100
            /**
             *  在当前作用域内查找 num 发现没有, 会去自己的父级作用域内查找, 也就是 fn1 函数内部
             * 
             *  在 fn1 函数内部发现一个变量 num 然后值为 999    我们会对这个变量做一个重新赋值的操作
             * 
             *  也就是将他的值 重新修改为 100
            */
        }
        fn2()
    }
    fn1()
    console.log(num)    // 666

    var num = 666
    function fn1() {
        function fn2() {
            num = 100
            /**
             *  在当前作用域内查找 num 发现没有, 会去自己的父级作用域内查找, 也就是 fn1 函数内部
             * 
             *  在 fn1 函数内部, 发现没有 这个变量, 继续去自己的父级作用域查找, 也就是 全局作用域
             * 
             *  在全局作用域发现了一个变量 叫做 num, 他的值是 666, 我们将这个变量重新赋值为 100
            */
        }
        fn2()
    }
    fn1()
    console.log(num)    // 100
递归函数
           本质上还是一个函数
      
           当一个函数在函数的内部, 调用了 自身, 那么就算是一个 所谓的 递归函数(只不过有点小缺陷)
    

     function fn(n) {
         
            计算 4 的阶乘
           
                4 的阶乘: 4 * 3的阶乘
         
         // return 4 * fn(3)
         return n * fn(n - 1)
     }
     var sum = fn(4)
     console.log(sum)    // 此时打印的值 为 4 的阶乘
    
       fn 函数 需要一个参数, 传入一个参数后 会得到这个参数的 阶乘结果
      
           fn(100) ->  100 的阶乘
           fn(99)  ->  99 的阶乘
           ....
           fn(4)   -> 4 的阶乘
    

    
       function fn(n) {
           return n * fn(n - 1)
       }
       fn(4)
      
      
       第一次调用, 传递 参数 4
               形参 n === 4
               函数体  return n * fn(n - 1)    ->  return 4 * fn(3)
      
               计算 fn(3)  -> 计算 3 的阶乘
                   调用的时候 传参是 3
                   形参 n === 3
                   函数体 return n * fn(n - 1)     ->      return 3 * fn(2)
      
               计算 fn(2)  -> 计算 2 的阶乘
                   调用的时候 传参是 2
                   形参 n === 2
                   函数体 return n * fn(n - 1)     ->      return 2 * fn(1)
      
               计算 fn(1)  -> 计算 1 的阶乘
                   调用的时候 传参是 1
                   形参 n === 1
                   函数体 return n * fn(n - 1)     ->      return 1 * fn(0)
      
               计算 fn(0)  -> 计算 0 的阶乘
                   调用的时候 传参是 0
                   形参 n === 0
                   函数体 return n * fn(n - 1)     ->      return 0 * fn(-1)
      
               .... 永无止境, 相当于写了死循环
      
      
       100 的阶乘      100 * 99 * 98 * 97 ..... * 3 * 2 * 1
    

    function fn(n) {
        if (n === 1) {
            // 说明此时想要计算 1 的阶乘, 那么我直接将 1 的阶乘的结果 return 出去
            return 1
        }
        return n * fn(n - 1)
    }
    var sum = fn(4)
    console.log(sum)    // 24

    var sum1 = fn(10)
    console.log(sum1)

    
       第一次调用 传递的参数为 4
           形参 n === 4
           函数体  1. if 分支      2. return 递归调用
                   此时 分支语句 不会执行, 开始 执行 return 递归调用
                   return n * fn(n - 1)        return 4 * fn(3)    == 24
      
           计算 fn(3)  === 6
               传递的 形参 n === 3
               此时分支语句不会执行, 开始执行 return 递归调用
                   return n * fn(n - 1)        return 3 * fn(2) === 6
      
           计算 fn(2)  === 2
               传递的 形参 n === 2
               此时分支语句不会执行, 开始执行 return 递归调用
                   return n * fn(n - 1)        return 2 * fn(1) === 2
     
           计算 fn(1)  === 1
               传递的 形参 n === 1
               此时 分支语句 判断成功, 所以直接 return 1       中断函数的递归