前言:
我们都知道,变量是用于存储信息的"容器"。在 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也未被定义同样保错,可结果如下图:
为什么会这样呢?原来所谓的变量提升,是指在 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开发的"三骑士法则"
- 优先使用 const → 90%场景适用
- 可变数据 用 let → 9%场景
- 避免使用 var → 1%特殊需求
现代JS开发流程
↓
默认使用const声明
↓
当检测到需要重新赋值时
↓
将const改为let
↓
仅在特殊场景保留var
"ES6要让JS成为企业级开发语言",这种声明规范使代码具有Java般的严谨性,同时保留JS的灵活性。 这也同时标志着JS从"玩具语言"到"工业级语言"的蜕变,使其在Vue、React等现代框架中大放异彩。