带你分析词法环境的“区别对待”以及JS 块级作用域与词法作用域攻略

144 阅读7分钟

前言

想象一下,你是一位探险家,正准备踏上一段充满未知与奇迹的旅程。在这段旅程中,你将探索JavaScript这片广阔而神秘的领域,揭开作用域与词法环境的面纱,发现那些隐藏在代码背后的秘密。

执行上下文、变量提升、调用栈、暂时性死区在我之前的文章已有提及从前端初学者到高手的必经之路:深入理解JavaScript变量提升与执行上下文在编程的海洋中,JavaScript无疑是 - 掘金

今天重点讲解块级作用域、词法作用域、词法环境。

词法作用域与词法环境以及块级作用域

词法环境

  • 概念: 词法环境是执行上下文的一部分,用于存储变量映射。每个执行上下文都有自己的词法环境。
  • 区别对待:ES6通过区分let/const的词法环境与var的变量环境,实现了对变量声明前不可访问的控制,即暂时性死区。

先来个案例让大家看看词法环境的作用

案例一:

// hoisting
console.log(a,fun);
console.log(b); // 词法环境中的变量/常量,在声明之前不可访问
// 暂时性死区 TDZ 
var a=1;
function fun(){
   
}

let b=2;
b++;// 词法环境里查找

// 执行结果为报错:

结果: 进行了报错,但输出了a和fun的值

undefined [Function: fun]
E:\workspace\lesson_hm\js\v8_runtime\1.js:3
console.log(b); // 词法环境中的变量/常量,在声明之前不可访问

图解:

image.png 讲解:

  • 首先我们可以看到,每有一个执行上下文,必然含有各自的变量环境和词法环境,而在变量环境中一般存放的都是varfunction类型,而词法环境中就是letconst类型,就就其中的区别对待了。
  • 而为什么可以输出a和fun,却不能输出b,这是因为词法环境中的变量/常量,在声明之前不可访问,存在暂时性死区。

块级作用域

块级作用域(Block Scope)是指在特定代码块(通常由一对花括号 {} 包围的代码区域)内声明的变量仅在该代码块内部可见。这种作用域机制使得变量的作用范围更加精确,有助于避免变量污染和命名冲突问题。在JavaScript中,letconst 关键字引入了块级作用域的概念。

块级作用域的特点
  1. 局部性:使用 let 或 const 在一个块内声明的变量,只在该块及其子块中可见。
  2. 暂时性死区 (TDZ) :在块内,let 和 const 声明的变量在声明之前不可访问。尝试访问会导致引用错误。
  3. 不提升:与 var 不同,let 和 const 声明的变量不会被提升到当前作用域的顶部。这意味着在声明之前访问它们会导致引用错误。
代码案例:

function foo(){
    var a=1;
    let b=2;
    {
        let b=3;
        var c=4;// 不支持块级作用域
        let d=5;
        console.log(a);
        console.log(b);
    }//运行完销户
    console.log(b);
    console.log(c);
    console.log(d);
}
foo();

大家可以猜猜结果是什么:

结果当然报错了

1
3
2
4
E:\workspace\lesson_hm\js\v8_runtime\2.js:14
    console.log(d);

图解:

iwEeAqNwbmcDAQTRAe8F0QF-BrD7TSvCQm7cYgctFB5KaRAAB9IoJU2sCAAJomltCgAL0gAA7OI.png_720x720q90.jpg

  • 因为var定义的标识不会被块级作用域所约束,所以var c 其实是声明于foo的全局之中。

  • a 为什么结果为1 呢? 那是因为规则是当该括号内的元素查找不到时,就会去外面查找是否有a这个元素,如下图的箭头一样的顺序进行查找。

  • 那么可能会疑惑,为什么词法环境中有两块区域隔开了,它的原理就如同外面的调用栈一般,先进后出,上面的b 先进行声明所有被压在下面,而块级作用域的元素位于函数的下面,故后进入词法环境中。

    {
        let b=3;
        var c=4;// 不支持块级作用域
        let d=5;
        console.log(a);
        console.log(b);
    }
  • 倘若,块级作用域中没有再一次声明b,那么就会从输出3变成2,查找原理和下图相同。
  • 最后解释一下为什么会报错,因为当块级元素里的工作完成后,就会把原来的在块级作用域中的函数进行销毁,且查找顺序是由内到外,是不能由外到内的,所有沿着作用域链查找,最终也没有找到d,因此抛出ReferenceError: d is not definediwEdAqNwbmcDAQTRAe8F0QFhBrDW_kMFqU0nsgctFCLCLjEAB9IoJU2sCAAJomltCgAL0gABb7M.png_720x720q90.jpg

