Skip to content

作用域进阶

没错, 又是作用域, 但是这次是进阶的了

作用域链

作用域链本质上是底层的变量查找机制

在函数被执行时, 会优先查找当前函数作用域中查找变量

如果当前作用域查找不到则会依次逐级查找父级作用域直到全局作用域

js
// 全局作用域
let A = 1
let B = 2
// 局部作用域
function f() {
    let A = 1
    // 局部作用域
    function g() {
        A = 2
        console.log(A)
    }
    // 调用g
    g()
}
// 调用f
f()

查找顺序: g->f->global

总结

  1. 嵌套关系的作用域串联起来形成了作用域链
  2. 相同作用域链中按着从小到大的规则查找变量
  3. 子作用域能访问父作用域, 父作用域无法访问子作用域

垃圾回收机制

垃圾回收机制(Garbage Collection)简称GC

JS中内存的分配和回收都是自动完成的, 内存在不使用的时候会被垃圾回收器自动回收

内存的生命周期

JS环境中分配的内存, 一般有以下生命周期:

  1. 内存分配: 当我们声明变量, 函数, 对象的时候, 系统会自动为他们分配内存
  2. 内存使用: 即读写内存, 也就是使用变量, 函数等
  3. 内存回收: 使用完毕, 由垃圾回收期自动回收不再使用的内存

全局变量一般不会回收(关闭页面才回收)

一般情况下局部变量的值, 不用了, 会被自动回收

内存泄漏: 程序中分配的内存由于某种原因, 程序未释放无法释放叫做内存泄漏

算法说明

堆栈空间分配区别:

堆(操作系统): 由操作系统自动分配释放函数的参数值, 局部变量等, 基本数据类型放在栈里面

栈(操作系统): 一般由程序员分配释放, 若程序员不释放, 由垃圾回收机制回收.复杂数据类型放到堆里面

下面介绍一下两种常见浏览器的垃圾回收机制算法: 引用计数法标记清除法

引用计数法

IE采用的引用计数法, 定义内存不再使用, 就是看一个对象是否有指向它的引用, 没有引用了就回收对象

算法
  1. 跟踪记录被引用的次数
  2. 如果被引用了一次, 那么就记录次数1, 多次引用会累加++
  3. 如果减少一个引用就减1 --
  4. 如果引用次数是0, 则释放内存
致命问题

嵌套引用(循环引用)

如果两个对象相互引用, 尽管他们已不再使用, 垃圾回收器也不会进行回收, 导致内存泄漏

js
function fn () {
    let A = {}
    let B = {}
    A.c = B
    B.c = B
    return "引用计算无法回收"
}

因为他们的引用次数永远不会是0

这样的相互引用, 如果说很大量的存在就会导致大量的内存泄漏

标记清除法

现代浏览器已经不再使用引用计数法了

现代浏览器通用的大多是基于标记清除法的某些改进算法, 总体思想都是一致的

算法
  1. 标记清除算法将不再使用的对象定义为无法达到的对象
  2. 就是从根部(在JS中就是全局对象)出发定时扫描内存中的对象.凡是能从根部到达的对象, 都是还需要使用
  3. 那些无法由根部出发触及到的对象被标记为不再使用, 稍后进行回收

闭包

一个函数对周围状态捆绑在一起, 内层函数中访问到其他外层函数的作用域

简单理解: 闭包 = 内层函数 + 外层函数的变量

看个例子:

js
function f() {
    const A = 1
    function g() {
        console.log(A)
    }
    g()
}
f()

闭包的作用: 封闭数据, 提供操作, 外部也可以访问函数内部的变量

js
function Outer() {
    let i = 1
    return function () {
        console.log(i)
    }
}
const Fun = Outer()
Fun()

危险

闭包容易引起内存泄漏

应用

实现数据的私有

比如, 我们要做个统计函数调用次数, 函数调用一次, 就++

不使用闭包

js
let Count = 1
function Fn() {
    Count++
    console.log(Count)
}

Fn()
// 2

// 尝试修改计数
Count = 100

Fn()
// 101

使用闭包

js
function Fn() {
    let Count = 1
    function Fun() {
        Count++
        console.log(Count)
    }
    return Fun
}

const Result = Fn()

Result()
// 2

// 尝试修改计数
Count = 100

Result()
// 3

总结

闭包可以做到数据的私有, 不会被外部修改

变量提升

变量提升是JS中比较奇怪的现象, 它允许在变量声明之前即被访问(仅存在于var声明变量)

举个例子:

js
function Fn() {
    console.log(Num)
    var Num = 10
}
Fn()
// undefined

警告

  1. 变量在未声明即被访问时会报语法错误
  2. 变量在var声明之前即被访问, 变量的值为undefined
  3. letconst声明的变量不存在变量提升
  4. 变量提升出现在相同作用域当中
  5. 实际开发中推荐先声明再访问变量