函数的预解析

81 阅读2分钟

函数的预解析

预解析的一个表现就是 声明式函数再定义前可以被调用

预解析是什么?

JS 在执行代码的时候, 会有一个 所谓的 解析阶段 解析阶段, 做了一件事, 就是 函数提升, 就是将 声明式 函数的定义, 提升到当前 作用域的最顶端 作用域的最顶端:

<script>
    
        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 函数已经定义完成了, 所以这里能够正常执行函数

</script>
复制代码

作用域

什么是 作用域? (这是一道面试题)

就是变量可以起作用的范围

作用域分为两个

  1. 全局作用域(直接在 script 内书写的代码) 再此作用域创建的变量, 我们叫做全局变量, 在当前 script 标签内的哪里都能使用
<script>
    var abc = '我是一个全局变量 abc'    // 创建一个全局变量 abc
        console.log(window)
</script>
复制代码

在 JS 中, 全局作用域中有一个 提前给我们准备好的 对象(一种数据格式, 后续会详细的讲解) 这个 对象叫做 window 我们创建的全局变量, 会被自动添加到 window 对象中 2. 局部作用域(在 JS 中, 只有函数能够创建局部作用域) 在此作用域创建的变量, 只能在当前作用域使用, 超出这个作用域(也就是在函数外边)去使用, 就会找不到变量

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

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

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

复制代码

作用域链 (这是一个纯概念性的东西, 面试也可能会问)

作用域链就是在访问一个变量的时候, 如果当前作用域内没有,会去自己的父级作用域, 也就是上一层作用域内查找, 如果找到就直接使用, 如果没有找到继续向上层查找 直到查找到 最顶层的全局作用域, 如果找到了直接使用, 如果没找到 报错提示变量不存在(未定义)

我们将这个一层一层向上查找的规律, 叫做作用域链

<script>
    var num = 999
    function fn1() {
    var num = 100

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

作用域链的赋值规则

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

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

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

<script>
    var num = 666
        function fn1() {
            function fn2() {
                num = 100
                /**
                 *  在当前作用域内查找 num 发现没有, 会去自己的父级作用域内查找, 也就是 fn1 函数内部
                 * 
                 *  在 fn1 函数内部, 发现没有 这个变量, 继续去自己的父级作用域查找, 也就是 全局作用域
                 * 
                 *  在全局作用域发现了一个变量 叫做 num, 他的值是 666, 我们将这个变量重新赋值为 100
                */
            }
            fn2()
        }
        fn1()
        console.log(num)    // 100
</script>
复制代码

递归函数

本质上还是一个函数 当一个函数在函数的内部, 调用了 自身, 那么就算是一个 所谓的 递归函数(只不过有点小缺陷:没有结束条件,所以会一直循环下去)

<script>
    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)
        //  * 
        //  *          .... 永无止境, 相当于写了死循环
</script>

复制代码
<script>
    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       中断函数的递归

</script>