自学内容网 自学内容网

JavaScript 知识点 - 作用域(变量提升、垃圾回收机制、闭包)

一、作用域

1、基本概念

是什么?

指变量、对象和函数在【代码中的可访问性范围】。

有什么用?

理解作用域对【编写高效和无错误的代码】至关重要

分类

局部作用域(函数作用域、块作用域)、全局作用域

涉及到那些知识点

作用域链、JS垃圾回收机制、闭包、变量提升等。

2、全局作用域

是什么?

在代码的任何地方都可以访问的变量和函数

怎么产生?

(1)【<script>标签和.js文件的最外层】声明的变量和函数

(2)【window 对象动态添加的属性】默认也是全局的

注意事项

实际开发中尽可能少的声明全局变量、防止全局变量被污染

3、局部作用域

【1】函数作用域

是什么?

只能在【变量或函数 所处的 函数内部】被访问的变量和函数

有什么用?

用于限制变量的可见性,避免与【其他函数或全局作用域中的变量产生冲突】

怎么产生?

(1)【函数内部】声明的变量和函数

(2)【函数的参数】 也是函数内部的局部变量

【2】块作用域

是什么?

块级作用域是【指在 {} 代码块内部】声明的变量(var声明的除外var 声明的变量不会产生块级作用域),在该块外部无法访问。

怎么产生?

【在 {} 代码块内部】声明的变量(var声明的除外)

示例

// var 声明变量 不形成块级作用域、 i 是全局作用域的
    for(var i = 1; i < 10; i++) {
      console.log(i, 'i');
      setTimeout(() => {
        console.log(i, 'setTimeout i');
      }, 100)
    }
    
    // let 声明变量 形成块级作用域
    for(let i = 1; i < 10; i++) {
      console.log(i, 'i');
      setTimeout(() => {
        console.log(i, 'setTimeout i');
      }, 100)
    }

第一段代码输出结果

  • 先打印 1 到 9,每个数字后面跟着 'i'
  • 当 setTimeout 的回调函数执行时,由于 var 使得 i 是全局作用域的,最终 i 的值是 10,所以将会打印 10,并且会重复打印 9 次。

第二段代码输出结果

  • 先打印 1 到 9,每个数字后面跟着 'i'
  • 由于 let 使得 i 在每次循环迭代中都具有块作用域,因此在 setTimeout 执行时,每个回调函数的 i 都是当前迭代的值。

二、作用域链

基本概念

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

查找原则

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

同一作用域内,变量按【声明的顺序进行查找】。

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

三、变量提升

1、基本概念

        变量提升 JavaScript 的一个机制JavaScript 引擎在执行代码时,会先查找 函数 和 var 声明的变量 提升到当前所处作用域顶部。

        ***变量提升只提升声明,不提升赋值***

2、注意点

函数提升

(1)  函数表达式不存在提升的现象

(2)  函数提升出现在相同的作用域中

变量提升

(1) 变量在未声明即被访问时会报语法错误

(2) 变量在 var 声明之前即被访问、变量的值为 undefined

(3) let/const 声明的变量不存在变量提升

(4) 变量提升出现在当前相同作用域当中

(5) 实际开发中推荐先声明再访问变量

3、DEMO 

// 全局作用域
    var globalVar = "I'm a global variable";

    function testHoisting() {
      console.log(globalVar); // 输出: "I'm a global variable"
      
      // 变量提升
      console.log(localVar); // 输出: undefined (因为声明被提升)
      var localVar = "I'm a local variable";
      console.log(localVar); // 输出: "I'm a local variable"

      // 函数提升
      console.log(innerFunction()); // 输出: "Inner function called!"

      function innerFunction() {
          return "Inner function called!";
      }

      // let 和 const 的提升
      try {
          console.log(letVar); // ReferenceError: Cannot access 'letVar' before initialization
      } catch (e) {
          console.log(e.message); // 捕获错误信息
      }
      
      let letVar = "I'm a let variable";
      const constVar = "I'm a const variable";
      
      console.log(letVar);  // 输出: "I'm a let variable"
      console.log(constVar); // 输出: "I'm a const variable"
    }

    testHoisting();

    console.log(globalVar); // 输出: "I'm a global variable"

四、JavaScript 的垃圾回收机制

基本概念

        JavaScript 的垃圾回收机制是指【JavaScript引擎会自动检测和清理不再使用的内存空间】,以【防止内存泄漏和提高性能】。JavaScript 使用一种自动内存管理的方式,开发者无需手动释放内存。
 

内存的生命周期

