☰
JS Tips 深度解读:JavaScript 变量声明(Basics Declarations)中的作用域与隐式全局陷阱
2026/10/9 7:46:30 网站建设 项目流程
  • 教程

【免费下载链接】jstips

This is about useful JS tips!

项目地址:https://gitcode.com/gh_mirrors/js/jstips
点击查看免费下载

本文以 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。

这里有两个值得记住的语法点:

  1. var y, x = y = 1是var的多变量声明形式,逗号分隔,var y只是声明(值为undefined),x = y = 1是声明加初始化;
  2. 赋值运算符是右结合的,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 = 11 / 11 / 1顶层声明,两个全局变量
2(() => { var x = y = 2 })()2 / 12 /2(全局被改)y未声明,隐式泄漏到全局
3(() => { var x, y = 3 })()undefined/ 13 / 2局部声明,全局不受影响
4(() => { var y, x = y = 4 })()4 / 14 / 2先声明后赋值,局部遮蔽全局
5顶层x = 55 / 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!

项目地址:https://gitcode.com/gh_mirrors/js/jstips
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询