面试被问 const 是否不可变?这样回答才显功底
wptr33 2025-10-23 12:38 3 浏览
作为前端开发者,我在学习ES6特性时,总被const的"善变"搞得一头雾水——为什么用const声明的数组还能push元素?为什么基本类型赋值就会报错?直到翻遍MDN文档、对着内存图反复推敲,才终于理清其中的底层逻辑。今天就结合实际代码和学习笔记,和大家聊聊这个让新手又爱又恨的const。
一、为什么需要const?从JS的"成长烦恼"说起
时间回到2015年之前,那时候JS开发者只能用var声明变量。这种"上古语法"有两个让人头疼的问题:
- 全局污染:用var声明的变量会默认挂在window对象上。想象一下,你在全局作用域声明了一个name变量,结果引入的第三方库也声明了同名变量——这种"变量打架"的情况在大型项目中简直是灾难。
- 变量提升的歧义:var存在"变量提升"机制,比如这段代码:
console.log(a); // 输出undefined,而不是报错
var a = 10;
- 代码的实际执行顺序是先声明a再赋值,但阅读时很容易产生"变量未声明就使用"的误解,尤其在复杂逻辑中非常影响代码可读性。
ES6(2015年发布)的出现,正是为了解决这些问题。作为ES6的第一个新特性,const(常量声明)和let(块级变量声明)的加入,让JS终于具备了现代编程语言的特性——这也标志着JS从"网页脚本语言"向"企业级开发语言"的转型。
二、const的基础规则:先记住这三个关键点
学习const,首先要明确它的核心规则:
- 必须初始化:声明时必须赋值,否则会报错:
const age; // 报错:Missing initializer in const declaration
2.块级作用域:和let一样,const声明的变量只在所在的块级作用域({}内)有效。比如:
if (true) {
const name = '张三';
}
console.log(name); // 报错:name is not defined
- "不可变"的本质:这是const最容易误解的点——它限制的是变量绑定(即变量指向的内存地址不可变),而不是变量的值不可变。
三、简单类型vs复杂类型:const的"双标"行为
理解const的关键,是搞清楚JS中简单数据类型和复杂数据类型的内存存储机制。
1. 简单数据类型:值不可变
JS的简单数据类型包括:Number、String、Boolean、Undefined、Null、Symbol(ES6新增)、BigInt(ES2020新增)。它们的特点是:直接存储在内存栈中。
内存栈的空间小但读取快,像酒店的"小格子储物柜",每个变量对应一个独立的格子。用const声明简单类型时,相当于给这个"格子"上了锁:
const age = 18;
age = 19; // 报错:Assignment to constant variable
因为age指向的栈内存地址被锁定,无法修改存储的值。
2. 复杂数据类型:地址不可变,内容可变
复杂数据类型(如Object、Array、Function)的存储方式不同:变量在栈中存储堆内存地址,实际数据存放在内存堆中。
内存堆像酒店的"大仓库",空间大但需要通过"地址牌"(栈中的引用)访问。const对复杂类型的限制是:栈中的地址牌不能换,但仓库里的东西可以改。
看一段实际代码(来自我的学习示例):
<script>
const friends = [
{ name: 'hh', home: '江西' },
{ name: 'xx', home: '河南' }
];
// 可以向数组中添加元素(修改堆内存中的内容)
friends.push({ name: 'zz', home: '湖南' });
// 可以修改对象属性(同样是修改堆内存)
friends[0].name = 'HH';
// 但不能重新赋值(更换栈中的地址牌)
friends = ['新数组']; // 报错:Assignment to constant variable
</script>
这段代码完美展示了const对复杂类型的特性:数组的堆内存地址被锁定,但堆内存中的具体内容(数组元素、对象属性)可以自由修改。
3. 为什么会有这种差异?
根本原因在于内存管理效率:
- 简单类型体积小(通常8字节以内),直接存栈中能快速访问;
- 复杂类型体积大(可能包含成百上千个属性),存堆中可以灵活扩展空间,栈中只存地址能节省空间。
const通过"锁定栈内存"的方式,既保证了简单类型的常量特性,又允许复杂类型在合理范围内修改内容——这种设计完美平衡了"数据安全"和"开发灵活性"。
四、从一段代码看const的"兄弟"let:块级作用域的重要性
提到const就不能不提let,它们都是ES6块级作用域的"践行者"。看一个经典的循环示例(来自我的测试代码):
<script>
// 使用let声明循环变量
for (let i = 0; i < 10; i++) {
setTimeout(function () {
console.log(i); // 依次输出0-9
}, 1000);
}
</script>
如果把let换成var,结果会变成输出10次10——因为var没有块级作用域,循环中的i共享同一个全局变量。而let为每次循环创建独立的块级作用域,setTimeout中的回调能正确捕获当前循环的i值
<script>
//闭包也一样解决问题
for(var i=0;i<10;i++){
(function(i){
setTimeout(function(){
console.log(i)
},1000)
})(i)
}
</script>
闭包能够解决这个问题的原因在于它能够捕获并保留外部函数的变量状态,即使外部函数已经执行完毕。具体来说:
- 变量作用域
在 for 循环中使用 var 声明的变量 i 是函数作用域的,而不是块级作用域。因此,i 的值在循环结束后会变成 10。 - 闭包的作用
通过立即执行函数(IIFE)创建一个闭包,每次循环时都会创建一个新的函数作用域,并将当前的 i 值传递给这个作用域。这样,setTimeout 回调函数中捕获的 i 值就是闭包创建时的值,而不是循环结束后的值。 - 代码执行过程
- 每次循环时,立即执行函数会捕获当前的 i 值。
- setTimeout 回调函数在 1 秒后执行时,会使用闭包中捕获的 i 值,而不是全局的 i 值。
因此,闭包通过保留每次循环时的 i 值,解决了 var 变量作用域带来的问题,确保 setTimeout 回调函数能够正确输出预期的值。
这种特性对大型应用至关重要:在React组件、Vue的v-for指令中,块级作用域能避免变量污染,让代码更可控。
五、常见误区:const的"不可变"是绝对的吗?
新手最容易犯的错误,是认为const声明的对象"完全不可变"。实际上:
- 对于数组,可以push/pop/splice,但不能重新赋值为[];
- 对于对象,可以修改属性值(obj.key = newVal),但不能重新赋值为{};
- 对于函数,可以修改原型方法,但不能重新赋值为function() {}。
如果需要彻底禁止修改对象内容,可以使用ES5的Object.freeze(): 冻结对象,使其不可修改。最高级别不可变性,禁止对象本身及任意直接属性的修改(但不影响它引用的其他对象)
const obj = Object.freeze({ name: '张三' });
obj.name = '李四'; // 赋值无效(非严格模式下静默失败,严格模式报错)
但要注意,Object.freeze()只能冻结对象的第一层属性,嵌套对象仍可修改——这属于更高级的"深度冻结"范畴了。
ps:“深度冻结”:在这个对象调用Object.freeze(),然后遍历它引用的所有对象并在这些对象上调用Object.freeze()。一定要小心无意冻结其他共享对象!
六、总结:const的"进化"背后是JS的成长
从var到const,看似只是一个关键字的变化,背后却是JS从"玩具语言"到"企业级语言"的蜕变:
- 开发者友好:块级作用域、常量声明让代码更易读、更安全;
- 内存管理优化:通过栈/堆分离设计,平衡了性能与灵活性;
- 生态扩展:ES6之后,TS、React、Vue等工具链的崛起,让JS能驾驭更复杂的业务场景。
下次再遇到const的"善变"行为,不妨打开浏览器的开发者工具,在Memory面板里观察内存地址的变化——你会更深刻地理解:所谓"不可变",不过是JS在内存世界里玩的一场"地址保卫战"。
相关推荐
- oracle数据导入导出_oracle数据导入导出工具
-
关于oracle的数据导入导出,这个功能的使用场景,一般是换服务环境,把原先的oracle数据导入到另外一台oracle数据库,或者导出备份使用。只不过oracle的导入导出命令不好记忆,稍稍有点复杂...
- 继续学习Python中的while true/break语句
-
上次讲到if语句的用法,大家在微信公众号问了小编很多问题,那么小编在这几种解决一下,1.else和elif是子模块,不能单独使用2.一个if语句中可以包括很多个elif语句,但结尾只能有一个...
- python continue和break的区别_python中break语句和continue语句的区别
-
python中循环语句经常会使用continue和break,那么这2者的区别是?continue是跳出本次循环,进行下一次循环;break是跳出整个循环;例如:...
- 简单学Python——关键字6——break和continue
-
Python退出循环,有break语句和continue语句两种实现方式。break语句和continue语句的区别:break语句作用是终止循环。continue语句作用是跳出本轮循环,继续下一次循...
- 2-1,0基础学Python之 break退出循环、 continue继续循环 多重循
-
用for循环或者while循环时,如果要在循环体内直接退出循环,可以使用break语句。比如计算1至100的整数和,我们用while来实现:sum=0x=1whileTrue...
- Python 中 break 和 continue 傻傻分不清
-
大家好啊,我是大田。...
- python中的流程控制语句:continue、break 和 return使用方法
-
Python中,continue、break和return是控制流程的关键语句,用于在循环或函数中提前退出或跳过某些操作。它们的用途和区别如下:1.continue(跳过当前循环的剩余部分,进...
- L017:continue和break - 教程文案
-
continue和break在Python中,continue和break是用于控制循环(如for和while)执行流程的关键字,它们的作用如下:1.continue:跳过当前迭代,...
- 作为前端开发者,你都经历过怎样的面试?
-
已经裸辞1个月了,最近开始投简历找工作,遇到各种各样的面试,今天分享一下。其实在职的时候也做过面试官,面试官时,感觉自己问的问题很难区分候选人的能力,最好的办法就是看看候选人的github上的代码仓库...
- 面试被问 const 是否不可变?这样回答才显功底
-
作为前端开发者,我在学习ES6特性时,总被const的"善变"搞得一头雾水——为什么用const声明的数组还能push元素?为什么基本类型赋值就会报错?直到翻遍MDN文档、对着内存图反...
- 2023金九银十必看前端面试题!2w字精品!
-
导文2023金九银十必看前端面试题!金九银十黄金期来了想要跳槽的小伙伴快来看啊CSS1.请解释CSS的盒模型是什么,并描述其组成部分。...
- 前端面试总结_前端面试题整理
-
记得当时大二的时候,看到实验室的学长学姐忙于各种春招,有些收获了大厂offer,有些还在苦苦面试,其实那时候的心里还蛮忐忑的,不知道自己大三的时候会是什么样的一个水平,所以从19年的寒假放完,大二下学...
- 由浅入深,66条JavaScript面试知识点(七)
-
作者:JakeZhang转发链接:https://juejin.im/post/5ef8377f6fb9a07e693a6061目录...
- 2024前端面试真题之—VUE篇_前端面试题vue2020及答案
-
添加图片注释,不超过140字(可选)...
- 今年最常见的前端面试题,你会做几道?
-
在面试或招聘前端开发人员时,期望、现实和需求之间总是存在着巨大差距。面试其实是一个交流想法的地方,挑战人们的思考方式,并客观地分析给定的问题。可以通过面试了解人们如何做出决策,了解一个人对技术和解决问...
- 一周热门
- 最近发表
-
- oracle数据导入导出_oracle数据导入导出工具
- 继续学习Python中的while true/break语句
- python continue和break的区别_python中break语句和continue语句的区别
- 简单学Python——关键字6——break和continue
- 2-1,0基础学Python之 break退出循环、 continue继续循环 多重循
- Python 中 break 和 continue 傻傻分不清
- python中的流程控制语句:continue、break 和 return使用方法
- L017:continue和break - 教程文案
- 作为前端开发者,你都经历过怎样的面试?
- 面试被问 const 是否不可变?这样回答才显功底
- 标签列表
-
- git pull (33)
- git fetch (35)
- mysql insert (35)
- mysql distinct (37)
- concat_ws (36)
- java continue (36)
- jenkins官网 (37)
- mysql 子查询 (37)
- python元组 (33)
- mybatis 分页 (35)
- vba split (37)
- redis watch (34)
- python list sort (37)
- nvarchar2 (34)
- mysql not null (36)
- hmset (35)
- python telnet (35)
- python readlines() 方法 (36)
- munmap (35)
- docker network create (35)
- redis 集合 (37)
- python sftp (37)
- setpriority (34)
- c语言 switch (34)
- git commit (34)