- 教程
【免费下载链接】jstips
This is about useful JS tips!
本文以 jstips 仓库中的 2016-02-16-basics-declarations.md(JS Tips #47)为核心,逐段拆解 JavaScript 中
var声明、链式赋值与闭包作用域的相互作用,重点揭示"未声明即赋值"导致的全局变量泄漏这一经典陷阱。读完本文,你将能准确预测任何var组合声明在函数/闭包内的行为,理解undefined的出现时机,并掌握在严格模式与let/const时代如何写出无副作用的声明代码。
一、这条 Tip 讲了什么:一个可运行的五连实验
仓库中该文档的原标题是Basics declarations,作者 adaniloff 用一组带console.log的短小代码,展示了 JavaScript 中"声明变量"的不同写法。整个实验在同一段连续代码中推进,前一个场景的变量状态会延续到后一个场景——这恰恰是理解"全局变量被意外污染"的最佳方式。
原文共五个场景,全部运行在浏览器控制台或 Node.js 中即可复现。本文先给出完整代码与输出,再逐行解释原理,并在最后补充底层机制与工程建议。原文还特别注明可以用 Babel 在线 REPL 验证(外链不再赘述),你完全可以在本地 Node.js 环境或浏览器 DevTools Console 中直接粘贴运行(仓库只读,仅作查看与验证即可):
node -e "var y, x = y = 1; console.log('--> 1:', \`x = \${x}, y = \${y}\`)"二、场景一:同一行声明并链式赋值var y, x = y = 1
var y, x = y = 1 //== var x; var y; x = y = 1 console.log('--> 1:', `x = ${x}, y = ${y}`) // Will print //--> 1: x = 1, y = 1第一行代码等价于三件事:先声明var x;、var y;(都是顶层/全局声明),再执行链式赋值x = y = 1。赋值表达式y = 1的结果是 1,随后把 1 赋给x,因此x与y都等于 1。
这里有两个值得记住的语法点:
var y, x = y = 1是var的多变量声明形式,逗号分隔,var y只是声明(值为undefined),x = y = 1是声明加初始化;- 赋值运算符是右结合的,
x = y = 1实际按x = (y = 1)求值,这也解释了为什么y必须先存在——它先被赋值为 1,其值再流入x。
由于这段代码位于最顶层(全局作用域),x和y都是全局变量。输出x = 1, y = 1完全符合预期,"这里并没有很多值得大惊小怪的东西"——原文如是说,真正的戏肉从下一个场景开始。
三、场景二:闭包内var x = y = 2—— 未声明的y泄漏成全局变量
;(() => { var x = y = 2 // == var x; x = y = 2; console.log('2.0:', `x = ${x}, y = ${y}`) })() console.log('--> 2.1:', `x = ${x}, y = ${y}`) // Will print //2.0: x = 2, y = 2 //--> 2.1: x = 1, y = 2这里用了一个IIFE(立即执行函数表达式)创建闭包。注释明确写出了等价展开:var x; x = y = 2;——关键就在这:y并没有被var声明!
在非严格模式下,对未声明变量y赋值不会报错,而会隐式创建一个全局变量(或者命中已存在的全局y)。由于场景一已经在全局留下了y = 1,这里y = 2直接把全局的y改成了 2。
- 闭包内部:
x是var声明的,属于闭包局部变量,值为 2;y在闭包内找不到声明,沿作用域链向上找到全局y,值为 2,所以输出2.0: x = 2, y = 2; - 闭包外部:全局
x仍停留在场景一的 1,而全局y已被悄悄改成 2,所以输出--> 2.1: x = 1, y = 2。
原文的总结一针见血:"代码只改变了全局的y,因为我们并没有在闭包里声明此变量"。这就是隐式全局(implicit global)污染:一行看似普通的赋值,却绕过了作用域边界,改写了闭包外的状态——这是大型项目中难以排查的 bug 温床。
前置知识:为何
;开头?;(() => {...})()行首的分号是防御性写法,防止前一行代码在自动分号插入(ASI)机制下与 IIFE 粘连出错。仓库中还有一篇专门讲解 IIFE 的 2016-01-25-Using-immediately-invoked-function-expression.md 可参考。
四、场景三:闭包内var x, y = 3—— 正确声明,全局不受影响
;(() => { var x, y = 3 // == var x; var y = 3; console.log('3.0:', `x = ${x}, y = ${y}`) })() console.log('--> 3.1:', `x = ${x}, y = ${y}`) // Will print //3.0: x = undefined, y = 3 //--> 3.1: x = 1, y = 2这次闭包内两个变量都显式声明了:var x;(无初始化)与var y = 3;。于是:
x声明后未赋值,值为默认的undefined,输出3.0: x = undefined, y = 3——这正是仓库另一篇 2016-01-05-differences-between-undefined-and-null.md 中所说的:JavaScript 会自动把未赋值的变量初始化为undefined;- 由于
x、y都是闭包局部变量,对它们的赋值完全在局部作用域内完成,全局的x(=1)与y(=2)保持原状,输出--> 3.1: x = 1, y = 2。
原文强调:"现在我们用var声明了两个变量,意味着它们仅在闭包内有作用。"对比场景二与场景三,区别只在是否用var声明——这正是"声明"与"赋值"在作用域语义上的分水岭。
五、场景四:闭包内var y, x = y = 4—— 先声明后赋值,局部遮蔽全局
;(() => { var y, x = y = 4 // == var x; var y; x = y = 4 console.log('4.0:', `x = ${x}, y = ${y}`) })() console.log('--> 4.1:', `x = ${x}, y = ${y}`) // Will print //4.0: x = 4, y = 4 //--> 4.1: x = 1, y = 2与场景二形式几乎一样,但多了var y,前缀。展开后是var x; var y; x = y = 4;——两个变量都先声明,再执行链式赋值。这样一来:
- 链式赋值
y = 4命中的是闭包内局部y,随后x = 4同样落在局部x上,闭包内输出4.0: x = 4, y = 4; - 因为局部变量遮蔽了同名全局变量,且赋值没有越过作用域边界,全局的
x(=1)、y(=2)完好无损,输出--> 4.1: x = 1, y = 2。
原文的注释点破了局部与全局的优先级:"两个变量都用var声明了,之后才给它们赋值。由于local > global,x和y在闭包内是局部的,意味着全局的x和y未被触碰。"这就是 JavaScript 作用域链查找的基本规则:先找局部,找不到再逐级向外,直到全局。
六、场景五:直接x = 5—— 对既有全局变量的再赋值
x = 5 // == x = 5 console.log('--> 5:', `x = ${x}, y = ${y}`) // Will print //--> 5: x = 5, y = 2回到全局作用域,直接对x赋值 5。此时全局x早已在场景一中声明存在,因此这行只是普通的再赋值,x从 1 变为 5;全局y依然是场景二中被泄漏改写的 2。输出--> 5: x = 5, y = 2。
原文评论:"最后这一行的结果是显而易见的。"它看似平淡,实则在提醒我们:一旦全局命名空间被隐式污染(如场景二中的y),后续所有代码都在与这个"意外共享状态"打交道——这就是为什么要警惕隐式全局。
七、五个场景结果对照总表
| 场景 | 代码(在闭包内则标注) | x(闭包内/全局) | y(闭包内/全局) | 关键结论 |
|---|---|---|---|---|
| 1 | 顶层var y, x = y = 1 | 1 / 1 | 1 / 1 | 顶层声明,两个全局变量 |
| 2 | (() => { var x = y = 2 })() | 2 / 1 | 2 /2(全局被改) | y未声明,隐式泄漏到全局 |
| 3 | (() => { var x, y = 3 })() | undefined/ 1 | 3 / 2 | 局部声明,全局不受影响 |
| 4 | (() => { var y, x = y = 4 })() | 4 / 1 | 4 / 2 | 先声明后赋值,局部遮蔽全局 |
| 5 | 顶层x = 5 | 5 / 5 | — / 2 | 对已声明全局变量再赋值 |
对照表可以清楚看到:只有场景二污染了全局y,其余场景的全局状态都保持稳定。整个实验的设计逻辑,就是围绕"声明是否存在"这个变量来反复对比。
八、底层原理:隐式全局、提升与严格模式
8.1 隐式全局为什么存在
在非严格模式下,y = 2这类对未声明变量的赋值,等价于隐式创建全局属性(在浏览器中即window.y = 2)。这是 ES5 之前遗留的宽松行为。仓库中的 2016-01-07-use-strict-and-get-lazy.md 明确警告:"默认情况下,JavaScript 允许程序员相当粗心,比如不要求用var声明就引入变量……这也是当变量名拼错或意外越出作用域时许多错误的根源。"该文给出的对策正是开启严格模式:
// Whole-script strict mode syntax "use strict"; var v = "Hi! I'm a strict mode script!";在严格模式下,对未声明变量赋值会直接抛出ReferenceError,场景二那样的泄漏在运行时就会被拦截,而不是静默污染全局。
8.2 声明提升(hoisting)与"声明 vs 定义"的区别
var的声明会被提升到所在函数作用域的顶部,但赋值不会。仓库中的 2016-01-11-hoisting.md 对此讲得很清楚:**声明(declaration)**让系统知道变量存在,**定义/赋值(definition)**才给它值。例如:
console.log(definedLater); // Outputs: undefined —— 声明被提升,赋值未提升 var definedLater; definedLater = 'I am defined!'; console.log(definedLater); // Outputs: 'I am defined!'回到本 tip:场景四中var y, x = y = 4之所以安全,正是因为这行代码"先声明、后赋值",赋值发生时局部y已经存在并遮蔽了全局y;而场景二中y = 2在闭包内从未被声明,提升机制也无从帮忙,赋值便顺着作用域链落到了全局。
8.3 声明与赋值在其他场景中的语义呼应
undefined是默认值:声明未赋值即得到undefined(场景三的x),详见 2016-01-05-differences-between-undefined-and-null.md;- 模板字符串:本 tip 全程使用
`x = ${x}, y = ${y}`进行输出,这是 ES6 模板字符串的插值语法,参见 2016-01-09-template-strings.md; - 传参机制:
var x = y = 2中y之所以能"逃出"闭包,本质上是作用域链解析而非传参问题;关于对象/引用的传递方式可参见 2016-02-13-know-the-passing-mechanism.md。
九、现代写法:let/const与严格模式下的声明纪律
本 tip 写于 2016 年,基于var的语义展开。ES6 之后,仓库中的 2016-08-25-keyword-var-vs-let.md 给出了更现代的作用域规则:
var是函数作用域(在函数外声明则为全局);let/const是块级作用域,且存在暂时性死区(TDZ)——在声明之前访问会抛ReferenceError,而不是得到undefined。
{ console.log(c); // undefined. Due to hoisting var c = 2; } { console.log(b); // ReferenceError: b is not defined let b = 3; }对照本 tip 的场景二可以得出一个清晰的现代建议:
| 问题 | var时代的行为 | 现代推荐 |
|---|---|---|
| 忘记声明就赋值 | 非严格模式:隐式创建全局变量(静默污染) | 开启严格模式,或改用let/const,让引擎直接报错 |
| 闭包内想创建局部变量 | 必须显式var声明 | 使用let/const,块级作用域更精确 |
链式赋值x = y = 2 | 极易让y泄漏 | 拆成let x = 2; let y = 2;,清晰且无歧义 |
如果必须使用var,记住本文的黄金法则:"要么用var显式声明,要么就别指望它是局部的。"每次写x = y = v之前,先问自己:y是否已经在当前作用域声明过?没有的话,它正在改写的很可能是全局状态。
十、在仓库中继续深入
本文围绕的核心文档位于 _posts/en/javascript/2016-02-16-basics-declarations.md,仓库还提供 简体中文翻译版(标题为"变量声明")。推荐的配套阅读路径:
- Hoisting 提升:理解"声明被提升、赋值不提升";
- use strict 与隐式全局的约束:严格模式如何拦截未声明赋值;
- var vs let:块级作用域与暂时性死区;
- undefined 与 null 的区别:
undefined是"未赋值"的默认值; - IIFE 立即执行函数:本 tip 闭包实验所依赖的语法。
验证本文所有输出最直接的方式:打开 Node.js 或浏览器控制台,把五个场景的代码依次粘贴运行,观察x、y在每个阶段的变化——理论与实践一一对应,正是这条 JS Tips 的价值所在。
- 教程
【免费下载链接】jstips
This is about useful JS tips!
相关推荐
jstips 精读:深入理解 JavaScript 变量声明的五种写法(Basics: Declarations)
jstips 精读:深入理解 JavaScript 变量声明的五种写法(Basics: Declarations) 导读 本篇文章基于 jstips 仓库的第
教程JS Tips 11 深度解析:JavaScript 变量提升(Hoisting)——声明、定义与函数作用域组织
JS Tips 11 深度解析:JavaScript 变量提升(Hoisting)——声明、定义与函数作用域组织 变量提升(Hoisting)是 JavaScr
教程Diff-SVC歌声转换实战指南:从环境搭建到专业级应用
Diff SVC歌声转换实战指南:从环境搭建到专业级应用 Diff SVC是一个基于扩散模型的歌声转换系统,能够将任意人声转换为目标歌手的声音特征。这项技术结合
人工智能深度学习语音音频
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考