函数的预解析
预解析的一个表现就是 声明式函数再定义前可以被调用
预解析是什么?
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 中断函数的递归