目前,let
和 const
关键字已经取代了传统的 var
,带来了更合理的作用域规则和更严格的使用限制。然而,即使是有经验的开发者,也会忽略一些微妙的细节。
var
的问题:为什么不要用它
在深入了解 let
和 const
之前,有必要先理解为什么我们不要用 var
:
函数作用域而非块级作用域
if (true) {
var x = 10;
}
console.log(x); // 10,变量 x 泄露到外部作用域变量提升(Hoisting)带来的困惑
console.log(x); // undefined,而非报错
var x = 5;允许重复声明
var user = "张三";
var user = "李四"; // 不报错,静默覆盖全局声明成为全局对象的属性
var global = "我是全局变量";
console.log(window.global); // "我是全局变量"(浏览器环境)
这些特性导致了许多难以追踪的 bug,尤其在大型应用程序中。
let
的核心特性:被忽略的细节
1. 暂时性死区(Temporal Dead Zone)
这可能是 let
最容易被忽略的特性:
console.log(x); // ReferenceError: x is not defined
let x = 5;
与 var
不同,let
声明的变量存在"暂时性死区"(TDZ)。从块作用域开始到变量声明之前,该变量都是不可访问的。这并非简单的"不提升",而是一种更精细的机制。
let x = 10;
function example() {
// 从这里开始,x 进入 TDZ
console.log(x); // ReferenceError
let x = 20; // 这里 x 离开 TDZ
}
被忽略的细节:即使外部作用域已有同名变量,内部作用域的暂时性死区仍然会阻止访问。
2. 真正的块级作用域
let
声明的变量严格遵循块级作用域规则,这点经常被低估:
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// 输出:0, 1, 2
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// 输出:3, 3, 3
被忽略的细节:每次循环迭代,let
声明都会创建一个新的变量实例,这在处理闭包时尤为重要。
3. 不污染全局对象
虽然知道 let
不会成为全局对象的属性,但有一个细节常被忽略:
var varVariable ="var';
let letVariable =ulet';
console.log(window.varVariable); //"var"
console.log(window.letVariable); //undefined
// 但这并不意味着全局 let 变量"消失"了
console.log(letVariable); //"let",仍然可以直接访问
被忽略的细节:全局 let
变量存储在称为"脚本作用域"的特殊环境中,而非全局对象上。
const
的核心特性:被误解的不变性
1. 对象和数组的可变性
许多开发者误以为 const
声明的对象或数组是完全不可变的:
const user={name:"张三"};
user.name =“李四"; //这是允许的
console.log(user.name); //“李四
const numbers=1,2,3];
numbers.push(4); //这也是允许的
console.log(numbers); //[1,2,3,4]
被忽略的细节:const
只保证引用不变,而非内容不变。要创建不可变对象,需要使用 Object.freeze()
:
const user=0bject.freeze({ name:"张三”});
user.name =“李四"; // 在严格模式下会抛出错误,否则静默失败
console.log(user.name); //仍然是"张三
但要注意,Object.freeze()
只是浅冻结:
const user= 0bject.freeze({
name:"张三,
address :{city:"北京"}
});
user.address.city=上海"; //可以修改嵌套对象
深度冻结需要递归应用 Object.freeze()
。
2. 声明时必须初始化
这似乎是显而易见的,但容易被忽略的是初始化的时机:
const x; //SyntaxError: Missing initializer in const declaration
// 甚至这样也不行
const y;
y=5; // 必须在声明的同时初始化
被忽略的细节:const
声明的变量不能被赋予新值,但这并不意味着它的内容不可变。
3. 性能考虑
一个常被忽略的事实是,在某些 JavaScript 引擎中,const
声明可能会有轻微的性能优势:
// 引擎可能会对这段代码进行更多优化
const PI=3.14159;
const MAX_USERS =10;
引擎可以确保这些值永远不会改变,从而可能进行常量折叠等优化。
实用的使用模式和最佳实践
1. 默认使用 const
,必要时退回到 let
// 推荐的方式
const response=await fetchData();
const basePrice =100;
let finalPrice = basePrice;// 使用 let,因为这个值后续会改变
if(hasDiscount){
finalPrice =applyDiscount(finalPrice);
}
这种方式可以最大限度减少代码中的变量重新赋值,提高可读性和可维护性。
2. 解构赋值中的 let
和 const
// 解构时可以混用
const {name,age}=user;
let { balance,isActive }= account;
// 最有趣的例子是函数参数解构
function process({data,options={}}){
//参数解构实际上是 const 声明
data.processed=true;//可以修改属性
// data = newData;// 这会抛出错误
}
被忽略的细节:函数参数解构本质上是 const
声明,不能重新赋值。
3. 循环中的 let
vs const
// 在 for-of 和 for-in 循环中,const 也是有用的
for(const item of items){
console.log(item);//每次送代创建新的 const 变量
}
//但在传统 for 循环中,必须使用
letfor(leti=0;i<10;i++){
// i 需要变化,所以必须使用 let
}
被忽略的细节:for-of
和 for-in
循环中使用 const
是合法的,因为每次迭代都会创建新的绑定。
深入理解:let
、const
的内部工作机制
理解 JavaScript 引擎如何处理这些声明,有助于避免常见陷阱:
// 简化的内部处理流程
function example() {
// 1. 创建词法环境
// 2. 对 let/const 声明进行"未初始化"标记(TDZ 开始)
// console.log(x); // 如果取消注释,会报错:x 在 TDZ 中
let x = 10; // x 从 TDZ 中解除,并赋值为 10
if (true) {
// 创建新的块级词法环境
const y = 20;
x = 30; // 可以访问外部 x
// y 仅在此块中可用
}
// console.log(y); // 如果取消注释,会报错:y 不在此作用域
}
词法环境(Lexical Environment)和变量环境(Variable Environment)的区别是 JavaScript 引擎如何区分处理不同类型的声明。
发表评论