从"变量失控"到"精密管控":JS变量声明演进史

137 阅读5分钟

前言:

我们都知道,变量是用于存储信息的"容器"。在 JavaScript 中,变量用于存储数据,并可以在程序执行过程中动态更改。

变量可以存储各种类型的数据,如数字、字符串、对象、函数等。

变量名是标识符,用于引用存储在变量中的数据。

在 JavaScript 中,可以使用 var、let 和 const 关键字来声明变量。

一、JS早期的"野马时代"

1.1 失控的变量提升

在ES5的洪荒年代, var 就像一匹未被驯化的野马。它允许开发者这样操作:

console.log(legacyVar); // undefined (变量提升的幽灵)
var legacyVar = 10;
function test() {
    console.log(innerVar); // undefined (函数作用域渗透)
    var innerVar = 20;
}

这种反直觉的"声明提升"机制,导致代码实际执行顺序与书写顺序严重背离,如同在代码中埋设时序地雷。

1.2 作用域的无政府状态

var 的作用域规则如同没有围墙的牧场:

for (var i = 0; i < 3; i++) {
    setTimeout(() => console.log(i)); // 3,3,3
}
console.log(i); // 3 (循环变量逃逸)

这与"块级作用域是大型语言的需要"形成强烈对比。在复杂业务场景中,这种作用域泄漏如同病毒传播,导致变量冲突率随代码量指数级增长。

1.3 内存管理的原始模式

ES5时代的内存管理如同原始部落:

var arr = new Array(1000000); // 堆内存粗放分配
function leakMemory() {
    var cache = {}; // 无块级作用域导致意外驻留
}

对比"内存栈是JS执行上下文分配内存的主战场"的现代理念, var 缺乏精确的内存生命周期控制,容易导致内存泄漏如同沙漏般难以察觉。

1.4 变量提升

var还有一个特性那就是变量提升,让我们先看下面一段代码:

showName() 
console.log(myname) 
var myname = 'kenny' 
function showName() { 
    console.log('函数showName被执行'); 
}

我们都知道代码是从上往下执行的,按理来说第一行showName函数未被定义应该会报错,第二行myname也未被定义同样保错,可结果如下图:

image.png

为什么会这样呢?原来所谓的变量提升,是指在 JavaScript 代码执行过程中,JavaScript 引擎把变量的声明部分和函数的声明部分提升到代码开头的行为。变量被提升后,会给变量设置默认值,这个默认值就是我们熟悉的 undefined。 所以变量提升会将代码变成这样:

var myname  //先把var声明放在代码段最上方,并且赋值为undefined
//其次是函数声明
function showName() { 
    console.log('函数showName被执行'); 
}
showName() 
console.log(myname) 
myname = 'kenny'  //最后赋值

为什么最后赋值呢?是因为myname的赋值操作是在console.log之后执行的,所以被放在了最后方。

通过这两步,我们就可以模拟实现变量提升的效果

二、ES6的"驯马术革命"

2.1 块级作用域——建造围栏

let和const是ES6的新特性,弥补了var的不足,支持块级作用域和常量声明。而且块级作用域是大型语言的需要,比如在for循环中使用。同时,内存分配方面,内存栈是执行上下文分配的主战场,而let和const不会污染全局变量,不像var会挂在window上。

{
    let blockVar = "局部变量";
    var oldVar = "全局污染源";
}
console.log(oldVar);     // "全局污染源"
console.log(blockVar);  // ReferenceError
  • 花括号 {} 形成作用域结界🔒
  • 避免"变量逃逸"问题

而且let在同一作用域内不允许重复声明,这与var允许重复声明不同,这样可以避免潜在的错误。可以强制开发者保持命名一致性,避免多人协作时的命名冲突

let armor = "盾牌";
let armor = "宝剑"; // SyntaxError:防御重复攻击

var weapon = "弓箭";
var weapon = "长矛"; // 允许:旧式无防护

2.2 const——终极封印术

const 的不可变性就像给变量施加了魔法封印:

const PI = 3.14;
PI = 3.1415; // TypeError: 试图破除封印!

const user = {name: "掘金"};
user.name = "开发者"; // 允许:对象内部可变
user = {}; // TypeError: 封印结界不可破

我们发现,为什么user对象的name属性可以改变呢?不是说const是常量吗?

原来对于基本数据类型(如数字、字符串、布尔值),const确保变量的值不会改变。

而对于复杂数据类型(对象,数组),我们可以改变对象的属性,但不能改变整个对象

这是因为基本数据类型的值存储在栈内存中,比如数字、字符串、布尔值等。当用const声明一个基本类型变量时,变量名直接指向这个值的内存地址。由于栈内存中的地址是不可变的,所以重新赋值会导致错误,因为无法改变指向的位置。

复杂数据类型,比如对象或数组,它们的值存储在堆内存中。变量名在栈内存中存储的是指向堆内存地址的引用,也就是指针。const在这里的作用是固定这个引用地址,不允许指向另一个堆地址。但堆内存中的内容本身是可变的,所以可以修改对象的属性或数组的元素,只要不改变引用地址。

内存结构示意图
───────────────────────────────────────────────
 基本类型(栈内存)          引用类型(栈内存+堆内存)
┌───────────────┐        ┌───────────────┐
│  Address      │        │  Address      │
├───────────────┤        ├───────────────┤
│ 0x001: 3.14   │ const  │ 0x002: <指针>  │ const
└───────────────┘        └───────┬───────┘
                                  │
                                  ▼
                                ┌───────────────┐
                                │ 堆内存对象     │
                                ├───────────────┤
                                │ name: "掘金"  │
                                └───────────────┘

三、现代JS开发的"三骑士法则"

  1. 优先使用 const → 90%场景适用
  2. 可变数据 用 let → 9%场景
  3. 避免使用 var → 1%特殊需求
现代JS开发流程
↓
默认使用const声明
↓
当检测到需要重新赋值时
↓
将const改为let
↓
仅在特殊场景保留var

"ES6要让JS成为企业级开发语言",这种声明规范使代码具有Java般的严谨性,同时保留JS的灵活性。 这也同时标志着JS从"玩具语言"到"工业级语言"的蜕变,使其在Vue、React等现代框架中大放异彩。