前言


1995年,布兰登·艾克用一周时间创造了JavaScript。这个带着浓厚“KPI项目”色彩的语言,最初只被设计来给网页添加幻灯片、表单验证等简单交互,甚至连名字都是为了蹭当时Java的热度。然而,谁也没料到,这个“浏览器的副产品”会在二十年后,成为驱动企业级大型应用、服务端乃至桌面软件的核心力量。随着应用复杂度爆炸式增长,JS早期设计中的“赶工”瑕疵逐渐暴露,其中最令人困惑的,便是变量。2015年,ES6(ECMAScript 2015)横空出世,带来了let和const,从根本上革新了变量声明、作用域和生命周期的规则,为JavaScript走向工程化扫清了关键障碍。

一、var的遗产:混乱的三大根源


在ES6之前,声明变量的唯一方式是var。它承载着那个时代的设计局限,带来了三个反直觉的核心问题。

1. 缺乏块级作用域,只有函数级作用域


var声明的变量,无法被花括号{}约束。它的作用域仅限于所在的函数内部,或直接泄露到全局。经典的for循环与setTimeout面试题完美揭示了这一缺陷:

for (var i = 0; i < 10; i++) {
  setTimeout(function() {
    console.log(i);
  }, 100);
}
// 实际输出:10 次 10,而非预期的 0 到 9

原因在于:var声明的i属于全局或函数作用域,不存在于循环体的块中。循环同步执行完毕时,唯一的i已变为10。100毫秒后,所有回调访问的都是同一个已为10的i,因为没有独立的块级作用域为每次迭代“锁定”当前值。

2. 诡异的变量提升


代码执行前,引擎会将所有var声明提升到作用域顶部,并初始化为undefined。

console.log(pizza); // 输出 undefined,不报错
var pizza = 'Margherita';


这种“先使用、后声明”却不报错的行为,与直觉严重不符,极易埋下隐蔽的bug。

3. 允许重复声明


同一作用域内,var可多次声明同名变量,后者静默覆盖前者。在大型项目中,这极易造成变量意外污染和逻辑混乱。

面对这些缺陷,社区只能用命名规范打补丁——比如约定全大写var PI = 3.1415926来“假装”声明常量。但纸包不住火,缺乏语言层面的强制约束始终是隐患。

二、ES6的新秩序:let与const的五大革新


ES6引入了两位纪律严明的法官,为变量世界带来了久违的秩序。

1. 真正的块级作用域


let和const将变量牢牢绑定在所在的{}代码块内。

for (let i = 0; i < 10; i++) {
  setTimeout(function() {
    console.log(i); // 完美输出 0, 1, 2, ..., 9
  }, 100);
}


每一次迭代,let都会为循环体创建一个全新的绑定,并为该次迭代分配当时的i值。10个回调各自捕获了独立的变量,互不干扰。

2. 暂时性死区,杜绝变量提升


let和const不存在变量提升。从块级作用域顶部到声明语句之前,变量处于不可访问的“死区”。

console.log(pizza); // ReferenceError: Cannot access 'pizza' before initialization
let pizza = 'Pepperoni';


报错而非静默输出undefined,让代码执行顺序与阅读顺序严格一致,将错误暴露在开发阶段。

3. 禁止重复声明


同一作用域内,用let或const重复声明同名变量,会直接抛出语法错误,从源头杜绝意外覆盖。

4. const:不可变绑定的承诺


const拥有let的全部特性,但增加了一条核心限制:声明时必须立即初始化,且绑定不可更改。对于简单数据类型,值本身不可变;对于复杂数据类型,绑定不可变,但内部状态可以改,因为const锁定的是内存地址引用。

const PI = 3.14;
PI = 3; // TypeError: Assignment to constant variable

const developer = { name: '张三' };
developer.name = '李四'; // 合法,修改对象属性
developer = { name: '王五' }; // 报错,试图改变绑定


5. 变量类型由值决定,语义更清晰

// 常量一开始就要赋值
const item = 1; // 常量必须在声明时赋值
let a; // undefined
// 简单数据类型
const key = 'abc123';
key = 'ABC123'; // TypeError: Assignment to constant variable. 常量不能被重新赋值
let points = 50;
// let 不止是值可以改变,它的类型也可以改变
// 不要这么干
points = 51; //不好的
let winner = false;
winner = '戴';
// 复杂数据类型 对象
// 值可以改变,但类型不能改变
const person = {
    name: 'zhangsan',
    age: 18
} 
person.age++
console.log(person); // 19
person = '111'; // TypeError: Assignment to constant variable. 常量不能被重新赋值


JavaScript是弱类型语言,变量类型在赋值时确定。let明确表达“此变量可被重新赋值”,const则表达“此绑定不再更改”——代码意图一目了然。

三、作用域嵌套、变量查找与生命周期


作用域是一套查找变量的规则,遵循“冒泡”机制:引擎先在当前作用域查找,找不到就向外层作用域冒泡,直到全局作用域。若全局仍未找到,则抛出ReferenceError: xxx is not defined。

const globalVar = '全局';
function outer() {
  const outerVar = '外层';
  if (true) {
    const innerVar = '内层';
    console.log(innerVar); // 当前块级作用域找到
    console.log(outerVar); // 向外冒泡到函数作用域找到
    console.log(globalVar); // 继续冒泡到全局找到
    console.log(notExist); // 所有作用域都找不到——报错
  }
}


