JavaScript的幕后秘籍:变量声明与作用域的艺术

259 阅读6分钟

JavaScript中的变量声明与作用域解析

在JavaScript的世界里,每一行代码的执行都离不开一个强大的幕后团队——JavaScript引擎。以Chrome浏览器中的V8引擎为例,它就像是一家公司的CEO(首席执行官),负责整个JavaScript程序的高效运行。在这支团队中,编译器扮演着CTO(首席技术官)的角色,专注于代码的解析与优化;而作用域则像是COO(首席运营官),确保每个变量都能在其正确的环境中被正确地访问和修改。

变量声明与作用域

当我们写下var a = 1;这行代码时,实际上触发了一系列复杂的操作。

var a = 1;
// 其实可以分为两个阶段
var a // 编译阶段 变量的声明
a = 1// 执行阶段 变量的赋值

编译器首先会将这段程序分解成”词法单元“,然后将词法单元解析成一个树结构。但是当编 译器开始进行代码生成时,它对这段程序的处理方式会和预期的有所不同。 可以合理地假设编译器所产生的代码能够用下面的伪代码进行概括:“为一个变量分配内 存,将其命名为 a,然后将值 1 保存进这个变量。”然而,这并不完全正确。

事实上编译器会进行如下处理。

  1. 遇到 var a,编译器会询问作用域是否已经有一个该名称的变量存在于同一个作用域的 集合中。如果是,编译器会忽略该声明,继续进行编译;否则它会要求作用域在当前作 用域的集合中声明一个新的变量,并命名为 a

  2. 接下来编译器会为引擎生成运行时所需的代码,这些代码被用来处理 a = 1 这个赋值 操作。引擎运行时会首先询问作用域,在当前的作用域集合中是否存在一个叫作 a 的 变量。如果是,引擎就会使用这个变量;如果否,引擎会继续查找该变量,如果引擎最终找到了 a 变量,就会将 1 赋值给它。否则引擎就会举手示意并抛出一个异常!提示变量未定义(NOT Defined)

接下来让我们来扣一下编译器处理过程的细节:

function foo(){
    var a = 1;
    var a = 2;
    console.log(a);
}
foo();

在上面的代码里面你会发现声明了两个变量a并对其进行了不同赋值。那接下来编译器会如何处理呢,很多同学会认为是下面的var a = 2将覆盖上面的声明,其实不是。

  • var a = 1;引擎交给编译器的var a 代码被执行,并声明了变量a,然后初始值为1.
  • var a = 2;由于变量a已经被声明,所以这里引擎会忽略第二个var a声明,但是赋值操作a = 2被执行。

变量提升与未定义

JavaScript是一门弱类型的编程语言,这意味着变量的数据类型是由它们被赋予的值所决定的。在JavaScript中,所有使用var关键字声明的变量都会经历一种称为“提升”的现象。这意味着无论变量是在哪里声明的,它们的声明都会被移动到其所在作用域的最顶部。但是,需要注意的是,只有变量的声明部分会被提升,而赋值操作不会。因此,尝试在声明之前使用变量会导致引用错误或返回undefined

让我们写一段代码来理解:

function foo(){
    console.log(a);
    var a = 1;
}
foo();

这里的var a定义的变量存在变量提升,但是赋值操作不会提升,所以会返回错误undefined.

function foo(){
    console.log(a);
    const a = 1;
}
foo();

如果将var改为const时这里就没有变量提升,报错ReferenceError,该错误为引用了未声明的变量。

LHS查找与RHS查找

在JavaScript中,变量的查找分为两种模式:LHS(Left Hand Side)和RHS(retrive his source value )。简单来说,LHS查找是为了给变量赋值,和查找变量对应容器,而RHS查找则是为了获取变量的源值。例如,在表达式a = b + 2;中,对a的查找属于LHS查找,因为我们的目的是将右侧的结果赋值给a;而对b的查找则是RHS查找,因为我们需要从b中取出其当前的值来进行加法运算。

让我们来扣一下LHSRHS的细节吧:

function foo(a){
   console.log(b)
   b = a;
}
foo(2);

这里会报出ReferenceError: b is not defined,说明在引用变量b之前没有定义这个变量。

function foo(a){
   b = a;
}
foo(2);
console.log(b)

这里换一个位置就可以正常执行,输出b = 2。为什么?很多同学该问了这里的b在赋值前也没有声明,为什么不会报出ReferenceError,这是因为LHS查找变量b时会默认声明了这个变量。

"use strict"
    // 严格模式时, 不允许LHS查询, 不允许在未声明的变量上赋值
function foo(){
    b = 2;
}
foo();
console.log(b);

此时使用了严格模式,这时候LHS查询就会失败,并报出ReferenceError

那我们以此类推,是不是RHS查找也会默认变量被声明。

function foo(a){
    b = a;
}
foo(2);
console.log(b,a)

报出ReferenceError: a is not defined,显然结果是不可以。

变量声明与赋值过程

var a
a = 1;
a();
  • 变量声明 var a; :声明了一个全局变量 a,初始值为 undefined
  • 赋值操作 a = 1; :通过LHS查找找到 a 的容器,并将值 1 赋值给 a,此时 a 的类型为 number
  • 调用操作 a(); :通过RHS查找找到 a 的值 1,尝试将其作为函数调用,但由于 1 不是函数,因此抛出 TypeError

作用域链

作用域链是JavaScript中用来描述不同作用域之间关系的概念。当在一个嵌套的作用域中查找变量时,JavaScript引擎会按照作用域链逐级向上查找,直至找到目标变量或到达全局作用域。这一机制确保了即使在复杂的程序结构中,也能准确无误地访问到所需的变量。

通过理解JavaScript中变量声明、作用域以及查找机制的工作原理,开发者能够更好地控制程序的行为,避免常见的错误,并编写更加高效、清晰的代码。