[JS]作用域

本节目标

了解作用域对程序执行的影响, 了解作用域链的查找机制, 使用闭包函数创建隔离作用域避免全局变量污染

  • 作用域
  • 作用域链
  • 垃圾回收机制
  • 闭包
  • 变量提升

作用域

作用域规定了 变量 能够被 访问 的"范围", 离开了这个"范围", 变量就不能被访问

分类: 作用域分别为 局部作用域 和 全局作用域

局部作用域

分类: 局部作用域分为 函数作用域 和 块作用域

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环境中分配的内存, 一般有如下生命周期:

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

说明:

  • 全局变量一般不回收(关闭页面回收)
  • 一般情况下, 局部变的值, 不用了, 就会被自动回收
  • 应该被回收的内存, 由于某些原因无法被回收, 就是内存泄漏

算法说明

常见的浏览器垃圾回收算法: 引用计数法 和 标记清除法

堆栈空间分配的区别:

  1. 栈(操作系统): 由操作系统自动分配/释放函数的参数值, 局部变量等, 基本数据类型放到栈里面
  2. 堆(操作系统): 一般由程序员分配释放, 若程序员不释放, 由垃圾回收机制回收, 复杂数据类型放到堆里面

引用计数算法:

引用计数法是一个简单有效的算法

<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, 垃圾回收器也不会回收,导致内存泄漏

标记清除法

现代浏览器大多是基于标记清除法进行垃圾回收

核心

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

小结:

从根部扫描对象, 能找到的就是使用的, 找不到的就要被回收

闭包

基本概念

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

简化: 内层函数使用了外层函数的变量, 就形成了闭包

 <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声明变量)

变量提升的流程:

  1. 先把var变量提升到当前作用域的最前面
  2. 只提升变量声明, 不提升变量赋值
  3. 然后依次执行代码
 <script>
    // 原始代码
    console.log(num);
    var num = 10  // undefined

    // 提升后的代码
    var num
    console.log(num)
    num = 10
  </script>

补充

  1. 变量在未声明的时候被访问应该报错
  2. 在使用var声明变量之前, 访问变量, 变量的值是undefiend
  3. 变量提升出现在相同作用域中
  4. 实际开发中推荐先声明再使用
  5. 变量提升容易出现意想不到的bug, 所以ES6引入了块级作用域
  6. let/const声明的变量不存在变量提升, 让代码写法更加规范和人性化

函数提升

函数提升与变量提升类似, 函数在声明之前可以被调用

函数提升的流程:

  1. 会把函数声明提升到当前作用域的最前面
  2. 只提升函数声明, 不提升函数调用
  3. 尽量保持先声明再调用的习惯
<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>

补充

  1. 函数提升可以使函数的声明调用更灵活
  2. 函数提升出现在相同作用域中
  3. 函数表达式不存在提升的现象, 必须先声明再使用
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值