深入了解JS作用域链,浏览器中JS的执行机制

前言

在之前的文章详细介绍了作用域和预编译,作用域链和这两者都有着紧密的联系。JavaScript 的作用域链(Scope Chain)是一个重要的概念,它决定了在代码执行过程中如何查找变量和函数。理解作用域链需要了解 JavaScript 中的词法作用域、执行上下文和作用域链的关系。

执行上下文

  • 在 JavaScript 中,执行上下文(Execution Context)是代码执行时的环境。每当 JavaScript 代码运行时,都会创建一个执行上下文。
  • 每个执行上下文都有自己的变量对象(Variable Object),用于存储变量、函数和参数。
  • 在 JavaScript 中,执行上下文对象在以下情况下会被创建:
  1. 全局执行上下文:当 JavaScript 程序开始执行时,会创建一个全局执行上下文,代表整个 JavaScript 文件的运行环境。这是代码中的最顶层执行上下文。

  2. 函数调用:每次调用函数时,都会创建一个新的执行上下文对象。这意味着每个函数调用都有自己的执行上下文,其中包含了函数的局部变量、参数以及在函数内部声明的其他内容。

  3. Eval 函数 :当使用 eval() 函数执行动态代码时,也会创建一个执行上下文对象。

  • 在预编译的时候会创建执行上下文对象和可执行的代码两部分,其中执行上下文里包含变量环境和词法环境,如下图所示:
js 复制代码
var a = 2;
function add(){
    var b = 10;
    return a + b;
}
add();

上面一段代码在预编译时先创建一个全局执行上下文,如下图所示,a的值在预编译时被赋值为undefined,在执行代码时被赋值为2

当执行到函数add的调用时,会创建一个add执行上下文,如下图所示:

而执行上下文是通过什么管理的呢,这就不得不提到栈结构了,JS引擎就是通过栈结构来管理执行上下文的。

调用栈

  • 栈结构:特殊的数组,后进先出

下面的一段代码你知道会打印出来的结果是什么吗?

js 复制代码
function foo(){
    var a = 1;
    let b = 2;
    {
        let b = 3;
        var c = 4;
        let d = 5;
        console.log(a);//1
        console.log(b);//3
    }
    console.log(b);//2
    console.log(c);//4
    console.log(d);//error
}
foo();

在执行上下文中查找值是先在词法环境中找,在词法环境中不可以找到值再去变量环境中找值,这也是为什么第9行代码中打印出来的结果是3,因为词法环境里面是一个栈结构,在foo函数中先声明let b = 2,先将这个b压入栈底,后面声明let b = 3,再将其压入栈中,查找时是从上往下,所以查找出来b的值为3。

  • 调用栈: JavaScript 中的调用栈是一个用于管理函数调用的数据结构,它遵循后进先出(LIFO)的原则。每当执行一个函数时,其对应的执行上下文会被推入调用栈,当函数执行完成后,执行上下文会被弹出。这个过程反复进行,直到调用栈为空。

举个例子:

javascript 复制代码
function firstFunction() {
    console.log("Inside firstFunction");
    secondFunction();
}

function secondFunction() {
    console.log("Inside secondFunction");
    thirdFunction();
}

function thirdFunction() {
    console.log("Inside thirdFunction");
}

firstFunction();

在执行这段代码时,调用栈的变化如下:

  1. 程序开始执行时,创建全局执行上下文,并将其推入调用栈中。
  2. firstFunction 被调用,创建 firstFunction 的执行上下文,并将其推入调用栈中。
  3. firstFunction 中调用 secondFunction,创建 secondFunction 的执行上下文,并将其推入调用栈中。
  4. secondFunction 中调用 thirdFunction,创建 thirdFunction 的执行上下文,并将其推入调用栈中。
  5. thirdFunction 执行完毕,其执行上下文从调用栈中弹出。
  6. secondFunction 执行完毕,其执行上下文从调用栈中弹出。
  7. firstFunction 执行完毕,其执行上下文从调用栈中弹出。
  8. 程序执行完毕,全局执行上下文从调用栈中弹出。

在这个过程中,调用栈记录了函数调用的顺序和当前的执行位置。如果函数调用嵌套太深,或者出现了递归调用没有终止条件,就可能导致调用栈溢出错误(stack overflow)。

  • 栈溢出: 调用栈的内存超出限制

作用域链

在 JavaScript 中,每个函数都会创建一个新的执行上下文,每个执行上下文都有自己的变量对象。当函数被调用时,会创建一个新的执行上下文,并将其推入执行上下文栈(Execution Context Stack)中,形成一个执行上下文的链式结构,即作用域链。

js 复制代码
function bar(){
    console.log(myName);// 结果为Jerry
}

function foo(){
    var myName = 'Tom';
    bar();
}

var myName = 'Jerry';

foo();

在作用域链中,outer 指向的是外部(上一级)执行上下文的作用域。这与词法作用域有关,bar函数声明在了全局作用域里,所以bar的outer指向全局执行上下文,同理,foo函数的声明也在全局作用域里,所以foo的outer也指向全局执行上下文。所以最终的打印结果是Jerry。

  • 作用域链并不是在调用栈中从上到下查找,而是看当前执行上下文变量环境中的outer指向来定,而outer指向的规则是,我的词法作用域在哪里,outer就指向哪里。
  • 词法作用域: 函数定义是所在时所在的作用域

结语

理解作用域链对于理解 JavaScript 中的变量作用域和作用域嵌套非常重要,这对于编写可维护、可扩展的代码至关重要。希望这篇文章可以给你带来帮助。

相关推荐
Allshadow8 分钟前
Nest.js - 目录结构
javascript·nestjs
张元清16 分钟前
React useCookie Hook:把 Cookie 变成响应式状态(2026)
javascript·react.js
Helen_cai28 分钟前
OpenHarmony 项目统一全局样式、尺寸、色彩主题封装 ThemeUtil(API23)
开发语言·前端·javascript·华为·harmonyos
winfredzhang1 小时前
用 wxPython + ECharts + 阿里矢量地图,做一个可离线兜底的上海雨量看板
前端·javascript·echarts
索西引擎1 小时前
【React】Immer.js 在现代 Redux 生态中的角色:不可变性保障的工程化实现与开发体验优化
前端·javascript·react.js
只一1 小时前
拿捏大模型输出随机性:Temperature、Top-K 原理 + LangChain 工程落地实战
javascript·langchain
胡萝卜术1 小时前
抽象的三级跳:从原生 DOM 到 React 组件树,我们到底在解决什么问题?
前端·javascript·面试
jun_bai1 小时前
Orthanc for Windows版PACS服务器安装教程
javascript·扩展屏应用开发
GuWen_yue1 小时前
Cursor黑盒拆解!1套LangChain.js手写Mini编程Agent,自动生成React项目,效率提升60%
javascript·react.js·langchain
用户298698530142 小时前
React 项目中灵活管理 Excel 工作表:增删与移动
javascript·react.js·excel