(1) 【内存分配】: 当我们声明变量、函数、对象的时候、系统会自动为他们分配内存

(2) 【内存使用】: 即读写内存、也就是使用变量、函数等

(3) 【内存回收】: 使用完毕、由【垃圾回收器】自动回收不再使用的内存

注意点

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

(2) 一般情况下【局部变量的值】、不用了、会被【自动回收】掉

内存泄漏

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

JavaScript 垃圾回收机制算法

1. 引用计数法 (早期少数浏览器使用)

原理:
  • 每个对象维护一个引用计数器,记录有多少个引用指向该对象。
  • 当一个对象被引用时,引用计数加一;当引用被移除时,引用计数减一。
  • 当引用计数为零时,说明没有任何引用指向该对象,可以安全地释放它的内存。
优点:
  • 实时性:能立即回收不再使用的对象,减少内存占用。
缺点:
  • 循环引用问题:如果两个对象相互引用,即使它们不再被其他对象引用,它们的引用计数也不会变为零,从而导致内存泄漏。

2. 标记清除法 (大多数现在浏览器使用)

原理:
  • 标记阶段:从根对象(如全局对象、活动函数的变量等)开始,递归地遍历所有可达的对象,并将其标记为“活着”。
  • 清除阶段:遍历所有对象,将那些未被标记的对象视为“死去”的,释放它们所占用的内存
优点:
  • 能够处理循环引用的问题,因为只要对象不可达,就会被回收。
缺点:
  • 性能开销:标记和清除的过程可能会消耗较多的 CPU 资源,导致应用程序在垃圾回收期间可能出现性能波动。

五、闭包

基本概念

        闭包(closure)是一个函数以及其捆绑的周边环境状态的引用的组合。换而言之,闭包让开发者可以从内部函数访问外部函数的作用域。在 JavaScript 中,闭包会随着函数的创建而被同时创建。

        简单来讲、一个函数 内部函数 引用了 外部函数的变量就会形成闭包。

作用

1、" 保护"作用

说明:

闭包可以创建私有变量,避免外部访问,从而保护变量不被随意修改。这种封装特性使得数据更安全,减少了全局作用域污染。

示例:

count 是一个私有变量,只有通过 incrementdecrementgetCount 方法才能访问和修改,保护了变量的安全性。

function createCounter() {
    let count = 0; // 私有变量

    return {
        increment: function() {
            count++;
            console.log(count);
        },
        decrement: function() {
            count--;
            console.log(count);
        },
        getCount: function() {
            return count;
        }
    };
}

const counter = createCounter();
counter.increment(); // 输出: 1
counter.increment(); // 输出: 2
console.log(counter.getCount()); // 输出: 2
// 直接访问 count 是不可能的
// console.log(counter.count); // undefined

2、" 保存"作用

说明:

        闭包能够保存外部函数的变量状态,即使外部函数已经执行完毕。这使得我们可以在异步操作或定时器中保留某个状态。

示例:

   timerId 被闭包保护并保存。即使 makeTimer 函数执行结束,timerId 的值依然可以被 startstop 方法访问和修改。

function makeTimer() {
    let timerId = 0;

    return {
        start: function() {
            timerId = setInterval(() => {
                console.log(`Timer ID: ${timerId}`);
            }, 1000);
        },
        stop: function() {
            clearInterval(timerId);
            console.log('Timer stopped');
        }
    };
}

const timer = makeTimer();
timer.start(); // 每秒输出 Timer ID
setTimeout(() => {
    timer.stop(); // 3秒后停止计时器
}, 3000);

产生的问题

内存泄漏

        闭包会保持对其外部作用域的引用,javaScript 的垃圾回收机制无法清除释放内存。从而产生内存泄漏问题、故使用闭包时需及时清理引用。

function createCounter() {
    let count = 0;

    return {
        increment: function() {
            count++;
        },
        getCount: function() {
            return count;
        }
    };
}

const counter = createCounter();
// 使用完后,清理引用
counter.increment = null;

变量状态混淆

        当多个闭包引用同一个外部变量时,可能会导致所有闭包共享该变量的最终值。

const functions = [];

for (var i = 0; i < 3; i++) {
    functions.push(function() {
        console.log(i);
    });
}

// 输出结果是 3, 3, 3
functions.forEach(func => func());

性能问题

        过度使用闭包可能会导致性能下降。每次创建闭包都会产生额外的函数和作用域,过多的闭包可能导致内存和性能的开销增加。


原文地址:https://blog.csdn.net/qq_61723274/article/details/142626284

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!