通过”军机处“来认识JavaScript的作用域

298 阅读5分钟

前言

假设你是皇帝,你有一个太监,由于你每天起来将会有很多事需要等着你去做,所以你不会在一件事情上花费过多,就比如批奏折,你不会一份一份地去看,你会先让军机处帮你去进行初步的整理等工作,而你只需审阅呈上来的奏折。在这里面,军机处做的事情就叫做编译

编译

通俗来说就是整理代码中的规则,编译做的事情有三。

  • 一.词法分析

这是编译过程的第一步,也称为扫描或分词。在这个阶段,源代码被分解成一系列称为词法单元的小片段,每个词法单元对应着源代码中的一个关键字、标识符、运算符、标点符号或者字面量等。如

var a = 123  // 被分解为var, a, =, 123  
  • 二是解析

表示了源代码的结构,包括程序的各个部分如何组合在一起,以及它们之间的关系。

  • 三是生成代码

生成目标代码。

话不多说,我们直接来认识作用域。

作用域

什么是作用域?
作用域(Scope)在编程语言中是一个基本而重要的概念,它定义了变量函数以及对象等标识符在何处可以被访问。简单来说,作用域规定了代码中哪些部分可以“看到”或者访问到哪些变量。通俗讲,即皇上能看到什么,军机处能看到什么。
作用域主要有两种类型:①全局作用域②函数作用域;但在JavaScript中还引入了两种额外的作用域概念:③块级作用域④词法作用域(也叫静态作用域)。

全局作用域

  • 全局作用域:在整个程序范围内都可访问的变量和函数定义于此。在浏览器中,全局变量属于window对象。

函数作用域

  • 函数作用域:在函数内部声明的变量只能在该函数内部访问。

词法作用域

  • 词法作用域(静态作用域) :变量的作用域在编写代码时就已经确定,由其在源代码中的位置决定,而不是在运行时决定。
    我们举个例子来更好认识这词法作用域。
    例子:
var a = 1

function foo() {
    var a = 2
}

foo()

console.log(a);//输出1

lQLPKG9wVo6H0snNBDjNB4CwI6ivUoaxUK0GOvldbqKPAA_1920_1080.png

代码从上往下运行,代码先执行a=1,再执行foo(),此时将会执行foo()函数内的内容,再输出a。但由于外部作用域a为1,外部作用域不能访问内部作用域,所以输出的结果a为1。

image.png

再比如这个例子

function foo(a) {
    var b = a * 2

    function bar(c) {
        console.log(a, b, c)// 输出2,4,12
    }
    bar(b * 3)
}

foo(2)

这两个例子告诉我们词法作用域的规则:内部作用域可以访问外部作用域,反之则不行

块级作用域

  • 块级作用域:这是通过letconst关键字引入的。块级作用域限定了变量的生命周期于最近的一对花括号 {} 内,比如在if语句、for循环或自定义的块中。
{
    let a = 1
}
console.log(a);

这段代码将会输出 -- ReferenceError: a is not defined 因为{}限定了变量的生命周期于最近的一对花括号 {} 内。
但如果是以下这种情况,代码也不能正常运行。

let a = 1
{
    console.log(a); // 暂时性死区
    let a = 2
}

这种情况会输出--ReferenceError: Cannot access 'a' before initialization。这种情况是因为发生了暂时性死区

暂时性死区

什么是暂时性死区?
暂时性死区(Temporal Dead Zone, TDZ)是JavaScript中的一个概念,特指在使用letconst关键字声明变量时,变量在声明之前无法被访问或使用的那段代码区域。

console.log(a);
const a = 1

这段代码也同样输出--ReferenceError: Cannot access 'a' before initialization
但是如果是用var声明,这将是不一样的情况。

 {
     var b = 2
 }
 console.log(b);

这段代码将正常输出2

image.png

补充知识

欺骗词法作用域

  1. eval() 可以执行一个字符串作为JavaScript代码。由于eval()执行的代码能够访问自身作用域,它可以访问或修改封闭作用域中的变量,从而“欺骗”词法作用域。通俗讲就是让原本不属于这里的代码,变得好像天生就定义在了这里一样。
    例如:
 function foo(a, str) {
     eval(str); // var b = 2
     console.log(a,b);
 }
= foo(1, 'var b = 2')

这段代码中的eval(str);相当于var b = 2
2. with() {} 这个语句可以改变作用域链,允许直接访问某个对象的属性,仿佛它们是当前作用域的一部分。当修改对象中不存在的属性时,这个属性会被泄露到全局,变成全局变量。 例如:

    var obj = {
    a: 1
    };
    with (obj) { 
    console.log(a); // 输出1,因为a被当作obj的属性访问 
    b = 2; // 如果b不存在于obj中,这行代码可能意外地创建了一个全局变量b 
    }

varlet的不同

  1. var 声明的变量存在声明提升 例如:
 {
     var b = 2
 }
 console.log(b);// 输出2

 {
     let a = 1
 }
 console.log(a);//输出-- ReferenceError: a is not defined

  • 使用let声明的变量遵循块级作用域规则,出了定义它的块就不可访问。
  • 使用var声明的变量尽管在块内定义,但由于变量提升,因此可以在块外访问。
  1. var 在全局声明的变量会添加到window对象上
    在浏览器环境中,任何函数外部声明一个var变量,它将成为全局变量(在浏览器中,全局变量是window对象的属性);let只在声明它的块中有效。

image.png 3. 重复声明

  • var: 允许在同一作用域内多次声明同一个变量,这可能会导致意外覆盖变量值,增加代码的不确定性。
  • let: 禁止在同一作用域内重复声明同一个变量,如果尝试再次声明,会抛出语法错误(SyntaxError),这有助于防止意外覆盖和提高代码的健壮性。
var a = 123
console.log(a); // 输出123
var a = 321

代码正常运行

let b = 123
console.log(b);
let b = 321

抛出异常--SyntaxError: Identifier 'b' has already been declared 'b'已经被声明。

以上内容,如有错误,请大佬纠正。