面试高频题,也是日常写代码天天踩的坑。这玩意儿说简单也简单,说深了能聊到执行上下文。本文用最直白的方式,把六个核心差异点讲清楚。
var是 ES6 之前长期使用的声明方式,在函数内是函数作用域、会提升、能重复声明。let和const是 ES6 引入的,块级作用域、存在暂时性死区、不可重复声明。const额外要求声明时必须初始化,且不能重新赋值。
这是最根本的区别。
// var — 函数作用域function testVar() { if(true){ var x = 1; } console.log(x);// 1 — if 块挡不住 var}// let/const — 块级作用域function testLet() { if(true) { let y = 1; const z = 2; }
console.log(y);// ReferenceError: y is not defined}
在函数内部,var只有函数体这个大括号才算"边界"。if、for、while后面的花括号它统统不认。这也是经典坑for循环用var出问题的根源:
for(var i=0; i<3; i++) { setTimeout(()=> console.log(i), 0);}// 输出:3 3 3 — 循环结束 i 已经变成 3 了for(let j=0; j<3; j++) { setTimeout(()=> console.log(j), 0);}// 输出:0 1 2 — 每次迭代都有独立的 j 绑定
一句话记:用 var 写 for 循环加 setTimeout,就是在给自己挖坑。
三种声明都会在作用域创建阶段被引擎登记,这通常被叫作"变量提升"。但它们的初始化时机不同,所以访问行为完全不同。
console.log(a);// undefined — 提升并默认初始化为 undefinedvar a = 1;console.log(b);// ReferenceError: Cannot access 'b' before initializationlet b = 2;
var的变量提升你肯定见过——变量还没声明就能访问,值是undefined。准确说,不是引擎真的把代码搬到了顶部,而是在创建作用域时先登记了a,并初始化为undefined;a = 1仍然在原来的位置执行。
let和const也会在作用域创建阶段被登记,但在执行到声明语句前不会完成初始化。从作用域开始到声明行之间的这段区域,就叫暂时性死区。在这个区域里访问变量,直接抛错。
一句话记:声明之前访问变量,var 给你 undefined,let/const 给你报错。
这个面试常考,单独拿出来讲。
{ // 这个块作用域的 TDZ 从这里开始 console.log(typeof foo);// ReferenceError(不是 undefined!) let foo = 1; // TDZ 在这里结束}
坑人的地方在于,用typeof检查一个没声明的变量本来会返回"undefined",但如果这个变量在当前作用域里用let声明了(只是还没执行到那一行),typeof同样会报错。因为这个绑定已经在作用域创建阶段登记了,只是还在死区里。
// 变量根本没声明过console.log(typeof neverDeclared);// "undefined" — 安全// 变量声明了但还没到{ console.log(typeof willBeDeclared);// ReferenceError — TDZ! let willBeDeclared = 1;}
var a = 1;var a = 2;// 没问题,后一次赋值覆盖前一次值
下面两个错误示例要分别运行:
let b = 1;let b = 2;// SyntaxError: Identifier 'b' has already been declared
const c = 1;const c = 2;// SyntaxError: Identifier 'c' has already been declared
var允许在同一个作用域里重复声明同一个变量名,如果后一次声明带初始化赋值,就会覆盖前面的值。这在大型文件里容易造成变量污染——你可能不小心覆盖了别人前面定义的变量,还找不到 bug。
let和const在同一作用域里重复声明会直接报错,逼你在声明时就想清楚。
注意:在同一个作用域里,var和let混着声明同名变量也不行:
var x = 1;let x = 2;// SyntaxError
但如果是嵌套块作用域,let可以遮蔽外层的var:
var x = 1;{ let x = 2;// OK,块作用域里的新变量}
const和let在作用域、TDZ、重复声明规则上基本一样,但多了两条限制:
第一,必须声明时初始化。
let a;// OK,默认 undefinedconst b;// SyntaxError: Missing initializer in const declaration
第二,不能重新赋值。
const PI = 3.14;PI = 3;// TypeError: Assignment to constant variable
但这里有个高频误区:const 约束的是变量绑定:这个名字不能再绑定到另一个值;它不负责冻结对象本身。
// 基本类型 — 不能重新赋值const num = 1;num = 2;// TypeError// 引用类型 — 默认情况下,内部内容可以改const arr = [1,2,3];arr.push(4);// 完全没问题arr[0] = 999;// 完全没问题arr = [5,6,7];// TypeError — 你在重新绑定到另一个数组,不行const obj = { name:'Alice' };obj.name = 'Bob';// OKobj.age = 18;// OKobj = { name: 'Cat' };// TypeError — 不能重新绑定到另一个对象
如果你想禁止对象第一层属性被改,可以用Object.freeze():
const obj = Object.freeze({ name: 'Alice' });obj.name = 'Bob';// 静默失败(严格模式下报错)
但要注意,Object.freeze()默认只冻结第一层:
const user = Object.freeze({ profile: { name: 'Alice' }});user.profile.name = 'Bob';// OK,嵌套对象没被冻结
如果要做深层不可变,需要递归冻结,或者使用专门的不可变数据方案。
一句话记:const 锁的是绑定,不是值。普通数组和对象的内部内容默认可以改;被冻结、属性不可写等情况除外。
在浏览器普通 script 的全局作用域下:
var globalVar = 1;let globalLet = 2;console.log(window.globalVar);// 1 — var 声明的变量挂到了 window 上console.log(window.globalLet);// undefined — let 声明的变量不会
一句话记:在浏览器普通 script 里,var 会把全局变量挂到 window 上,let/const 不会。
注意,这条只针对浏览器普通 script。ES Module、Node.js 顶层作用域都不完全一样。
搞清楚区别之后,日常怎么写?
默认用
const。大部分变量你并不会重新赋值,用 const表明了"这个变量不会被重新赋值"的意图,代码可读性更高。需要重新赋值时用
let。比如循环计数器、累加器、需要 swap 的临时变量。 现代业务代码默认别用
var。除非你在维护老代码、需要保持 ES5 兼容,或者必须跟旧项目风格一致。
// 日常推荐姿势const name = 'Alice';// 不会重新赋值const items = [1,2,3];// 不会重新绑定到别的数组let count = 0;// 需要改的值for(let i = 0; i<10; i++){ // 循环变量 console.log(i);}
把这篇文章收藏起来,面试前扫一眼这张图就够了。