本节目标
了解作用域对程序执行的影响, 了解作用域链的查找机制, 使用闭包函数创建隔离作用域避免全局变量污染
- 作用域
- 作用域链
- 垃圾回收机制
- 闭包
- 变量提升
作用域
作用域规定了 变量 能够被 访问 的"范围", 离开了这个"范围", 变量就不能被访问
分类: 作用域分别为 局部作用域 和 全局作用域
局部作用域
分类: 局部作用域分为 函数作用域 和 块作用域
1,函数作用域
再函数内部声明的变量只能在函数内部访问, 外部无法访问
<script>
//函数作用域
function getSum() {
// 函数内部就是函数作用域, 属于局部变量
const num = 10
}
console.log(num); // 报错: 函数外部不能使用局部作用域变量
</script>
补充
- 函数内部声明的变量, 在函数外部无法被访问
- 函数的参数也是函数内部的局部变量
- 不同函数 内部声明的变量 无法相互访问
- 函数执行完毕后, 函数内部的变量实际被清空了
2,块作用域
在JavaScript中使用 { } 包裹的代码称为代码块, 代码块内部声明的变量 在外部 有可能 无法访问
<script>
// 块作用域
for (let i = 1; i < 5; i++) {
// i 只能在当前代码块中被访问
console.log(i);
}
console.log(i); // 报错: 超出了 i 的作用域
</script>
<script>
for (let i = 1; i < 5; i++) {
// 块作用域
console.log(i);
}
for (let i = 1; i < 5; i++) {
// 块作用域
console.log(i);
}
if(true) {
// 块作用域
let i = 1
}
</script>
补充:
- let声明的变量和const声明的常量都会产生块作用域
- var声明的变量不会产生块作用域
- 不同代码块之间的变量无法相互访问
- 推荐使用let和const
小结:
- 局部作用域分为函数作用域和块作用域
- 函数作用域就是函数内部
- 块作用域就是 { } 内部
- 超出作用域范围后, 变量无法访问
全局作用域
<script>标签和 .js文件 的最外层 就是全局作用域, 在此声明的变量可以在任何地方访问
<script>
// 全局作用域
const num = 10
function fn() {
// 全局作用域声明的变量可以在任何位置访问
console.log(num);
}
</script>
注意:
- 为window对象动态添加的属性默认也是全局的, 不推荐
- 函数中不使用关键字声明的变量也是全局变量, 不推荐
- 尽可能减少全局变量, 防止全局变量污染
小结:
- <script>标签 和 .js文件 的最外层 都是全局作用域
- 全局作用域的变量可以在任何地方访问
作用域链
代码有错误码? 如果没有错误打印的结果是几?
<script>
// 全局作用域
let a = 1
let b = 2
function f() {
// 局部作用域1
let a = 1
function g() {
// 局部作用域2
a = 2
console.log(a); //2
}
g()
}
f()
</script>
执行流程:
- 在函数被执行时, 会优先查找当前函数作用域中的变量
- 如果当前作用域找不到, 则会依次逐级查找父级作用域, 直到全局作用域
- g() 作用域 -> f()作用域 -> global作用域
作用域链本质上就是变量的查找机制
总结:
- 嵌套关系的作用域串联起来形成了作用域链
- 相同作用域链中按照从小到大的规则查找变量
- 子作用域能够访问父作用域, 父作用域不能访问子作用域
- 由于作用域链的存在,变量的访问具有就近原则.
垃圾回收机制(GC)
JS中内存的分配和回收都是自动完成的, 内存在不使用的时候会被垃圾回收器自动回收
内存的生命周期
<script>
// 分配内存
const age = 18
// 分配内存
const obj = {
age: 19
}
// 分配内存
function fn() {
const age = 18
console.log(age);
}
</script>
JS环境中分配的内存, 一般有如下生命周期:
- 内存分配: 声明变量/函数/对象的时候, 系统自动为他们分配内存
- 内存使用: 读写内存, 也就是使用变量/函数
- 内存回收: 使用完毕, 由垃圾回收器自动回收不再使用的内存
说明:
- 全局变量一般不回收(关闭页面回收)
- 一般情况下, 局部变的值, 不用了, 就会被自动回收
- 应该被回收的内存, 由于某些原因无法被回收, 就是内存泄漏
算法说明
常见的浏览器垃圾回收算法: 引用计数法 和 标记清除法
堆栈空间分配的区别:
- 栈(操作系统): 由操作系统自动分配/释放函数的参数值, 局部变量等, 基本数据类型放到栈里面
- 堆(操作系统): 一般由程序员分配释放, 若程序员不释放, 由垃圾回收机制回收, 复杂数据类型放到堆里面
引用计数算法:
引用计数法是一个简单有效的算法
<script>
// 示例1
// arr变量引用数组, 数组的引用计数为1
const arr = [1, 2, 3, 4]
// arr变量指向null, 数组的引用计数为0
// 引用计数为0的对象会被回收
arr = null
</script>
<script>
// 定义对象, 对象被person变量引用
// 对象的引用计数为1
let person = {
age: 19,
name: 'zs'
}
// 变量p最终指向对象, 对象的引用计数为2
let p = person
// person不再指向对象,对象的引用计数为1
person = 1
// p不再指向对象, 对象的引用计数为0
p = null
</script>
问题: 引用计数法存在嵌套引用的问题
<script>
// 嵌套引用
function fn() {
let o1 = {}
let 02 = {}
o1.a = o2
02.a = o1
return '引用计数无法回收'
}
fn()
</script>
两个对象互相引用, 尽管他们不再使用, 但是引用次数永远不为0, 垃圾回收器也不会回收,导致内存泄漏
标记清除法
现代浏览器大多是基于标记清除法进行垃圾回收
核心
- 标记清除算法将"无法到达的对象"定义为垃圾
- 从根部(在JS中就是全局对象)出发, 定时扫描内存中的对象, 凡是能从根部到达的对象, 都是还需要使用的
- 那些无法由根部出发触及到的对象被标记为"不再使用",稍后进行回收
小结:
从根部扫描对象, 能找到的就是使用的, 找不到的就要被回收
闭包
基本概念
概念: 一个函数对周围状态的引用捆绑在一起, 内层函数中访问到其外层函数的作用域
简化: 内层函数使用了外层函数的变量, 就形成了闭包
<script>
function outer() {
const a = 1
function f() {
console.log(a)
}
f()
}
outer()
</script>
常见形式
<script>
function outer() {
let a = 100
function fn() {
console.log(a)
}
// 这里是返回函数, 而不是调用函数
return fn
}
// fun === outer() === fn === function fn() {}
const fun = outer()
// 调用函数
fun()
</script>
闭包作用
封闭数据, 提供操作, 外部也可以访问函数内部的变量
<script>
// 需求: 统计函数的执行次数
// 简单写法: 简单, 但是全局变量容易被篡改
let count = 0
function fn() {
count++
console.log(`函数执行了 ${count} 次`);
}
fn() // 1
// 闭包写法: 数据私有, 无法被直接修改
function outer() {
let count = 0
return function fun() {
count++
console.log(`函数执行了 ${count} 次`);
}
}
const result = outer()
result() // 1
result() // 2
</script>
可能的问题
内存泄漏:
闭包返回一个函数, 函数一旦调用, 内部变量就会被引用, 所以不会被垃圾回收机制清除, 就会出现函数执行完毕, 但是函数内部变量未被释放的现象
总结:
- 闭包 = 内层函数 + 外层函数的变量
- 作用: 封闭数据, 实现数据私有, 外部也可以访问函数内部的变量
- 作用: 闭包很有用, 因为它允许将函数与其所操作的某些数据(环境)关联起来
- 问题: 可能会引起内存泄漏
变量提升
变量提升是javaScript中比较奇怪的现象, 它允许在变量声明之前使用变量(仅存在于var声明变量)
变量提升的流程:
- 先把var变量提升到当前作用域的最前面
- 只提升变量声明, 不提升变量赋值
- 然后依次执行代码
<script>
// 原始代码
console.log(num);
var num = 10 // undefined
// 提升后的代码
var num
console.log(num)
num = 10
</script>
补充
- 变量在未声明的时候被访问应该报错
- 在使用var声明变量之前, 访问变量, 变量的值是undefiend
- 变量提升出现在相同作用域中
- 实际开发中推荐先声明再使用
- 变量提升容易出现意想不到的bug, 所以ES6引入了块级作用域
- let/const声明的变量不存在变量提升, 让代码写法更加规范和人性化
函数提升
函数提升与变量提升类似, 函数在声明之前可以被调用
函数提升的流程:
- 会把函数声明提升到当前作用域的最前面
- 只提升函数声明, 不提升函数调用
- 尽量保持先声明再调用的习惯
<script>
// 函数提升前
fun() // 函数调用
function fun() {
console.log('函数可以先调用再声明...')
}
// 函数提升后
function fun() {
console.log('函数可以先调用再声明...')
}
fun()
</script>
<script>
// 提升前
sun() // 报错
var sun = function () {
console.log('数表达式不存在提升现象')
}
// 提升后
var sun
sun() // 报错
sun = function () {
console.log('数表达式不存在提升现象')
}
</script>
补充
- 函数提升可以使函数的声明调用更灵活
- 函数提升出现在相同作用域中
- 函数表达式不存在提升的现象, 必须先声明再使用