词法作用域

最后来聊聊词法作用域,它的概念很简单,也很有意思,虽然和词法环境很像,但它们概念没有什么关系。

  • 词法作用域(Lexical Scoping)是JavaScript中一种确定变量和函数可见性与访问性的机制。它基于代码在编写时的位置来决定变量的作用域,而不是基于代码的执行流程。词法作用域也被称为静态作用域,因为它在编译阶段就已经确定了。
  • 简单来说就是:定义在哪个域中 就属于那个词法作用域
案例
function bar(){
    console.log(myname);
}
function foo(){
    var myname="zhangsan";
   
    bar();
    console.log(myname);
}
var myname="lily";
foo();
// function bar(){
//     console.log(myname);
// }
function foo(){
    var myname="zhangsan";
    function bar(){
        console.log(myname);
    }
    bar();
    console.log(myname);
}
var myname="lily";
foo();

大家可以好好看看这两段代码的区别,并且始终记住一个概念:定义在哪个域中 就属于那个词法作用域,再尝试去分析一下结果

结果:

  • lily zhangsan
  • zhangsan zhangsan 如何呢,你分析对了吗?

第一个案例分析:

我们先看看图:

iwEcAqNwbmcDAQTRAhgF0QL2BrCA-3-UDQlVKActFCexHv8AB9IoJU2sCAAJomltCgAL0gADID4.png_720x720q90.jpg 遇到难题,咱们还得画图呀!

解释: 我们可以看到,barfoo 都声明在全局中,那么它们的词法作用域也就在全局中,所以当foo()函数运行时,中的bar()运行后,在bar作用域中,无法找到myname属性,接着不是去foo中查找,而是在全局中查找,因为函数声明于全局之中。

图解: iwEcAqNwbmcDAQTRAmcF0QMDBrDqC9A7g_9bCActFCwz_ksAB9IoJU2sCAAJomltCgAL0gAEPGM.png_720x720q90.jpg 在这张图中,outer 指的是当前执行上下文的外部执行上下文。 也就是在当前作用域找不到的标识符,它下一步去哪里找。

第二个案例分析:

怎么样当我讲完第一个案例分析,是不是第二个就很清晰了。

解释: 在第二个案例中,我们可以看与第一个案例不同的是:bar函数声明在foo函数之中,根据原理,在哪儿声明,作用域也就属于哪儿。当foo函数中的bar运行时,因为找不到myname属性,那么引擎就会去foo函数中查找,这个案例中foo函数声明了myname属性,如果没有的话,就再去全局查找, 这就是作用域链将它们无形的连接。

结语

通过上述内容,我们深入探讨了JavaScript中的词法环境、块级作用域以及词法作用域的概念。想必你已经理解的很好了,下面是对本文要点的总结:

  • 词法环境 是执行上下文的一部分,用于存储变量映射。它区分let/constvar的不同处理方式,尤其是暂时性死区(TDZ)的存在,确保了在声明之前不可访问letconst定义的变量。
  • 块级作用域 通过letconst关键字引入,使得变量的作用范围更加精确,有助于避免全局污染和命名冲突。这种机制支持局部性,并且在声明前尝试访问会触发引用错误。
  • 词法作用域 是基于代码在编写时的位置来决定变量的作用域,而不是基于运行时的情况。这意味着函数在何处被声明决定了它的词法作用域,在这个范围内可以访问外部变量。