深入理解JavaScript中的变量提升与词法环境

前言

之前笔者已经在这篇文章【ES6】让你彻底搞懂const ,let和var的区别里粗略的介绍过var 的变量提升及let和const的暂时性死区,然而光了解这些是远远不够的,本文笔者将深入探讨变量环境、调用栈、执行上下文、词法环境、作用域、块级作用域和作用域链及他们之间的羁绊。

变量环境和词法环境

var的变量提升

首先,让我们回顾一下变量提升的基本原理。在JavaScript中,所有的变量声明(使用var关键字)都会被提升到其所在作用域的顶部,这意味着无论变量声明位于何处,都可以在整个作用域内访问。然而,只有声明部分被提升了;如果同时进行了初始化,则初始化不会被提升。例如:

javascript 复制代码
console.log(a); // 输出 undefined
var a = 1;

这段代码会输出undefined而不是报错,因为虽然变量a的声明被提到了作用域顶部,但它的赋值操作依然保持在原位。

letconst 的引入及暂时性死区 (TDZ)

随着ES6标准的推出,JavaScript引入了新的关键字letconst用于声明变量。这两个关键字带来了对变量声明的不同处理方式。使用letconst声明的变量也存在提升现象,但在实际声明之前尝试访问这样的变量会导致引用错误,这种状态被称为"暂时性死区"(Temporal Dead Zone, TDZ)。

考虑以下代码片段:

javascript 复制代码
console.log(b); // 抛出 ReferenceError
let b = 2;

这里,由于b是用let声明的,在声明之前访问它会抛出一个引用错误。这与使用var的情况不同,后者允许在声明之前访问变量,只是此时该变量的值为undefined

变量环境

变量环境是一个抽象的概念,它代表了当前执行上下文中的所有变量和函数声明。每个执行上下文都有一个与之关联的变量环境,这个环境包含了当前上下文中定义的所有变量和函数。对于全局执行上下文,变量环境就是全局对象(如浏览器中的window或Node.js中的global)。对于函数执行上下文,变量环境通常不可直接访问,但可以通过this关键字间接引用其中的一些属性。

词法环境

词法环境是JavaScript中用于管理变量和函数声明的一种机制,它不仅包含了当前作用域内所有变量和函数的绑定信息,还持有对外部环境的引用,从而形成了一条作用域链。这使得内部作用域能够访问外部作用域中的变量,即使在函数调用结束后也能通过闭包保持对这些变量的访问。每个新的执行上下文(如全局或函数)都会创建一个新的词法环境实例,其中记录了该上下文中定义的所有变量和函数,并通过链接到其外部环境来实现嵌套作用域间的变量查找。这种机制确保了代码中的变量能够根据其声明位置正确地被解析和访问。

调用栈和执行上下文

调用栈是js中管理函数调用的一种机制。在js 程序开始启动时,会先创建一个全局上下文,全局上下文会位于调用栈的底部,每当一个函数被调用时,就会创建一个新的执行上下文并将其推入调用栈顶部。当函数执行完毕后,相应的执行上下文会从调用栈中弹出,控制权返回给调用它的上下文,但如果是直接由全局代码调用的函数,那么控制权将返回给全局执行上下文。

js 复制代码
function a() {
  console.log("a");
  b();
}

function b() {
  console.log("b");
}

first(); // 输出 "a" 和 "b"

在这个例子中,a函数被调用,它的执行上下文被推入调用栈。当a内部调用b时,b的执行上下文又被推入调用栈顶部。一旦b执行完毕,它的执行上下文被移除,然后控制权返回给a,最终整个过程结束。

词法作用域与块级作用域

词法作用域决定了变量在何处可见,基于代码书写的位置来确定变量的作用范围。当一个函数被创建时,它不仅记录了自身的局部变量,还记录了其外部环境的引用。这样,当函数被调用时,它能够访问到其定义时所在环境中的变量。这种通过链接各个嵌套层次上的作用域来查找变量的过程称为作用域链。

接下来,我们来看一个更复杂的例子,它展示了词法作用域、变量提升以及块级作用域的概念:

javascript 复制代码
function foo() {
  var a = 1;
  let b = 2;
  {
    let b = 3; // 块级作用域内的b
    var c = 4; // 尽管c在块内部声明,但它仍然属于foo的作用域
    let d = 5; // 块级作用域内的d
    console.log(a); // 输出 1
    console.log(b); // 输出 3
  }
  console.log(b); // 输出 2
  console.log(c); // 输出 4
  console.log(d); // 抛出 ReferenceError: d is not defined
}
foo();

相信不少人看到这段代码都不知道正确答案是什么及为什么是这样,下面让我用一张图来解释一下:

当foo函数被调用时,js引擎会为该函数创建一个新的执行上下文,其中包括变量环境和词法环境。在这个过程中,使用var声明的变量a和c会被提升到当前作用域的顶部,并初始化为undefined,而使用let声明的变量b(在两个不同作用域中)和d也会被提升,但它们处于暂时性死区(TDZ),直到实际到达声明语句前不能访问。因此,在进入函数体后,a首先被赋值为1,b被赋值为2。接着进入内部块级作用域,这里的b被重新声明并赋值为3,同时c被赋值为4,d被赋值为5。在块级作用境内,console.log(a)输出1,因为a是在外部作用域中定义的;console.log(b)输出3,因为它引用的是最近的作用域内的b。退出块级作用域后,第二次console.log(b)输出2,因为这时它指向的是外部作用域中的b。console.log(c)输出4,因为c也是在foo函数的作用域内定义的。最后,尝试打印d会导致一个引用错误,因为在块级作用域外d是不可见的。

函数间的变量共享与作用域链

另一个重要的概念是关于函数之间共享变量的能力。当我们有一个函数内部调用了另一个函数时,内部函数可以访问外部函数的变量。这是通过作用域链实现的。下面的例子说明了这一点:

javascript 复制代码
function bar(){
  console.log(myname);
}

function foo(){
  var myname = 'zhangsan';
  bar(); // 输出 "lisi"
  console.log(myname); // 输出 "zhangsan"
}

var myname = "lisi";
foo();

这个输出结果应该是出乎了绝大多数人的意料,下面让我来解释一下为什么是lisi、zhangsan。

在这里,bar函数直接访问了全局变量myname,因为在bar的作用域链中并没有找到名为myname的局部变量。而当foo函数执行时,它创建了自己的局部变量myname,但这个变量只在其自身的作用域内有效。因此,当foo调用bar时,bar仍访问的是全局变量myname

相关推荐
xjt_09011 分钟前
基于 Vue 3 构建企业级 Web Components 组件库
前端·javascript·vue.js
我是伪码农13 分钟前
Vue 2.3
前端·javascript·vue.js
夜郎king38 分钟前
HTML5 SVG 实现日出日落动画与实时天气可视化
前端·html5·svg 日出日落
辰风沐阳1 小时前
JavaScript 的宏任务和微任务
javascript
夏幻灵2 小时前
HTML5里最常用的十大标签
前端·html·html5
冰暮流星2 小时前
javascript之二重循环练习
开发语言·javascript·数据库
Mr Xu_2 小时前
Vue 3 中 watch 的使用详解:监听响应式数据变化的利器
前端·javascript·vue.js
未来龙皇小蓝2 小时前
RBAC前端架构-01:项目初始化
前端·架构
程序员agions2 小时前
2026年,微前端终于“死“了
前端·状态模式
万岳科技系统开发2 小时前
食堂采购系统源码库存扣减算法与并发控制实现详解
java·前端·数据库·算法