
1. 从一次翻车开始为什么连“赋值”都这么多讲究我记得刚学JS那会儿特别喜欢抄网上的代码段往自己页面里塞。有一次从Stack Overflow抄了一段处理表单的逻辑里面有个变量写的是let userInput document.getElementById(username).value;我照抄之后页面死活报错提示userInput is not defined。当时我一脸懵明明代码就在那儿怎么就没定义呢后来一排查发现我把let userInput写成了let UserInput后面用的时候又是userInput——一个字母大小写的差异折腾了我快两个小时。从那时候起我就明白JS里的变量、字面量这些最基础的概念如果只是“好像懂了”的程度后面写代码真的会被各种奇怪的问题教会做人。这篇博文我不会跟你念教科书我想用当年踩坑的视角把这些概念掰开揉碎讲清楚顺便聊聊那些查文档经常忽略、但实际开发特别关键的细节。无论你是刚接触JS还是学了几个月还在云里雾里或者写了一阵子发现老在一些低级的“变量问题”上报错这篇内容都适合你。我会把变量和字面量这两个最基础的东西彻底讲透包括它们怎么用、为什么这么用、什么场景容易出问题以及如何从一开始就写出更稳的代码。我先说一句我的整体观点变量和字面量就是JS这座大厦的砖和水泥几乎所有其他概念——函数、对象、闭包、作用域——都建立在它们之上。如果你把这两个概念理解到“肌肉记忆”的程度后面学什么都会轻松很多。2. 字面量代码里直接写出来的那些“值”2.1 什么是字面量先别背定义“字面量”这个词很多教材直接翻译成“literal”然后解释成“代码中直接书写的值”。这解释听着挺对但你仔细一琢磨好像也没说啥。我换个说法字面量就是你在代码里“直接写出来”的那个值本身。比如let age 18;这里的18就是一个字面量。再比如let name 张三;这里的张三也是字面量。从本质上来讲字面量描述了“我就是要这个值”而不是“我去哪儿算一个值来用”。有人可能会问这有啥好学的不就是直接写数字和字符串吗别急JavaScript里的字面量种类比你想象的丰富。除了最基础的数字和字符串还有数组字面量、对象字面量、布尔字面量、正则字面量甚至ES6之后有了模板字符串字面量。你在代码里直接写[1, 2, 3]这就是数组字面量直接写{ name: 张三 }这就是对象字面量直接写/abc/这就是正则字面量。理解字面量真正的价值在于你看代码的时候能一眼认出“这是个固定值”还是“这是个变量引用”。这看起来简单但很多人调试代码的时候看到一行if (status 1)就开始查status从哪儿来的却忘了1本身就是写死的字面量——这个值不会变变的只可能是status。2.2 常见字面量类型到底有哪些我整理了一下JS中最常见的字面量形式每个都配上实际代码方便你对照着记数字字面量最直接的类型。100、3.14、-42都是数字字面量。JS里还有个特殊数字NaN它表示“不是一个数字”但它也是数字类型。另外ES2021之后还支持数字分隔符比如1_000_000表示一百万这个在数字很大的时候特别好用可读性提升非常明显。字符串字面量用单引号、双引号或反引号括起来的内容。hello、world、你好都是字符串字面量。单引号和双引号在JS里基本没区别但反引号不一样它支持字符串插值也就是可以在字符串里直接嵌入变量。例如let name 张三; console.log(你好${name});这个${name}的写法就是模板字面量的核心特性比过去写字符串拼接你好 name简洁太多。布尔字面量只有两个true和false。它们表示“真”和“假”在条件判断里天天用。数组字面量用方括号括起来的一串值比如[1, 2, 3]、[苹果, 香蕉]。数组字面量在JS里出现的频率极高因为很多数据本身就是列表形式。对象字面量用花括号括起来的键值对集合比如{ name: 李四, age: 25 }。这是JS里最强大也最常用的字面量形式后面写任何稍微复杂一点的程序都离不开它。正则字面量用斜杠包裹的匹配模式比如/\d/g表示匹配所有数字。这个对于处理字符串非常有用初学阶段不一定要立刻掌握但至少要知道代码里那个“夹在两个斜杠之间的东西”也是字面量。2.3 字面量和数据类型到底啥关系很多人会混淆“字面量”和“数据类型”觉得这俩是一回事。我在各个技术社区里看到这个问题的频率特别高有人问“字面量类型和数据类型的区别是什么”下面回复五花八门。我打一个比方字面量是你写在纸上的一串字数据类型是这串字适合装进什么样的格子里。字面量决定了“内容是什么”数据类型决定了“计算机怎么理解这个内容”。18这个字面量如果被识别为数字类型就可以参与加减乘除如果被识别为字符串类型那就是两个字符1和8没法直接算乘法。所以更准确地说每个字面量都有它对应的数据类型但数据类型本身是更大的概念。数据类型还包括没有“字面量写法”的一些特殊情况比如undefined表示“未定义的值”它本身是全局属性不是从某个“字面量对象”里来的。还有null表示“空值”它是独立的一个值但它是有字面量写法的直接写null就行。这里有个极其容易踩的坑undefined和null都表示“没有值”但含义不一样。undefined是声明了变量但没赋值系统给的默认值null是你主动表示“这个位置我放个空”。我刚开始写代码的时候经常混用要么把没数据的情况赋值成undefined要么把应该初始化的变量写成null后来才发现这些习惯会在团队协作中造成很多不必要的沟通成本。2.4 为什么推荐尽量用“字面量”而不是“包装对象”JS里有个比较特殊的设计基本类型数字、字符串、布尔都有对应的“包装对象”比如new Number(18)、new String(hello)。用new关键字创建这些包装对象得到的结果跟直接用字面量写值在行为上有微妙区别。比如let a 18; let b new Number(18); console.log(a b); // falsea是数字基本类型b是对象类型两者用全等比较的时候不相等。这就是为什么我强烈建议你平时写代码一律用字面量不要用包装对象。字面量写法简洁、直观、性能更好而且不会出现“数字不等于数字”这种诡异的问题。在实际项目中我见过一些从Java或C#转过来的同学习惯用new String()或new Boolean()来创建值。在JS里这是一个非常不推荐的做法不仅没有好处还会引入类型判断麻烦。直接用hello、true这些字面量就好少敲好几个字符省心又安全。3. 变量给值起个名字但名字背后有讲究3.1 变量到底是个啥为什么需要变量变量这个概念不同语言里解释大同小异。简单说变量就是一个“有名字的容器”用来在内存里存储值。你在代码里写let count 1;就是告诉JS引擎“我在内存里划一块地儿这块地儿叫count里面存了数字1。”之后想用这个值的时候不需要重复写1直接写count就行。你可能会问我直接写字面量不就完了吗为什么非要绕一圈存到变量里因为真实场景里值会变。比如你做一个计数器用户每点击一次按钮数字就加1。如果直接用字面量你没法表达“在原来基础上加1”这个逻辑。有了变量你就能写let count 0; count count 1;第一行创建一个叫count的变量初始值是0。第二行把count当前的旧值取出来加1再放回到count里。这个过程就叫“赋值”。变量让程序有了“记忆”这是它最核心的价值。3.2 声明变量的三种方式var、let、constJS里声明变量有三种关键字var、let、const。很多新手一开始就懵为什么一个声明要搞三个关键字我用最简单的逻辑给你理清楚。var是ES6之前就有的老家伙。它的特点是“函数作用域”和“变量提升”。啥意思你用var在函数内部声明一个变量这个变量在整个函数体内都可见哪怕你是在函数末尾才声明的函数开头也能访问到值会是undefined。这个特性容易造成很多难以预测的行为。举个极端例子function test() { console.log(a); // undefined不报错 if (true) { var a 10; } console.log(a); // 10 }var a虽然写在if块里面但它实际上在整个test函数作用域里生效所以函数开头的console.log(a)不会报错。这在老代码里经常导致变量泄漏和意外覆盖现在新项目已经不太推荐用var了。let是ES6引入的“块级作用域”版本。用let声明的变量只在它所在的{}代码块里有效。上面的例子如果换成let a 10函数开头的console.log(a)就会直接报错ReferenceError: Cannot access a before initialization。这种报错其实是好事它能帮你尽早发现“变量还没初始化就用了”的逻辑问题。const跟let类似但多了一个限制声明时必须赋值而且之后不能再重新赋值。用const声明的变量是一个“常量引用”。注意关键词是“引用”不是“值”。这里有个非常经典的坑你用const声明一个对象这个对象的内容是可以修改的。比如const user { name: 张三 }; user.name 李四; // 这行不会报错 console.log(user.name); // 李四从语言角度来说const user保证的是“user这个变量不能再指向别的对象”但对象内部的属性依然可变。很多人在这一点上栽跟头以为const声明的对象就不能改了。那如果你想让整个对象不可变呢需要用Object.freeze()方法但那是更深一层的知识初学阶段先记住“const指针不可变但指针指向的内容可能变”就够了。3.3 变量声明和赋值是两件不同的事很多初学者会把“声明变量”和“给变量赋值”混在一起觉得let count 0;是一步操作。实际上JS引擎执行这行代码时内部是分开处理的先声明count再给它赋值为0。这两步虽然写在一行但概念上必须分开理解。为什么这个概念重要因为它直接关系到“变量提升”和“暂时性死区”这两个JS里著名的机制。变量提升HoistingJS引擎在代码执行之前会先扫描一遍当前作用域里的var和function声明把它们“提前”到作用域顶部。这就是为什么你用var写在函数末尾的变量函数开头访问也不会报错。但提升只提升声明不提升赋值。你想一下这行代码console.log(name); // undefined var name 张三;引擎实际执行顺序相当于var name; // 声明提升了 console.log(name); // undefined因为赋值还没执行 name 张三; // 赋值还在原来的位置暂时性死区TDZlet和const也有提升但它们不会初始化。在声明之前的区域你访问这个变量会直接抛错。这个“声明之前不能访问”的区域就叫暂时性死区。我理解这个概念的时候把它类比成电影院座位。你用var买了一张票虽然票上写了座位号但你随时可以进放映厅乱坐系统不怎么管你用let或const呢座位号写死了开场前你硬要坐进去检票员JS引擎直接把你拦下来。这个“拦住”的错误提示其实是JS在帮你强制规范代码逻辑。3.4 变量命名的规则还有那些没人明说但很重要的约定变量命名是初学阶段最容易忽略、但到后面影响最大的一件事。规则层面比较简单记住四点变量名只能包含字母、数字、下划线_和美元符号$不能以数字开头不能是JS保留字比如if、for、let这些对大小写敏感name和Name是两个不同的变量这些是语法层面的硬规则违反就报错。但项目里真正让你头疼的往往是“没违反规则但不合理”的命名。我见过不少人给变量起名叫a、b、c或者temp、data、xxx。你写的时候觉得省事了过两周自己回来看代码完全不记得a是啥意思。我自己现在坚持的一条规定是变量名要能表达“这个变量存的是什么”。价格用price用户名用username列表用list或items。如果觉得名字太长就用更精确的缩写但别为了省事放弃可读性。对于布尔类型的变量我喜欢用is、has、can开头比如isLoggedIn、hasPermission这样读代码的时候一眼就知道这个变量是“真/假”判断。还有一个比较推荐的习惯是“不用拼音命名”。拼音变量名在很多老项目里经常遇到比如mingzi、nianling。不是不能用但英文单词的通用性远高于拼音。要是英语水平有限现在网上一查翻译很容易花一分钟查个准确的英文术语比留个拼音让后来的人猜半天要强得多。4. 变量与字面量的组合拳从赋值到表达式4.1 赋值语句的本质把右边的值放进左边的容器理解了变量和字面量分别是什么之后就该看它们怎么配合了。最常见的组合是赋值语句let 变量名 字面量;。是赋值符号它的意思是“把右边的值赋给左边的变量”。这个方向不能搞反。const b 8;是把字面量8赋给变量b。如果你写成const 8 b;JS引擎直接报错因为8是一个字面量不是一个可以容器的名字。赋值语句可以多次执行。第一次给变量一个值后面可以随时换成新的值前提是用let声明的变量const不行。比如let score 10; // 第一次赋值 score 20; // 重新赋值 score score 5; // 读取当前值计算再赋回去 console.log(score); // 25重点理解最后一行score score 5;。JS引擎处理这行时先看等号右边score当前值是20加5得25然后把这个25放进score变量。整个过程里等号右边的score读的是旧值等号左边的score是“写入目标”。新旧值虽然共享同一个变量名但在赋值那一瞬间它们在逻辑上是完全分开的。4.2 变量在表达式中的读取机制变量不光能出现在赋值语句的右边还能出现在各种表达式里。比如let width 5; let height 3; let area width * height; console.log(area); // 15第三行里width和height在计算时都会被替换成它们存储的数值width * height就相当于5 * 3。注意这里的“替换”是在运行时发生的。也就是说变量在表达式里是动态读取的——你用变量当前的值参与运算而不是用变量声明时的初始值。这个动态特性有时候会带来反直觉的行为。比如let count 1; let result count 10; // result 得到 11 count 100; console.log(result); // 还是 11result在计算时用了当时count的值1得出11。后来count改成100result并不会跟着变因为它已经把计算完的结果存在自己的内存里了。新手特别容易在这里产生误会以为“result 跟 count 绑定了”。JS里没有这种“绑定关系”变量存的就是当时算出来的值。当然如果你存的是对象情况会复杂一些。因为变量里存的其实是对对象的“引用”所以当你用两个变量指向同一个对象时它们共享同一个对象。想体现这点看下面的例子let obj1 { value: 1 }; let obj2 obj1; obj2.value 999; console.log(obj1.value); // 999obj2.value 999这行改的是obj1和obj2共同指向的那个对象的内容。对于初学者来说这个现象非常反直觉我只是改了obj2为什么obj1也变了因为对象变量里存的是“指向内存里某个对象的地址”obj2 obj1复制的是这个地址不是对象本身。这个概念理解了后面学深拷贝、浅拷贝、状态管理的时候会轻松很多。4.3 不同数据类型的变量在运算中的差异变量的数据类型不同同样用号运算结果可能天差地别。数字类型用是加法运算let a 10; let b 20; console.log(a b); // 30字符串类型用是拼接let firstName 张; let lastName 三; console.log(firstName lastName); // 张三如果一边是数字、一边是字符串呢JS会把数字自动转成字符串然后进行拼接let a 10; let b 20; console.log(a b); // 1020这个规则让很多人懵过。特别是从后台接口拿到数据明明以为是个数字结果是带引号的字符串一相加就变成了字符串拼接结果完全不对。解决方法是先做类型转换。最粗暴的方式是调用Number()let a 10; let b 20; console.log(a Number(b)); // 30还有一种更简短的技巧用一元加号运算符console.log(a b); // 30注意两个加号之间没有空格这个b的意思是把b强制转成数字。挺酷但可读性稍微差点团队项目里如果大家不熟悉这个写法建议还是用Number()更稳妥。4.4 一个巩固概念的小案例交换两个变量的值理解了变量和字面量我们来做一个非常经典的练习交换两个变量的值。这个需求在排序算法里到处出现也是检验你是否真正理解“变量存的是值”的好例子。假设有两个变量let a 1; let b 2;你想让a变成2b变成1。直接写a b; b a;行不行不行。你试一下这个过程a b先把b的值2赋给a此时a和b都是2。接着b a把a当前的值已经是2赋给b结果b还是2。a原来的1已经在第一步被覆盖掉了找不回来了。正确做法是引入一个临时变量let a 1; let b 2; let temp a; a b; b temp; console.log(a); // 2 console.log(b); // 1temp在这里扮演“暂存处”的角色。先把a的旧值存到temp然后a可以放心地接收b的值最后再把temp里的旧值给b。整个过程就像你手里有两杯饮料要交换必须先找一个空杯子倒腾一下。ES6之后有个更简洁的数组解构写法let a 1; let b 2; [a, b] [b, a];这一行直接完成交换。原理是创建了一个新的数组字面量[b, a]也就是[2, 1]然后通过解构把2赋给a、把1赋给b。这个写法既优雅又不易出错但理解底层逻辑仍然需要前面“临时变量”的思路。5. 程序执行中的变量从作用域到内存5.1 全局作用域、局部作用域和块级作用域变量不是在哪里都能访问的。它在哪个作用域里声明就主要在哪个作用域里生效。JS里最常见的三类作用域全局作用域、函数作用域、块级作用域。全局作用域在函数和代码块外部声明的变量就是全局变量。它在整个JS程序里都能访问。全局变量在小型脚本里挺方便但项目一大全局变量越多命名冲突的概率越大。你声明一个name别人也声明一个name后声明的会覆盖先声明的bug 分分钟出现。函数作用域在函数内部用var声明的变量就属于函数作用域。函数外部访问不到函数内部随便用。块级作用域用let或const声明的变量属于最近的{}代码块。啥是代码块if语句的{}、for循环的{}、while的{}都是一个块。在块里声明的变量出了这个块就访问不了。我来构造一个综合例子let globalVar 我是全局变量; function demo() { var functionVar 我是函数作用域变量; if (true) { let blockVar 我是块级作用域变量; console.log(globalVar); // 可以访问 console.log(functionVar); // 可以访问 console.log(blockVar); // 可以访问 } console.log(globalVar); // 可以访问 console.log(functionVar); // 可以访问 // console.log(blockVar); // 这里会报错 }理解作用域有什么用最直接的作用是帮你排查“变量为什么访问不到”的问题。我在代码审查里经常看到新人把变量声明在某个if块里结果在块外面用当然报错。这个问题的根源不是代码写得少而是对作用域的理解不到位。5.2 变量提升的实际表现与背后的执行顺序前面提过变量提升这里再展开说说它在实际代码里带来的“迷惑行为”。看这段代码console.log(myVar); // undefined var myVar 5;如果你不了解变量提升会觉得它应该报错“myVar is not defined”但实际情况是打印出undefined。因为var myVar的声明被提到了作用域顶部但 5这个赋值还在原处。相当于引擎偷偷帮你改成了var myVar; console.log(myVar); // undefined myVar 5;有意思的是用let声明同样的代码结果完全不一样console.log(myLet); // ReferenceError: Cannot access myLet before initialization let myLet 5;let不存在“初始化为undefined”的步骤它在声明之前是一个“没法访问”的状态。这个“声明了但访问不了”的区域就是之前说的暂时性死区。我刚开始遇到这种报错的时候第一反应是“JS怎么这么坑”后来理解了引擎的执行顺序逻辑才明白这是为了让程序员尽早暴露“先使用后声明”的问题。如果你用的var程序可能不报错但值是undefined用这个undefined去参与运算后面一堆逻辑都会跑偏而且很难追查。let直接报错反而帮你在第一时间定位问题。5.3 内存视角原始类型与引用类型在变量中的存储差异前文提到对象和基础类型的存储方式不一样。这个差异在面试里几乎必考在实际开发里也是bug高发区。我给你用生活化的方式拆一遍。原始类型数字、字符串、布尔、null、undefined存储在“栈内存”里。变量直接存的就是值本身。你写let a 10; let b a;b复制的是10这个值。之后改b不影响a。引用类型对象、数组、函数的变量存的是“内存地址”真正的数据在“堆内存”里。你写let objA { value: 1 }; let objB objA;objB复制的是那个对象的地址。两个变量指向堆内存里的同一个对象。改任何一个另一个看到的都变了。这也是为什么现在前端框架比如React、Vue里特别强调“不可变数据”。如果你直接修改了对象里的某个值所有引用这个对象的地方都跟着变了UI渲染就乱套了。我举个实际场景。在一个项目里我从接口拿到一个用户列表然后想复制一份来做筛选let originalList [{ name: 张三 }, { name: 李四 }]; let filteredList originalList; // 并不能创建一份“新列表” filteredList.pop(); console.log(originalList.length); // 1原列表也被改了这个问题的原因就是filteredList和originalList指向同一个数组。要真正复制一份得用展开运算符let filteredList [...originalList];这样才创建了一个新数组。不过要注意这是浅拷贝如果数组里嵌套了对象嵌套的对象仍然是共享的。这个话题往深里说还能扯出结构化克隆、深拷贝的各种方案但初学阶段先记住“直接等号只是复制引用”就够了。5.4 变量的生命周期与垃圾回收虽然初学阶段不一定要深入理解垃圾回收但知道一点基本原理对理解内存泄漏非常有帮助。简而言之当一个变量不再被任何作用域或引用“使用”时JS引擎的垃圾回收器会在合适的时机把它占用的内存回收掉。这意味着你不需要像C语言那样手动free内存但也意味着如果某个变量被偷偷“引用”着而不自知内存就回收不掉久而久之程序变卡。最常见的“偷偷引用”场景是全局变量。全局变量只要页面不关就一直占着内存。你不经意间写了一个temp { ... }忘写letJS就把它变成全局变量了。这在严格模式下会报错但非严格模式下很容易溜过去。所以我现在自己写代码有个习惯任何变量声明都写const或let从不省略。这不仅是规范问题也是内存管理的基本修养。6. 变量字面量在真实项目中的体现6.1 从接口获取数据并渲染到页面的完整例子概念讲了不少我们来一个比较完整的实际例子。假设页面上有一段用户信息需要从接口获取然后显示!DOCTYPE html html langzh-CN body div idapp p idusername/p p idage/p /div script // 用对象字面量模拟接口返回的数据 const mockResponse { username: 张三, age: 25, education: { level: 本科, school: 某大学 } }; // 获取DOM元素引用 const usernameEl document.getElementById(username); const ageEl document.getElementById(age); // 从对象里读取值并赋给DOM元素的内容 usernameEl.textContent mockResponse.username; ageEl.textContent mockResponse.age 岁; /script /body /html这个例子虽然简单但里面有字面量对象字面量、数字字面量、字符串字面量、变量声明const声明变量保存DOM引用、从对象读取属性值、模板拼接等多个知识点。注意这里我特意用const声明usernameEl和ageEl因为这两个变量只是“指向DOM元素的引用”不需要重新赋值。如果你试图给usernameEl重新赋值程序会报错这能防止你无意间改变DOM引用的逻辑。6.2 在浏览器控制台里验证你对变量的理解我强烈推荐一种学习方式边看文章边打开浏览器控制台按F12把代码自己敲一遍。光看不练概念永远停留在“好像懂了”的层面。比如你可以依次输入let city 北京; console.log(city); city 上海; console.log(city);第一行创建变量并赋值为北京第二行打印出来第三行重新赋值第四行打印出来。你会看到控制台分别输出北京和上海。这看起来太基础了但亲手敲一遍和只读文章是完全不同的感受。你还可以试试故意写错比如console.log(notDefinedVar);控制台会输出一段红色报错。仔细观察报错信息——ReferenceError: notDefinedVar is not defined。这个报错信息里的关键词ReferenceError标明是“引用错误”说明JS压根找不到这个名字。熟悉各种报错类型能帮你以后快速定位问题。6.3 常见报错信息以及它们背后的含义根据我自己的经验新手阶段遇到的报错80%跟变量有关。我在下面整理了一张常见报错速查表建议遇到问题先对号入座。报错信息含义常见原因ReferenceError: xxx is not defined变量没有声明拼写错误、变量未定义就使用TypeError: Cannot read property xxx of undefined尝试读取undefined的属性接口返回的数据不是预期结构SyntaxError: Unexpected token语法错误括号不匹配、字符串引号未闭合Assignment to constant variable给const变量重新赋值了用const声明后又写了赋值NaN而不是报错某个值不是有效数字把非数字字符串当数字用最后一行NaN比较特殊它不报错但结果不对。比如parseInt(abc)返回NaN你用NaN做任何数学运算结果还是NaN。这种错误特别隐蔽因为它不崩溃只是数据变得不正常。排查的时候你可以用Number.isNaN()判断一个值是不是NaN。6.4 快速记忆与自查清单在文章的最后部分我给你一套针对变量和字面量的自查清单你可以用来检验自己是否真的掌握了这些基础能在代码里一眼看出哪些是字面量、哪些是变量引用知道var、let、const三者的区别以及为什么新代码用let和const理解是赋值方向等号右边先计算再放入左边变量知道undefined和null的区别理解作用域的概念能解释“为什么块外访问不到块内的let变量”知道原始类型和引用类型在变量赋值时的区别能够用临时变量或解构完成两个变量的交换如果你大部分都能答上来说明基础打得很扎实。如果有些点还模糊不妨倒回去再看一遍对应章节或者打开控制台自己写几个小例子验证一下。7. 一些实操心得还有给新手的建议我最后说几句个人感受。很多人觉得JS变量和字面量太基础了不值得花太多心思觉得“会写let x 1就算会了”。但我见过太多人因为这些基础概念不牢在后面的作用域、闭包、this指向问题上苦不堪言。变量就是整个JavaScript大厦的地基地基没打好上面盖多少层都容易塌。我自己带新人时第一个礼拜不让他们写业务代码就让他们练基础题声明变量、交换变量、判断类型、读对象属性、改数组内容、把值在浏览器控制台里打印出来。目的不是让他们背语法而是让他们对“变量存的是什么”形成直觉。再分享一个我写代码的习惯。我一般能用const就不用let。因为const强制你思考“这个值真的需要变吗”。如果一个变量声明成const之后你从来没觉得需要改它说明这个变量设计得很合理。反过来如果一个项目里到处都是let而且还经常被重新赋值代码逻辑容易变得难以跟踪。当然循环计数器、临时中间值该用let还得用不要死板教条。另外养成命名清晰的习惯越早越好。变量名就是代码的注释。我之前重构过一个老项目里面全是a、b、c这种名字每次改需求都像在玩解谜游戏。后来我花了一整天把所有变量名改成有意义的英文单词整个项目的可维护性提升了好几个档次。最后想跟大家说的是编程这事没有那么多“天赋决定论”。变量、字面量这些基础概念都是靠一遍遍动手写、动手调练出来的。你看再多文章不如自己在控制台里敲一行代码看看输出结果再故意改错看看报错信息。每一次报错都是学习的好机会别害怕报错报错反而是JS在告诉你“这里有问题”帮你把思路理清楚。如果你在练习中遇到了什么奇怪的变量问题欢迎在评论里分享当时的报错内容和你最终怎么解决的——很多时候你踩过的坑恰好是别人正在遭遇的坑。