从作用域到TDZ:var、let、const机制与选型

发布时间:2026/10/31 13:46:49
从作用域到TDZ:var、let、const机制与选型 翻出这段代码的时候我愣了一下同一个函数里var声明的循环变量被三个延时回调共享最后打印出来是三个一模一样的数字。这段代码是三年前写的当时还觉得逻辑挺顺。后来把它改成let输出立刻变成 0、1、2一行没动逻辑只是换了个关键字。这件事让我意识到var、let、const这三个词看起来是入门第一课的内容但真正能吃透它们背后机制的人并不多——大多数人记住的是“能用 const 就用 const”这种口诀一旦遇到闭包捕获、暂时性死区报错、块级作用域嵌套还是会卡住。这篇内容就是把这三个关键字摊开讲透。核心关键词是作用域、变量提升、暂时性死区、块级作用域和不可变绑定。我会从执行上下文和词法环境的底层机制讲起解释每一次声明到底在引擎里发生了什么再用大量可以复制粘贴的代码验证结论最后给出实际项目里的选型规则和排查套路。适合已经写过 JavaScript、但被这三个关键字坑过的开发者也适合刚入门、想一次性把地基打牢的朋友。看完你应该能做到看到一段代码不用运行就能判断出每个变量在哪个作用域、什么时候可访问、会不会报错。1. 先把三个关键字放在同一张桌子上对比1.1 一个真实踩坑场景引出的问题先说个具体的场景。某后台管理项目里有个表格渲染函数需要给每一行绑定一个点击回调回调里要用到当前行的索引。写法大概是这样function bindRows(rows) { var handlers []; for (var i 0; i rows.length; i) { handlers.push(function () { console.log(当前行索引:, i); }); } return handlers; } var list bindRows([a, b, c]); list.forEach(function (fn) { fn(); });跑出来的结果是三个当前行索引: 3。当时第一反应是“引擎有 bug 吧”第二反应才是去查闭包和作用域然后把var i改成let i问题消失。这个例子之所以经典是因为它同时踩中了三个知识点var只有函数作用域没有块级作用域、var的声明会被提升到函数顶部、闭包捕获的是变量本身而不是变量在某一时刻的快照。三个点单独拿出来都懂凑在一起就废了。所以我不打算按“语法—示例—注意事项”这种教材顺序讲而是先给出三个关键字在四个维度上的完整对照让你心里有个坐标系后面每一节都是往这个坐标系里填细节。对比维度varletconst作用域类型函数作用域或全局块级作用域块级作用域变量提升提升初始化为 undefined提升但不初始化进入 TDZ提升但不初始化进入 TDZ暂时性死区无有有重复声明允许同作用域禁止同作用域禁止声明时必须初始化否否是for-in/for-of 头部除外可否重新赋值可以可以不可以绑定不可变顶层声明是否挂到全局对象是脚本顶层否否循环中是否为每次迭代创建新绑定否是是这张表建议存下来。它不是我编的总结每一条后面都能对应到规范里的具体行为下面几节我会逐条拆开验证。1.2 三个关键字其实在回答两个问题很多人把这三个关键字当成“新旧语法”的替代关系其实它们是在回答两个彼此独立的问题。第一个问题这个绑定活在哪一层作用域里。var的答案是“函数体内部或者全局”它压根不认识{}let和const的答案是“离它最近的那对花括号内部”包括if、for、switch、try、单独的裸块甚至case分支如果你手动加了大括号。第二个问题这个绑定能不能被重新指向别的值。var和let说能const说不能。注意这里的关键词是“重新指向”不是“值不能改”这个区别是理解const的核心第 5 节会专门讲这里先埋个伏笔const声明的对象属性照样可以改因为被锁住的是变量到对象之间那根线不是对象本身。把这两个问题分开看你会发现let其实就是“块级作用域版的 var”const就是“块级作用域 不可重新赋值版的 var”。它们不是同一个东西的三个版本而是两个独立维度组合出来的三种配置。至于为什么会出现这种局面稍微提一句历史背景就够了早期 JavaScript 设计仓促只有函数作用域闭包和循环配合起来到处是坑后来语言层面补上了块级作用域但为了不破坏已经运行在无数网页上的老代码var的行为被完整保留下来只能新增关键字。这也解释了为什么用var写的代码能跑十几年不出问题——不是它好是它被冻结在旧语义里了。2. 作用域差异块级作用域到底改变了什么2.1 函数作用域与块级作用域的实测对比先看一段最朴素的验证代码把var和let放在同一个函数里的不同块中function scopeTest() { if (true) { var a var 声明; let b let 声明; } console.log(a); // var 声明 console.log(b); // ReferenceError: b is not defined } scopeTest();结果很干脆a在if块外面照样能访问b直接报错。原因不复杂——var声明被提升到了scopeTest函数体的顶部if的花括号对它没有任何约束力let声明被绑定在那对花括号形成的块级作用域里出了块就查不到这个名字。这里有个细节容易被忽略console.log(b)报的是ReferenceError: b is not defined而不是undefined。这说明b这个名字在块外的作用域链上根本不存在引擎连“提升后的空位”都懒得给你留。这也是块级作用域的一个隐性好处——把变量关进它该待的盒子里外部世界看不到它命名冲突的概率大幅下降。再看一个更隐蔽的场景for循环里的变量for (var i 0; i 3; i) {} console.log(i); // 3i 泄漏到了外层 for (let j 0; j 3; j) {} console.log(j); // ReferenceError: j is not definedvar版本里的i在循环结束后依然存活值停在 3。这个特性在某些老代码里被当成技巧用循环结束后还能拿到计数但它同时也是全局污染的源头之一。let版本则干净利落循环结束变量随块一起销毁。我在实际项目里踩过的一个坑是同一个函数里有两段for (var i ...)循环中间隔着几十行业务逻辑第二段循环忘了重置i因为第一段的i已经提升到函数顶部并且带着结束值导致第二段一次都没执行。这种 bug 在阅读时几乎不可见因为两段代码隔得太远视觉上完全没有关联。换成let之后每一段循环各自拥有独立的绑定物理上就不可能有这种串扰。2.2 for 循环与闭包let 的逐迭代绑定机制回到开头那个打印三次 3 的例子。为什么换成let就正常了答案不是“let 有块级作用域”这么简单而是规范里一个很关键的机制for语句中用let或const声明的变量会为每一次循环迭代创建一个新的绑定。用代码验证一下// var所有闭包共享同一个 i var fns1 []; for (var i 0; i 3; i) { fns1.push(() i); } console.log(fns1.map(f f())); // [3, 3, 3] // let每次迭代产生新的绑定闭包各捕获一份 var fns2 []; for (let j 0; j 3; j) { fns2.push(() j); } console.log(fns2.map(f f())); // [0, 1, 2]机制上说let版本的循环大致等价于在每一轮迭代内部执行了一次let j 上一轮的值然后跑循环体。所以每个箭头函数捕获的是那一轮迭代里独一无二的j互不干扰。这里有个更细的点值得知道for循环的头部其实有两层作用域。一层是for语句本身创建的循环作用域用来存放初始化表达式里的声明另一层是每轮迭代创建的作用域循环体在这个作用域里执行。规范对迭代次数的处理是复制上一轮的值到新的绑定然后再执行i和条件判断。这就是为什么闭包捕获到的是 0、1、2 而不是 1、2、3——递增发生在迭代结束、下一轮绑定创建之前。理解了这个机制你就能预判一些更复杂的写法for (let k 0; k 3; k) { setTimeout(() console.log(k), 0); } // 输出 0 1 2而用var的话就得靠立即执行函数把值封起来这是 ES5 时代的标准套路for (var k 0; k 3; k) { (function (captured) { setTimeout(() console.log(captured), 0); })(k); }我不是说 IIFE 不好但两段代码放一起对比let版本的意图明显更清晰少了一层函数嵌套也少了一次作用域跳转。实际项目中我见过不少代码为了“兼容性”保留 IIFE其实现在构建链路早就把let转译好了没必要背这个历史包袱。2.3 那些容易被忽略的块级作用域位置说起块级作用域大多数人只想到for和if实际上能形成块的位置比想象中多。switch的case分支就是重灾区switch (type) { case a: let msg 类型 A; console.log(msg); break; case b: let msg2 类型 B; // 如果这里也用 let msg会直接抛重复声明错误 console.log(msg2); break; }switch的整个case列表共享同一个块级作用域因为花括号只有一对所以在case a里声明let msg在case b里再声明let msg就会报Identifier msg has already been declared。解决办法是给每个case手动加一对大括号形成独立的块switch (type) { case a: { let msg 类型 A; console.log(msg); break; } case b: { let msg 类型 B; console.log(msg); break; } }这个写法一开始看着别扭但用习惯之后你会发现它反而更安全——每个分支的临时变量互不可见也不会因为漏写break而读到上一个分支的残留值。另一个常被忽视的位置是块内的函数声明。在非严格模式的老代码里块内写function的行为是引擎自己决定的“扩展行为”有的实现会把它提升到外层有的只在块内有效非常混乱。严格模式下规范做了明确规定块内函数声明属于该块出了块就不可见。所以我的建议很直接——不要在块内声明函数需要复用就提到外面需要局部化就用const fn () {}既不依赖引擎实现语义也更清楚。3. 变量提升与暂时性死区最容易报错的一层3.1 var 的提升声明上去了赋值留在原地先说var的提升因为它的行为最反直觉也最常被误解。核心只有一句话声明被提到作用域顶部赋值留在原来的位置。console.log(a); // undefined不报错 var a 1; console.log(a); // 1上面这段代码在引擎眼里等价于var a; // 声明提升到这里 console.log(a); // undefined a 1; // 赋值还在原地 console.log(a); // 1所以那句“var声明的变量会提升”其实不准确完整的说法是“声明提升初始化成 undefined 这一步也一起提前但具体的赋值表达式不动”。这就解释了为什么打印出来是undefined而不是报错也解释了为什么下面的代码会输出undefined而不是10var x 10; function demo() { console.log(x); // undefined不是 10 var x 20; } demo();函数内部的var x被提升到了函数顶部遮蔽了外层的x所以在赋值之前访问到的是本地那个还是undefined的x。这类 bug 在老代码里非常难查因为你在函数第一行看到的是“外层变量”但引擎看到的是“本地声明”。这也是我后来彻底不用var的主要原因之一——它让你读代码时建立的直觉和引擎行为经常对不上。3.2 暂时性死区let 和 const 的“提升但不初始化”let和const也有提升但它们提升了之后不做初始化变量处于一个“已经存在但还不能碰”的状态。这段从作用域开始到声明语句执行之间的区域就叫暂时性死区Temporal Dead Zone简称 TDZ。{ console.log(name); // ReferenceError: Cannot access name before initialization let name 测试; }注意报错信息不是name is not defined而是Cannot access name before initialization。这两句话的区别很关键前者说明名字不存在后者说明名字存在只是还没初始化。这从侧面证实了let确实是提升的只是提升后不给你初始化值。TDZ 有几个容易踩的变体第一个是typeofconsole.log(typeof notDeclared); // undefined未声明的变量不报错 console.log(typeof declared); // ReferenceErrorTDZ 变量照样报错 let declared 1;很多人以为typeof是绝对安全的运算符这是 ES5 时代留下的印象。在 TDZ 面前它并不可靠这一点在排查报错时很有用如果typeof都抛了Cannot access ... before initialization那说明这个变量一定在同一个作用域后面用let或const声明过。第二个变体是函数参数默认值function fn(a b, b 1) { return a b; } fn(); // ReferenceError: Cannot access b before initialization参数的默认值是按顺序从左到右求值的a求值时b还没初始化直接掉进 TDZ。改成function fn(a 1, b a)就正常了因为b用a的时候a已经求值完成。这种写法在给参数设默认值时会碰到尤其是想把某个参数默认为另一个参数的函数时。第三个变体是类声明class也有 TDZ而且比let更严格const instance new MyClass(); // ReferenceError class MyClass {}3.3 从执行上下文理解提升的本质如果只记结论很容易混淆“提升”和“TDZ”这两件事因为听起来矛盾既然提升了为什么不能访问要解开这个疑惑得稍微往引擎里看一眼。JavaScript 执行代码前会先创建执行上下文其中包含两个环境变量环境和词法环境。创建阶段做的事情叫“声明实例化”扫描当前作用域内的所有声明var声明的变量会在变量环境里登记一个名字并初始化为undefined函数声明会被完整地放进环境所以函数声明能整体提升包括函数体而let和const声明的名字会被登记到词法环境里但标记为未初始化。代码开始执行后遇到let x 1这一行引擎才把词法环境里x的状态从未初始化改为已初始化赋上值。在这之前任何访问都会触发“未初始化”检查从而抛错。这就是 TDZ 的物理来源——它不是语法层面的禁令而是环境里一个真实存在的状态标记。顺便说一句正因为var和let注册在不同的环境对象里才引出了后面第 4 节要讲的全局对象挂载差异以及第 4 节里的重复声明报错规则。这些现象看起来零散根子都在这一层。知道了机制就能理解为什么 TDZ 报错总出现在函数开头这类位置函数体执行前不会扫描内部的let但函数体开始执行时所有let声明已经在词法环境里登记完毕所以你在第一行访问后面才声明的变量必然撞上未初始化状态。4. 重复声明、全局挂载与污染边界4.1 重复声明的规则差异与真实影响var允许重复声明后面覆盖前面var v 1; var v 2; console.log(v); // 2let和const在同一个作用域里不允许重复声明无论跟谁重复let a 1; let a 2; // SyntaxError: Identifier a has already been declared var b 1; let b 2; // SyntaxErrorvar 和 let 混用同样报错注意第二个错误的性质它是在语法解析阶段抛出的SyntaxError不是运行时错误。这意味着整段脚本根本不会执行而不是执行到那一行才失败。这个区别在实际调试时很关键——如果你在控制台里贴了一段代码结果报SyntaxError那前面的代码可能一行都没跑不要误以为是逻辑问题。重复声明的限制在实际项目里的价值比想象中大。以前用var时两个人分别在不同模块里声明同名的全局变量后加载的会把前面的悄悄覆盖掉运行起来完全不报错只是某个功能默默失效。换成let之后这种冲突会在加载时直接暴露成语法错误虽然也需要修但至少是“立刻发现”而不是“上线三天后用户投诉”。不过有一点要注意let的重复声明检查只在同一个作用域内生效嵌套块之间是允许同名的let name 外层; { let name 内层; // 合法这是另一个块级作用域 console.log(name); // 内层 } console.log(name); // 外层这叫遮蔽shadowing是块级作用域的正常用法和重复声明不是一回事。写代码时合理利用遮蔽能减少命名焦虑比如各个块里都可以叫result但反过来也要小心遮蔽层数太多容易搞不清当前用的是哪一个。4.2 顶层声明到底挂在谁身上在脚本顶层不是模块、不是函数内声明变量时三个关键字的表现又不一样var g1 1; let g2 2; const g3 3; console.log(window.g1); // 1 console.log(window.g2); // undefined console.log(window.g3); // undefinedvar在顶层声明的变量会成为全局对象的属性let和const不会。原因是var被登记在全局的变量环境里而全局变量环境对象就是全局对象本身let和const被登记在全局的词法环境里这是一个独立于全局对象的结构。这个差异带来两个实际后果。第一用let声明的全局变量不会污染window减少和其他库的冲突概率第二它依然可以在其他脚本里访问因为共享同一个全局词法环境只是不体现为对象属性// 第一个脚本 let shared a; // 第二个脚本 console.log(shared); // a能访问 console.log(window.shared); // undefined这也解释了一个常见困惑有些人用window.xxx去判断某个全局变量是否存在明明变量能用判断结果却是undefined。原因就是那个变量是用let或const声明的。在模块环境里情况又变了var也不会挂到全局对象上因为模块有自己的作用域。现代项目基本都是模块化开发所以“全局污染”这个老问题在实际业务代码里已经很少见了真正需要警惕的是往window上手动挂东西这种主动行为。4.3 全局污染的现实代价说一下我遇到过的真实麻烦。某项目里有个工具函数挂在全局用var声明后来引入的一个第三方脚本里也有同名的全局变量也是var两者互相覆盖表现是页面某个角落的数字偶尔算错。排查花了大半天因为控制台里看到的始终是“正确”的那个值——打印的时候覆盖已经发生完了。这类问题的根源不在于var本身而在于隐式创建全局变量这种坏习惯。不过var确实更容易促成这类问题因为它容忍重复声明并且挂到全局对象上等于把所有冲突都藏起来了。用let和const之后冲突变成语法错误虽然也需要处理但至少是明面上的。顺带提一个常见误解有人说let比var快因为块级作用域“查找范围更小”。这个说法在现代引擎上基本不成立。真实的性能差异主要来自变量被引擎优化成寄存器还是留在内存里而这取决于变量是否被闭包捕获、生命周期多长跟用哪个关键字关系不大。选择let的理由是语义清晰和避免踩坑不是性能。5. const 到底锁住了什么5.1 绑定不可变值本身仍然可变这是const最容易搞错的一点我用一个例子说清楚const user { name: A }; user.name B; // 可以改的是对象属性 console.log(user.name); // B user { name: C }; // TypeError: Assignment to constant variableconst锁住的是“user这个名字指向哪个值”这层绑定关系不是值的内容。对象是引用类型user存的是一个地址只要不改这个地址怎么折腾对象内部都没问题。想改绑定的指向就会抛TypeError。同理数组可以push、pop、改下标但不能重新赋值const list [1, 2]; list.push(3); // 可以 list[0] 99; // 可以 list []; // TypeError还有一点要注意const声明时必须初始化除非在for-in和for-of的头部const a; // SyntaxError: Missing initializer in const declaration for (const key in obj) { /* 合法每轮迭代自动赋值 */ } for (const item of arr) { /* 合法 */ }for-in/for-of是特例因为绑定在每轮迭代时由引擎自动赋值不需要你手写初始化。这个特性配合前面的逐迭代绑定机制在遍历时特别好用比如给每个元素创建独立的回调。5.2 Object.freeze 的边界在哪里既然const锁不住内容想真正冻结对象就得用Object.freezeconst config Object.freeze({ level: 1, nested: { deep: true } }); config.level 2; // 静默失败严格模式下抛 TypeError config.nested.deep false; // 可以冻结是浅层的两个坑要记住。第一Object.freeze是浅冻结只冻结第一层属性嵌套对象照样能改。第二非严格模式下修改冻结属性是静默失败不报错也不生效这种“看起来改了其实没改”的情况最容易误导人。如果你确实需要深冻结得自己递归function deepFreeze(obj) { Object.getOwnPropertyNames(obj).forEach(function (key) { const value obj[key]; if (value typeof value object) { deepFreeze(value); } }); return Object.freeze(obj); }不过我要泼一盆冷水业务代码里大部分场景不需要真冻结。const提供的那层绑定保护已经足够覆盖常见需求深冻结的代价是递归遍历和后续调试的麻烦你想临时改个配置值调试结果改不动。真正需要冻结的场合主要是导出的常量配置、缓存映射这类“一旦初始化就不该被改”的结构用浅冻结通常够了。注意Object.freeze之后正则会失效lastIndex不可写、Date的内部槽位也无法更新冻结内置对象前先想清楚。5.3 什么时候必须用 let什么时候坚持 const关于选型我总结出一条经过多个项目验证的规则默认写const只有在确定这个变量会被重新赋值时才改成let。这条规则的第一个好处是减少认知负担。看到const你就知道这个变量的指向在后续代码里不会变读的时候不用一路往下找有没有重新赋值。看到let你就知道这里存在会变化的状态自然会更留神。第二个好处是减少误伤。实际调试时我最怕的是变量被意料之外的地方重新赋值尤其是循环里和条件分支里。const会把这个可能性直接排除掉。但有些场景用let更自然强行用const反而别扭// 需要累加/累积 let total 0; for (const item of list) { total item.price; } // 循环计数器 for (let i 0; i 10; i) {} // 需要条件赋值 let result; if (condition) { result doA(); } else { result doB(); } // 交换 let a 1, b 2; [a, b] [b, a];还有一种写法我不太推荐为了用const把条件赋值改成嵌套三元表达式。比如把上面那个result的写法改成一行三元看着“纯函数”了但可读性反而下降尤其是分支里有副作用或者逻辑超过两层时。我的原则是让代码更好读而不是让关键字更统一。6. 常见问题与排查技巧实录6.1 报错信息速查表这三个关键字相关的报错其实就那么几种认准报错原文能省一半排查时间报错信息触发场景排查方向Cannot access x before initialization在let/const声明之前访问找同一作用域里靠后的声明把访问语句挪到声明之后x is not defined变量在当前位置的作用域链上不存在检查是否出了块、是否拼写错误、是否漏了声明Identifier x has already been declared同一作用域内let/const重复声明或与var冲突检查同名声明注意switch的case共享一个块Assignment to constant variable给const变量重新赋值确认是否真的需要重新赋值需要就把声明改成letMissing initializer in const declarationconst x;没有初始化补上初始值或改用letTypeError: Cannot add property, object is not extensible严格模式下给冻结对象加属性检查是否被Object.freeze过这张表里最值得留意的是第一条和第三条它们占了实际排查时间的绝大多数。尤其是 TDZ 报错新手容易把它当成“变量没定义”然后跑去检查拼写和引入路径方向就偏了。记住那条判据就行报错里出现before initialization就直接在附近找let/const声明。6.2 一套可复用的排查流程遇到变量相关的异常我一般按这个顺序走基本能在五分钟内定位第一步看报错类型。ReferenceError说明名字或访问时机有问题TypeError说明值或操作有问题SyntaxError说明整段脚本没跑起来要优先处理语法。第二步看作用域边界。判断报错位置处于哪个函数、哪个块内然后在块内往上找声明。如果是循环体里要特别注意var声明的变量会不会在循环外被读到。第三步做最小化验证。把可疑的那几行单独拎到控制台或空文件里跑去掉无关逻辑。很多问题在这个过程里就自己暴露了因为脱离了大段业务代码作用域关系变得一目了然。第四步利用调试器的作用域面板。在断点停下来之后Chrome DevTools 的 Scope 面板会把Local、Block、Closure、Global分开列出来你能直接看到某个变量落在哪一层。这比单纯读代码可靠得多尤其是嵌套闭包的情况。第五步临时替换法。把let换成var或者反过来看现象是否变化。这不是为了修复问题而是为了确认问题是否来自作用域或 TDZ。我经常用这招做二分——如果换成var之后不报错了那基本可以确定是 TDZ 或块级作用域引起的。顺便分享一个调试小技巧在怀疑有 TDZ 的地方可以临时插一行debugger;然后用console.log(typeof 变量名)试探。如果直接抛before initialization说明声明在下面如果返回undefined说明压根没这个变量或者已经初始化为undefined了。6.3 用工具把规则固化下来靠人记住这些规则不现实尤其是在多人协作的项目里。我的做法是把约束交给静态检查工具让它替人把关。主流检查工具里有两条规则基本是标配一条禁止使用var另一条建议能用const的地方就用const。开启之后提交前就能把大部分问题挡掉。配置大概长这样{ rules: { no-var: error, prefer-const: error, block-scoped-var: error, no-shadow: warn } }第三条block-scoped-var专门检查var是否被用在它声明块之外能提前发现那种循环变量泄漏的问题。第四条no-shadow是遮蔽检查我把它设成警告而不是错误因为合理遮蔽是有价值的但过度遮蔽确实会降低可读性交给开发者自己判断。还有一点值得提醒如果你的项目用了转译工具把新语法降级到 ES5那么let和const在产物里会被改写成var块级作用域靠变量重命名来模拟TDZ 一般不会全都模拟到位。所以不要依赖转译产物去验证 TDZ 行为调试时要么用现代浏览器直接跑源码要么在开发环境保留原始语法。我曾经在这个上面浪费过一小时——本地测试环境里明明复现不了线上却报错最后发现是两边的构建配置不一样。注意降级转译对for循环里的let会额外生成闭包包裹的代码产物体积和运行时的对象创建都会变多。如果循环体很大且对性能敏感值得看一下编译产物确认没有意外开销。7. 我个人的使用习惯与几条硬建议写到这里把我在实际项目里形成的几条习惯摊开说说都是踩坑之后固定下来的不是什么规范推荐。第一条新代码里不出现var。这条没有例外。哪怕是写一个三行的临时脚本我也用const和let。理由是习惯一旦松动老写法就会慢慢回流最后整个文件风格混乱。第二条默认const遇到需要改再换成let。写的时候不要提前预判“这个值可能会变”先写const等真的需要重新赋值时编辑器或检查工具会提示你那时候再改成本极低而且能顺便确认这个变化是不是真的必要。第三条循环里一律用let或const绝不用var。这是最容易出闭包问题的地方也是收益最直接的地方。for-of和for-in的迭代变量直接用const因为每轮都是新绑定。第四条块内尽量不声明函数需要局部函数就用箭头函数赋给const。这样能避开块级函数声明在某些环境下的历史遗留行为语义也更明确。第五条导出的常量和配置用const加浅冻结涉及到多层结构时把嵌套层级压平或者接受它不会被完全冻结这个事实。深冻结留到真正需要的场景再用。最后补一个小细节如果你在维护一段老代码不确定某个var能不能安全改成let判断方法很简单——找这个变量的作用域边界。如果它只在声明所在的函数体内使用且没有在循环里被闭包捕获那改成let基本安全如果它在块外被访问或者被循环里的函数引用那就要小心改完之后必须实际跑一遍相关逻辑。我见过不少“批量替换”引发的线上问题都是因为跳过了这一步验证。改语法很简单确认语义没变才是真正的工作量。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询