变量的生命周期始于声明,终于其所在作用域执行结束。当一个函数或{}代码块执行完毕后,其内部变量通常会被垃圾回收机制从内存中销毁,释放空间。理解这一点对避免内存泄漏至关重要:若全局作用域持有了本该销毁的闭包引用,闭包所引用的外部变量就会迟迟无法释放。这也是为什么在大型应用中,合理控制变量的作用域范围、及时解除不必要引用,是性能优化的关键一环。

四、作用域体系详解


作用域是变量生效的代码范围,JS作用域分为三类,ES6的核心贡献就是完善了块级作用域体系,形成完整的作用域层级。

4.1 全局作用域


脚本全局区域定义的变量拥有全局作用域,可在任意代码位置访问,生命周期贯穿整个页面运行周期,页面关闭后才会销毁。

4.2 函数局部作用域


在函数内部定义的变量仅在函数内部生效,函数执行完毕后,局部变量会被垃圾回收,内存自动释放,有效隔离函数内部变量与全局环境。

4.3 块级作用域


由{}包裹的代码块形成独立块级作用域,if、for、while等代码块均适用。这是ES6新增的核心作用域规则,let、const声明的变量严格受块级作用域限制,彻底解决了var语法的作用域混乱问题。

var height = 200; //全局作用域变量
// 局部作用域 global scope 全局作用域
function setWidt()
{
    // 局部作用域变量
    var width = 100;
    console.log(width,height); //可以访问width和height
}

setWidt();
// console.log(width); //会报错,因为width是局部变量,不能在函数外访问
console.log(height); //可以访问height,因为它是全局变量
var age = 100;
if(age > 12){
    // 块级作用域
    // es6 常量 不可以改变
    //const dog = age * 7;
    let x = 111; //es6 块级作用域变量
    var dog = age * 7; //es5 全局作用域变量
    console.log(dog);
    dog ++;
}
console.log(dog); 
// console.log(x); //会报错,因为x是块级作用域变量,不能在块外访问

五、经典案例:for+setTimeout作用域差异

  1. for循环搭配定时器是验证var与let作用域差异的经典案例,能直观体现ES6语法的优越性。使用var声明循环变量i时,因无块级作用域,整个循环仅存在一个全局i变量。循环同步执行完毕后i值固定为10,后续异步执行的setTimeout定时器,最终打印结果均为10。
  2. 而使用let声明i时,每一次循环都会生成独立的块级作用域,每个循环的i都是独立的局部变量,互不干扰。定时器执行时,会对应读取每一轮循环的独立i值,依次打印出对应数值,完美契合开发者的编码预期。
// 全局作用域
{
// 代码块
// 块级作用域
//申明了变量,属于当前块级作用域
const name = 'zhangsan';
console.log(name);
}
// console.log(name); //会报错,因为name是块级作用域变量,不能在块外访问
//退出循环,才是10
for(let i=0;i<10;i++)// for循环的块级作用域 10块
    {
    //用var i  同步代码 尽快执行完
    console.log(i);
    //异步代码  1秒后执行 i已经是10了
    setTimeout(function(){
        console.log('This number is ' + i);
    },1000);
    } 
- var 不支持块级作用域 只有一个 i
同步 i 10,setTimeout 打印10
- let 支持块级作用域 嵌套着n个局部作用域

六、变量提升机制与ES6优化

变量提升是JS早期的语法缺陷,代码执行分为编译和执行两个阶段。编译阶段会提前扫描所有var声明的变量,将其提升至作用域顶部,赋值为undefined,导致变量在声明前可访问,打乱代码执行顺序,不符合编码直觉。

// 执行顺序
// 编译阶段 检测代码语法
// 准备好执行上下文 (变量环境)
// 执行阶段
console.log(pizza); // ReferenceError: pizza is not defined
let pizza = 'Deep Dish';

ES6彻底优化了这一问题,let和const声明的变量不存在变量提升,存在暂时性死区。在变量声明前访问变量,会直接抛出Cannot access before initialization的报错,强制规范代码书写顺序,让代码执行逻辑与书写逻辑完全一致,大幅降低bug出现概率。

总结


var的混乱源于历史妥协:缺乏块级作用域、诡异的变量提升、允许重复声明。let与const以五大革新——块级作用域、暂时性死区、禁止重复声明、不可变绑定、语义清晰——彻底重塑了JavaScript的变量体系,使其从“能用就行”走向严谨与可预测。

作用域查找遵循冒泡规则,变量生命周期随作用域结束而终结,而三个经典报错共同构成了ES6的变量保护机制:Cannot access before initialization在死区拦截提前访问,is not defined在作用域链尽头宣告变量不存在,Assignment to constant variable在运行时守卫常量的不可变绑定。

现代开发的三条黄金法则:

默认使用const——凡是不需要重新赋值的变量,一律用const,最大化代码的不可变性。

需要重新赋值时用let——如循环计数器、状态标志位等场景。

彻底摒弃var——它已是历史符号,ESLint的no-var规则应成为所有新项目的标配。

从一周赶工的“急就章”,到如今承载全栈生态的基石,JavaScript的进化史正是软件工程追求秩序与可靠性的缩影。理解并善用let和const构建的块级作用域新秩序,是每一位开发者从入门到精通的必修课,也是在大型项目中自信驾驭复杂性的基石。

更多推荐