函数的预解析
预解析的一个表现就是 声明式函数再定义前可以被调用
预解析是什么?
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>
复制代码
作用域
什么是 作用域? (这是一道面试题)
就是变量可以起作用的范围
作用域分为两个
- 全局作用域(直接在 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>