JavaScript 的工作原理

JavaScript 是一种**高级解释型编程语言**,常用于 Web 开发以创建动态和交互式网页。它可在浏览器(客户端)和服务器(服务器端,使用 Node.js 等环境)上运行。要了解 JavaScript 的工作原理,需要了解其执行模型、环境以及调用堆栈、事件循环和异步行为等关键机制的几个概念。

以下是 JavaScript 工作原理的概述:

1.执行上下文和调用堆栈

JavaScript 代码在**单线程环境**中执行,这意味着一次只能处理一个操作。JavaScript 引擎使用**调用堆栈**来跟踪函数调用。

调用堆栈

  • 执行上下文:执行 JavaScript 代码时,会创建一个执行上下文。有两种类型的上下文:全局上下文:代码不在任何函数内时的默认上下文。函数上下文:调用函数时创建。
  • 调用堆栈:跟踪函数调用的堆栈。当调用某个函数时,它会被添加到堆栈顶部。一旦函数执行完毕,它就会从堆栈中弹出。
  • 例子:
    function first() {
      console.log('First function');
    }
    
    function second() {
      first();
      console.log('Second function');
    }
    
    second();

    **流动**:

  • 调用 second() 函数,将其推送到堆栈。
  • first() 在 second() 中被调用,并将其也推送到堆栈中。
  • 一旦 first() 完成,它就会被弹出,并且程序返回到 second()。
  • 当 second() 完成时,它会从堆栈中弹出,并且程序结束。
  • 2. 事件循环

    JavaScript 本质上是**非阻塞**的,这意味着它可以执行异步任务而不阻塞主线程(浏览器中的 UI 线程)。

  • 事件循环:事件循环负责处理异步操作,它不断检查调用栈是否为空,以及消息队列中是否有需要处理的消息(事件)。
  • 消息队列:异步任务(如网络请求、setTimeout 等)完成后,回调函数会被放入消息队列,事件循环会检查调用栈是否为空,然后将回调从消息队列推送到调用栈。
  • 异步执行示例:
    console.log("Start");
    
    setTimeout(() => {
      console.log("Timeout 1");
    }, 1000);
    
    setTimeout(() => {
      console.log("Timeout 2");
    }, 500);
    
    console.log("End");

    **流动**:

  • console.log("Start") 被执行。
  • 第一个setTimeout被调度,但是它不会阻止执行,并在1秒后进入事件队列。
  • 第二个 setTimeout 被调度并在 500ms 后进入事件队列。
  • console.log("End") 立即执行。
  • 500ms 之后,第二次超时被移到调用堆栈并执行。
  • 1000ms 之后,第一个超时被移到调用堆栈并执行。
  • 输出:

    Start
    End
    Timeout 2
    Timeout 1

    3. 执行环境

    JavaScript 在不同的环境中运行,每个环境都有特定的全局对象和功能:

  • 浏览器环境:包括允许与 HTML 元素交互的 DOM(文档对象模型)和全局对象窗口对象。
  • Node.js 环境:提供对文件系统、网络和其他服务器端功能的访问。它还有自己的全局对象,称为 global。
  • 浏览器中的**全局对象**是`window`,在Node.js中则是`global`。

    4. 同步代码与异步代码

    JavaScript 可以执行同步和异步代码。

  • 同步代码:这种类型的代码是依次执行的。每个语句仅在前一个语句完成后才执行。它本质上是阻塞的,这意味着如果一个操作需要时间(例如,读取文件),整个脚本将等待该操作完成。
  • 异步代码:这种类型的代码不会阻止执行。JavaScript 不会等待操作完成,而是继续执行其余代码。当异步操作完成(如获取数据)时,结果将通过回调、promise 或 async/await 处理。
  • 同步代码示例:
    console.log("A");
    console.log("B");
    console.log("C");

    输出:

    A
    B
    C
    异步代码示例:
    console.log("A");
    
    setTimeout(() => {
      console.log("B");
    }, 1000);
    
    console.log("C");

    输出:

    A
    C
    B

    这里的`setTimeout`是异步的,所以在“C”之后打印“B”。

    5. JavaScript 中的事件处理

    JavaScript 很大程度上依赖于事件,尤其是在 Web 开发中。例如,当您单击按钮时,浏览器会触发一个事件,JavaScript 可以监听并响应该事件。

    事件驱动编程

    在浏览器中,可以使用事件监听器捕获诸如“点击”,“提交”和“加载”等事件。

    const button = document.querySelector('button');
    button.addEventListener('click', () => {
      console.log("Button clicked!");
    });

    6. 闭包

    JavaScript 函数可以“记住”其外部作用域,即使函数在该作用域之外执行也是如此。此功能称为**闭包**。

    function outer() {
      let counter = 0;
      return function inner() {
        counter++;
        console.log(counter);
      };
    }
    
    const increment = outer();
    increment();  // 1
    increment();  // 2

    这里,即使“外部”函数已经执行完毕,内部函数仍然可以访问“计数器”。

    7. 提升

    在 JavaScript 中,**变量和函数声明**在执行阶段会被提升到其范围的顶部,这意味着它们甚至在代码中声明之前就已经可用。

  • 函数声明被完全提升,因此你可以在定义它们之前调用它们。
  • 使用 var 声明的变量会被提升,但只有当代码执行到该点时才会赋值。使用 let 和 const 声明的变量不会以相同的方式提升。
  • console.log(foo()); // works because function is hoisted
    function foo() {
      return 'Hello!';
    }
    
    console.log(bar); // undefined because `bar` is hoisted but not initialized yet
    var bar = 'World';

    8.内存管理和垃圾收集

    JavaScript 会自动管理内存。当对象不再需要时,它们会被标记为**垃圾回收**,并释放其占用的内存。

    JavaScript 使用一种称为**标记-清除**的算法来检测和清理未使用的对象。

    JavaScript 执行流程总结:

  • 全局执行上下文:JavaScript 首先在全局上下文中执行代码。
  • 函数调用:当调用一个函数时,就会创建一个新的执行上下文并将其添加到调用堆栈中。
  • 事件循环和异步代码:异步操作(例如,setTimeout,网络请求)由事件循环处理,允许非阻塞执行。
  • 回调的执行:一旦同步代码完成,事件循环就会将回调从消息队列移动到调用堆栈进行执行。
  • 内存管理:未使用的变量和对象最终会被垃圾收集清理。