由作用域延伸到 var, let 和 const
什么是 作用域
作用域(Scope)就像一个容器,它决定了变量,函数和对象在代码中哪些地方可以被访问。简单来说,就是变量的"有效范围"和"生存空间"。
JavaScript 中有两种基本的作用域类型:
全局 作用域:任何地方都可以访问。
局部 作用域:只能在特定区域内访问。又可以细分为函数作用域和块级作用域。
深入理解函数 作用域 和块级作用域
函数 作用域:在ES6以前,JavaScript 只有全局作用域和函数作用域。这意外着用 var 声明的变量,它的作用域被限定在它所在的函数内部。
示例如下:
javascript
function greet() {
var message = "Hello, world!";
console.log(message); // 可以在函数内部访问
}
greet(); // 输出: "Hello, world!"
console.log(message); // 报错: ReferenceError: message is not defined
为什么会报错?
因为 message 变量被声明在函数内部,作用域仅限于函数。一旦函数被执行完毕,变量就会被销毁,在函数外部就无法访问了。这就是函数作用域。
块级 作用域 :
块指的是任何有 {} 包裹的代码。比如if语句,for循环,while循环。
在ES6引入 let 和 const 之后,JavaScript 增加了块级作用域。这意味着用 let 或 const 声明的变量,它的作用域被限制在 {} 内部。
示例如下:
javascript
if (true) {
let greeting = "Hello again!";
console.log(greeting); // 可以在块内部访问
}
console.log(greeting); // 报错: ReferenceError: greeting is not defined
为什么报错?
因为 greeting
变量是用 let
声明的,它只存在于 if
语句的 {}
块中。一旦跳出这个块,它就无法被访问了。这就是块级 作用域。
Var, let 和 const 的演变
理解了作用域,就很容易理解 var, let 和 const 的区别了。
首先上个示例:
ini
function myFunction() {
var a = 1;
let b = 2;
if (true) {
var a = 3; // 这里的 a 覆盖了上面的 var a
let b = 4; // 这是一个新的变量,只在 if 块内有效
console.log(a); // 3
console.log(b); // 4
}
console.log(a); // 3 (var 变量在 if 块内被修改了)
console.log(b); // 2 (let 变量只在 if 块内有效,外部访问的是原来的 b)
}
myFunction();
这个例子就很好的说明了 var 是函数作用域,而 let 和 const 是块级作用域。
因为 var a 在if块里被永久地改变了,而 let b 在if块里重新声明了一个变量,并不影响if块以外的变量。
而 const 与 let 的行为几乎一模一样,唯一不同的是 const 声明的变量必须初始化,并且不能被重新赋值。那是怎么不能被重新赋值的呢?
Const 声明的基本类型变量的值是不能改变的,但如果声明的是一个引用类型(如对象和数组),那么引用本身是不能改变的,但引用指向的对象内部的属性是可以修改的。
ini
const person = {
name: "David",
age: 25
};
person.age = 26; // 这没问题,修改了对象的属性
console.log(person.age); // 输出: 26
// person = { name: "John" }; // 报错!你不能把 person 重新赋值给一个新的对象
那么,Var, let 和 const 三者对比如下:
特性 | var | let | const |
---|---|---|---|
作用域 | 函数作用域 | 块级作用域 | 块级作用域 |
重复声明 | 允许 | 不允许 | 不允许 |
是否可以修改 | 可以 | 可以 | 不可以(引用类型除外) |
变量提升 | 提升并初始化为undefined | 提升,但有"暂时性死区" | 提升,但有"暂时性死区" |
从 V8 引擎的角度深入理解 var, let 和 const
让我们从 V8 引擎的角度,也就是 JavaScript 引擎是如何处理 var
、let
和 const
的,来深入理解它们之间的区别。这就像是揭开表面现象,看看底层代码到底发生了什么。
编译阶段 vs 执行阶段
要理解 V8,我们首先要明白一个关键概念:JavaScript 代码在运行前会经过一个编译阶段。
- 编译阶段:V8 引擎会解析你的代码,在这个阶段,它会识别所有的变量声明( var, let 和 const ),并为它们分配内存空间。
- 执行阶段:引擎会一行一行地执行代码,进行变量赋值和函数调用等操作。
而 var, let 和 const 的主要区别,就体现在编译阶段的内存分配和处理方式上。
V8 眼中的 var:函数作用域与 "旧时代"的遗留
当 V8 遇到 var 时,它会在编译阶段做两件事:
- 为变量声明分配 内存:V8会在当前函数作用域或全局作用域的内存空间中,为 var 变量创建一个槽位
- 立即初始化为 undefined:这个槽位会被立即赋予 undefined 值。
这就是我们常常说的变量提升(hosting)。在执行阶段,无论 var 声明出现在代码的哪个位置,所对应的内存空间都已经准备好了,知识里面的值是 undefined。
arduino
// 编译阶段:
// V8 在当前作用域创建一个名为 myVar 的变量,并赋值为 undefined。
// var myVar = undefined;
// 执行阶段:
console.log(myVar); // 此时 myVar 已经存在,值为 undefined
myVar = 10; // 执行赋值操作
V8 眼中的 let 和 const:块级作用域与"暂时性死区"
V8处理 let 和 const 的方式完全不同,这正是 ES6 的核心改进。当 V8 遇到 let 或 const 时:
- 为变量分配内存:V8 同样会在编译阶段为
let
或const
变量分配内存空间。 - 不进行初始化:关键的区别在于 V8 不会像 var 那样将那些变量初始化为 undefined。相反,会将这些变量放置在一个"暂时性死区(Temporal Dead Zone, TDZ)"。
暂时性死区就像一个"隔离区",代码执行到 let 或 const 声明的那以后之前,任何对变量的访问都会触发一个 ReferenceError。只有执行到声明语句时,变量才会被初始化,并从暂时性死区中解封。
因此暂时性死区的存在是为了强制开发者在声明变量后再使用它,避免了 var 的意外行为。
arduino
// 编译阶段:
// V8 在当前块级作用域创建一个名为 myLet 的变量,
// 但不给它赋值,并把它放入 TDZ。
// myLet <处于未初始化状态>
// 执行阶段:
console.log(myLet); // 引擎检查 myLet,发现它在 TDZ 中,立即抛出 ReferenceError
myLet = 20; // 永远无法到达这里
从 V8 的角度看,let
和 const
并不是简单地"没有变量提升",而是它们的变量提升和初始化过程被设计得更加严格和安全。这种底层处理方式的差异,是它们在行为上完全不同的根本原因。
谈谈最近学习的感想
最近 kitty 同学马上秋招了,秋招面试对于底层的理解要求是至关重要的,同时 Kitty 同学在秋招准备的路上不由得感慨,对于理论也好,实践也罢,注重对于细节的把控是必不可少的。对于同样学习的内容,该怎么把这一块学习的价值发挥到最大呢,那就和 Kitty 同学一起学习吧~