想象你家的厨房还是三十年前的老样子:所有调料敞口放在灶台上,用得久了互相串味;写菜谱要剪纸条、抹胶水,想改个菜名得撕掉重写;蒸馒头每次用手工捏形状,熟手和生手做出来的大小不一。这就是用旧版 JavaScript 写代码的日常——变量到处都是还容易互相覆盖,拼接字符串要拿加号把一段段文字粘起来,写个简单函数得敲一长串 function 和 return。 ES6+ 正是给这间“老厨房”做的一次现代化改造:密封调料罐(let/const)让每种调料各归各位;磁吸标签菜谱(模板字符串)随手一贴就能改;智能厨具(箭头函数)一键完成操作;冰箱分类抽屉(解构赋值)按标签取料;整袋食材直接下锅(扩展运算符);标准模具做点心(class 语法)。最后还有一台“老灶台翻译器”(Babel),把新菜谱翻译成老灶台听得懂的语言,让所有浏览器都能做出新菜。 这篇文章带你参观这间改造后的“新厨房”,每个核心语法都讲清楚用法、对比旧写法、说明适用场景,读完你就能在实际项目中直接上手。

岗位关联

无论你是前端入门、想从旧写法升级,还是面试需要扎实的 ES6 基础,掌握这些现代语法都能让你写出更简洁、更可靠的代码。在 AI 时代的前端工程、可视化大屏和 Node.js 服务开发中,这套基本功更是绕不开的基石。

目录
  1. 可视化演示
  2. 1. let 与 const——密封调料罐,各归各位
  3. 2. 箭头函数——一键智能厨具
  4. 3. 解构赋值——冰箱抽屉按标签取料
  5. 4. 模板字符串——磁吸菜谱随手贴
  6. 5. 扩展运算符——整袋食材倒进锅
  7. 6. class 类语法——标准模具做点心
  8. 7. 兼容与转译——老灶台翻译器
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

动态展示选择排序每轮查找最小值并交换到前方的过程,直观呈现分治策略与原地排序的特性。

本节配有交互动效(进入视口后自动加载,约 3KB)。

选择排序 - 第1轮 扫描未排序部分查找最小值 将最小值交换到已排序末尾 min=a[k] 每轮选最小元素

1. let 与 const——密封调料罐,各归各位

老厨房里,所有调料都敞口放在灶台上,辣椒粉的气味能飘到白糖罐里,盐罐和糖罐经常傻傻分不清楚。这就是 var 的处境——它只有函数作用域和全局作用域,没有“块”的概念。在 if 或 for 的代码块里用 var 声明的变量,代码块结束后依然在外面“飘着”,就像辣椒粉的味道传遍整间厨房,你根本管不住它。

ES6 给出的解决方案是 let 和 const 两个“密封调料罐”。它们把变量的可见范围锁定在所在的花括号 {} 内部,这个范围在 JavaScript 里叫“块级作用域”。一旦代码块结束,变量就像密封罐的盖子合上,味道一丝都不会漏出去。let 是“可换罐”——还能倒掉重装新的调料;const 是“终身锁罐”——一旦装好调料,再也不能灌入其他东西,但罐子里的食材本身(比如对象里某个属性)还是可以取用修改的。

块级作用域可以用集合的语言来理解:一个块内声明的变量属于这个块的集合,外部集合不包含它。公式如下。

把 var 换成 let 或 const,就像给厨房所有调料配上带盖密封罐。代码块之间互不串味,程序的每个部分都可预测、不互相污染。老厨房里最让人头疼的“飘味问题”,从此被一劳永逸地解决。

S块={v∣v 在 {} 中声明}且v∉S外S_{块} = \{ v \mid v \text{ 在 } \{ \} \text{ 中声明} \} \quad \text{且} \quad v \notin S_{外}
这个公式想说:代码块内的变量只在块内“登记在册”,块外查无此人。就像密封罐里的调料,香味和位置都被盖子锁住了。
  1. 块内集合花括号代码块里声明的所有变量,构成一个“块内集合”,这就是密封罐的罐体空间。
    S块={v∣v 在 {} 中声明}S_{块} = \{ v \mid v \text{ 在 } \{ \} \text{ 中声明} \}
  2. 外部排除块内变量不属于外部集合,块外代码访问它会报错或得到 undefined,就像调料味被封在罐子里。
    v∉S外v \notin S_{外}
  3. 暂时性死区let 声明之前的区域是“禁区”,提前访问会直接抛出 ReferenceError——罐子还没放到架子上,你伸手去够,自然会扑空。
    TDZ:声明前访问 v⇒ReferenceError\text{TDZ}: \text{声明前访问 } v \Rightarrow \text{ReferenceError}

2. 箭头函数——一键智能厨具

老式灶台做饭要好几步:先开煤气阀门,再拧打火旋钮,然后手动调火候,最后还得自己盯着看菜熟没熟。旧版 JavaScript 写函数也一样繁琐——要写 function 关键字,要有 return 语句,一个简单的“输入乘二”要占好几行。ES6 的箭头函数则像一台智能厨具:按下按键(=>),食材进去,成品自动出来,中间过程全部省略。

箭头函数的核心是“映射”思想:给它一个输入,它直接返回一个输出。(x) => x * 2 就是把 x 映射到 x 的两倍,读作“x 映射到 2 倍 x”。当只有一个参数时,连括号都可以省略;当函数体只有一个表达式时,连 return 都可以省略——这个特性叫“隐式返回”,厨具默认就把唯一做好的菜端出来,不用你额外说“端菜”。

箭头函数还有三个重要的“脾气”需要记住。第一,它的 this 是“词法绑定”,在写代码的那一刻就固定继承外层作用域的 this,不会像普通函数那样由调用者决定,相当于智能厨具按出厂设置工作,不受操作者干扰。第二,它不能当作构造函数使用,没有自己的 prototype,就像一键式厨具没有手动模式的零件。第三,它没有自己的 arguments 对象,需要用 ...args 这样的 rest 参数来收集所有传入实参。

从数学角度看,箭头函数的表达更接近函数定义本身,公式如下。

在项目中善用箭头函数,你会发现回调函数、数组遍历、事件处理的代码量肉眼可见地缩水。就像一键智能厨具一样,把做饭流程压缩成一个按键的动作,省时又省心。

f:x↦x×2⟺f(x)=x×2f: x \mapsto x \times 2 \quad \Longleftrightarrow \quad f(x) = x \times 2
箭头符号 => 和数学上的映射箭头 ↦ 气质相同:左边放输入,右边给输出。这种直白的映射表达让代码读起来像数学公式一样清晰。
  1. 映射符号x ↦ 2x 表示输入 x 输出 2x,这就是箭头函数最本质的行为——处理一个输入,给出一个规律性的输出。
    f:x↦x×2f: x \mapsto x \times 2
  2. 等价定义它和普通函数 function f(x) { return x * 2; } 完全等价,只是写得更短、视觉负担更小。
    f(x)=x×2f(x) = x \times 2
  3. this 的定格箭头函数的 this 在定义时就被“拍照定格”,等于外层作用域的 this,不再随调用者动态变化。
    this箭头=this外层定义时\text{this}_{\text{箭头}} = \text{this}_{\text{外层定义时}}

3. 解构赋值——冰箱抽屉按标签取料

老冰箱的食材全部堆在一个大格子里,想找根葱得翻半天。每次从对象里取值,都要写 obj.name、obj.age,一行一行地取,就像在杂物堆里反复扒拉。ES6 的“解构赋值”则像一台带分类抽屉的新冰箱:每个抽屉贴着标签,你要什么,直接按标签拉开,一次就能把想要的食材全部拿齐。

解构赋值分为两种常见形态。数组解构按“位置”取值:const [a, b] = [1, 2] 会让 a 得到 1、b 得到 2,就像在抽屉里按顺序拿第一件、第二件食材。对象解构按“名字”取值:const {name, age} = obj 会把 obj.name 和 obj.age 自动填进同名变量,就像对着标签 "name"、"age" 直接把对应的抽屉抽出来,不需要关心抽屉在第几层。

解构赋值还支持两个贴心功能:默认值和嵌套。取不到值的时候可以用等号设置兜底,比如 const {age = 18} = obj,如果 obj 里没有 age,就用 18 顶上——相当于抽屉空着时自动放一份备用食材。嵌套解构则像拉开冰箱门后再拉里面的小抽屉:const {info: {city}} = obj 可以一步拿到嵌套深处的数据,省去逐层访问的冗长代码。

解构最有名的一个妙用是交换两个变量的值,看这个优雅的公式。

解构赋值最能体现“按需取料”的优雅。它让代码少了好几行冗余赋值,让数据从“哪来的、怎么取的”变得一看就懂,就像打开冰箱扫一眼标签就能拿到全部食材。

[a,b]=[b,a]⇒a′=b, b′=a[a, b] = [b, a] \quad \Rightarrow \quad a' = b, \ b' = a
传统的交换要引入第三个临时变量 temp,就像拿一个空盘子来倒腾两个碗里的菜。解构赋值则像把两个抽屉直接互换位置,一次到位、零中间变量。
  1. 左侧模式等号左边的 [a, b] 是“模式”,它声明两个变量,并按位置顺序对应右侧元素,相当于告诉冰箱“我要一格和二格的食材”。
    [a,b][a, b]
  2. 右侧来源等号右边的 [b, a] 作为数据源,把它的第 0 位元素给 a、第 1 位元素给 b,实现一次赋值。
    [b,a][b, a]
  3. 交换结果经过赋值,a 的新值是旧的 b,b 的新值是旧的 a。整个过程不需要声明任何临时变量。
    a′=b, b′=aa' = b, \ b' = a

4. 模板字符串——磁吸菜谱随手贴

老式厨房写菜谱,得剪一张张小纸条,用胶水贴在菜谱本上。想改一道菜名,得把旧纸条撕下来、重新剪、重新贴,麻烦不说,还容易把本子弄得黏糊糊。旧版 JavaScript 拼字符串正是这个体验:'你好,' + name + '!',加号就像胶水,片段一多,顺序一乱,整行代码就变成一锅粥。

模板字符串把菜谱本换成了磁性小黑板,用一对反引号 ` ` 围起来,中间想放变量或表达式的地方,用 ${}23你好,${name}! 读起来就是一个完整的句子,name 的位置一目了然。磁贴不仅能贴变量,还能贴任意表达式,比如 ${price * 0.8}` 可以直接在字符串里算打折后的价格。

模板字符串的三大优势让写字符串从“手工劳动”升级为“所见即所得”。第一,多行字符串原生支持——反引号内可以随意换行,不用再写 \n 和加号去拼好几行文字,就像在小黑板上长句、短句分行写都顺其自然。第二,支持嵌套模板——${} 里面还可以再写模板字符串,灵活组合出复杂结构,相当于磁贴上可以再叠磁贴。第三,标签模板——在反引号前加一个函数名,就能对模板内容做自定义加工,这是高级玩法,类似在小黑板外用一层滤镜控制最终呈现效果。

模板字符串和旧式拼接在结果上完全等价,但体验天差地别,用公式表达如下。

从此以后,生成 HTML 片段、拼接日志信息、组合接口提示语,都像在小黑板上工整地写字。胶水和纸条的时代落幕了。

\text{'你好,'} + name + \text{'!'} \ \Longleftrightarrow \ \text{\`你好,}\${name}\text{!\`}
这个等价式说明:两种写法输出相同,但模板字符串让变量的“嵌入位置”可视化了——磁贴贴在句子的哪个位置,一眼就能看到。
  1. 旧式拼接用加号把字符串片段和变量一块块粘起来,片段一多就分不清顺序,就像用胶水粘纸条容易粘错位置。
    ’你好,’+name+’!’\text{'你好,'} + name + \text{'!'}
  2. 插值磁贴${} 是磁吸位置,括号里的表达式会被求值后换成字符串嵌入,变量名直接出现在句子中。
    $name\${name}
  3. 等价关系左式与右式输出完全一致,但右式更直观、容错率更高,尤其适合多段落、多变量的动态文本生成。
    ⟺\Longleftrightarrow

5. 扩展运算符——整袋食材倒进锅

做汤的时候,传统做法是一颗颗把土豆、胡萝卜、洋葱夹进锅里,费时又容易漏掉。旧代码里要把数组的每个元素传给函数,得用 apply 或者写循环,笨重得很。ES6 的扩展运算符 ... 就像是把整袋食材直接倒进锅里——干干净净、一次到位、一个不漏。

扩展运算符做的事情是“展开”:把数组或可迭代对象拆成一个个独立的元素。最经典的场景有三个。数组合并:const newArr = [...arr1, ...arr2] 把两个数组的元素全部倒进一个新数组,比 concat 更直观。函数传参:Math.max(...nums) 把数组 nums 的元素铺平成一个个独立参数传入,不需要再绕道 apply。对象合并:const merged = {...obj1, ...obj2} 把两个对象的属性“倒进”一个新对象,后者同名属性会覆盖前者,非常适合做配置合并。

扩展运算符还有一个“反向操作”——rest 参数。当 ... 用在函数参数位置时,它做的事情不是展开而是收拢:把多个参数收集成一个数组。function sum(...nums) { ... } 可以接收任意数量的参数。想象你有一个“收纳袋”,把散落在桌上的食材全部收进袋子里,倒出来是散的(展开),装进去是一袋的(收拢)。

从数学角度看,展开就是把“集合的集合”摊平成直接的元素列表,公式模拟如下。

处理接口返回的数组、合并用户配置、封装不定参数的函数,扩展运算符都是那把“万能倒料勺”——倒下去干净利落,收起来整整齐齐。

max⁡(…[3,7,5])≡max⁡(3,7,5)\max(\ldots[3, 7, 5]) \equiv \max(3, 7, 5)
方括号里的数组被 ... 解开了外壳,元素一个个排列出来,效果等同于直接写出所有参数。这就是“整袋倒进锅”的数学面貌。
  1. 展开动作...[3, 7, 5] 把数组的方括号“脱掉”,只留下里面的元素序列 3、7、5,就像倒出整袋食材。
    …[3,7,5]\ldots[3, 7, 5]
  2. 等价调用展开后传入 Math.max 与直接写三个参数完全等价,函数收到的是三个独立参数而非一个数组。
    max⁡(3,7,5)\max(3, 7, 5)
  3. rest 收拢在参数位置,...args 把调用者传入的多个参数收进一个数组,公式上等价于构建一个列表。
    f(…args)⇔arr=[args1,args2,…]f(\ldots args) \Leftrightarrow arr = [args_1, args_2, \ldots]

6. class 类语法——标准模具做点心

在没有模具的时代,做点心全靠手工捏:先揉面,再塑形,每个厨师捏出来的形状都不一样,还得记住“这个点心要继承那个点心的大小和花纹”这类复杂配方。旧版 JavaScript 用构造函数加原型链模拟面向对象,写法绕来绕去,小白容易看晕。ES6 的 class 语法像引入了一套标准模具——形状、花纹、尺寸一次定制,之后每个点心都按模具批量生产,整齐统一。

class 本质上是一层“语法糖”,底层依然是构造函数加原型链,但表达方式直白了很多。用 class 定义一个类,里面有 constructor 构造函数,在 new 的时候自动执行,负责初始化实例的属性。方法直接写在类体里,自动挂到原型上,所有实例共享,不会每个实例都复制一份。

类之间还能用 extends 建立继承关系,比如 class Dog extends Animal,Dog 自动拥有 Animal 的所有方法。子类的构造函数中必须先调用 super() 才能使用 this,这是为了保证父类的初始化先完成——就像用模具做点心时,必须先放好底模,才能压出上层花纹,顺序错了整个形状就乱了。super 关键字还可以用来调用父类的方法,比如在子类里 super.makeSound() 先让父类发声再做自己的事。

class 继承在底层等价于一次原型链的连接,用公式表示如下。

用 class 组织代码,就像厨房引进了一套标准模具:动物类、机器人类、菜单类……每个类都规规矩矩,继承关系一目了然,团队协作时默契度直线上升。

class B extends A≡B.prototype=Object.create(A.prototype)\text{class } B \text{ extends } A \quad \equiv \quad B.\text{prototype} = \text{Object.create}(A.\text{prototype})
这个等价式说明:表面的 extends 关键字,底层做的是把子类的原型对象“挂在”父类原型之上,形成一条原型链,就像模具嵌套模具,子模具自动带出父模具的纹路。
  1. 继承声明class B extends A 声明 B 继承 A,B 的实例可以直接使用 A 中定义的方法,相当于模具 B 自动复制了模具 A 的所有花纹。
    class B extends A\text{class } B \text{ extends } A
  2. 原型连接底层实现是把 B 的 prototype 指向一个“以 A 的 prototype 为原型”的新对象,形成原型链——沿着链条向上找,总能找到父类的方法。
    B.prototype=Object.create(A.prototype)B.\text{prototype} = \text{Object.create}(A.\text{prototype})
  3. super 先行子类构造函数中先调用 super() 再使用 this,等价于先完成父类模具的压模,再做子类的个性化加工。
    constructor(){ super(); … }\text{constructor}() \{ \ \text{super}(); \ \ldots \ \}

7. 兼容与转译——老灶台翻译器

厨房改造完毕后,菜谱都换成新式写法,可家里的老灶台听不懂新名词,怎么办?老一辈的办法是另外架一台“翻译器”:把新菜谱逐句翻译成老灶台能执行的旧指令。前端世界里,这台翻译器叫 Babel。它把 ES6+ 的语法翻译成 ES5——箭头函数变成普通 function,let 变成 var 并模拟块级作用域,class 变成构造函数加原型链。老浏览器读完翻译后的代码,照样能跑。

不过语法能翻译,能力没法凭空翻译。新语法还配套了一批“新厨具”——Promise、Map、Array.from 这些 API。翻译器可以把“做菜步骤”翻译成旧语言,但老环境里根本没有这些厨具本身。这时就需要另一个角色出场:polyfill(补丁),它像在旧灶台上加装一个模拟配件,让老环境也能拥有新 API 的行为,比如给不支持 Promise 的浏览器注入一份简易实现。

实际工程中,你不需要手动折腾这些。Webpack、Vite 等构建工具在生产环境打包时,会自动调用 Babel 并引入需要的 polyfill,你只管写新语法,工具链负责“翻译”。浏览器兼容矩阵也由配置统一管理——就像厨房改造后请了专业的监理团队,新菜谱写好,交给他们去适配所有老灶台。

从源码到可运行代码,整个链路可以抽象成一次“翻译加打补丁”的变换,公式如下。

有了翻译器和补丁,你完全不用为了兼容而退回旧写法。新语法带来的开发效率与代码质量,老浏览器照样能“尝到”——这就是现代化厨房惠及每一代灶台的意义。

ES6 源码 →Babel 转译 ES5 等价代码 + polyfill 补丁\text{ES6 源码} \ \xrightarrow{\text{Babel 转译}} \ \text{ES5 等价代码} \ + \ \text{polyfill 补丁}
前半段是语法翻译,后半段是能力补丁。翻译解决“看不懂”的问题,补丁解决“做不到”的问题,两者配合才让老环境完整兼容新代码。
  1. 源码输入ES6 源码是新菜谱,语法简洁、结构清晰,但老浏览器不识别。
    ES6 源码\text{ES6 源码}
  2. 转译动作Babel 将新语法翻译成 ES5 等价写法,箭头函数、模板字符串、class 等都被逐句改写,相当于翻译器逐行转换。
    →Babel\xrightarrow{\text{Babel}}
  3. 补丁注入语法之外的新 API 由 polyfill 注入运行时,老环境得到模拟能力,最终产出可在旧浏览器执行的完整代码。
    ES5 代码+polyfill\text{ES5 代码} + \text{polyfill}

AI 应用场景

场景一:AI 可视化大屏的前端重构

阶段:前端开发 / 数据可视化工程

解决的问题:AI 模型的预测结果需要在前端动态展示,传统 ES5 代码变量混乱、回调嵌套深、手动拼接 HTML 极易出错。使用 ES6+ 重构后,用解构赋值读取模型返回的数据字段,用箭头函数简化渲染回调,用模板字符串生成图例与弹窗,代码量减少约四成,逻辑清晰可维护。

具体案例:某团队用 ECharts 展示推荐算法实时曲线,原代码有 300 多行面条式拼接。重构后用 const { labels, values } = apiResult 一步取数,渲染函数写成箭头函数,图例用模板字符串动态生成,整段逻辑从 300 行精简到 180 行,Bug 率明显下降。

场景二:Node.js 中搭建 AI 模型服务的 API 层

阶段:后端服务开发

解决的问题:模型推理服务要处理大量请求参数,默认参数与用户参数合并、不定长输入收集是高频需求。传统写法用循环遍历和临时变量,繁琐还容易漏参数。用扩展运算符合并参数对象,用 rest 参数收集不定长的请求列表,用 class 封装模型加载与推理流程,接口层既简洁又健壮。

具体案例:一个推荐模型服务用 class ModelLoader 封装权重加载、预处理、推理三个环节;请求处理函数用 const params = { ...defaults, ...req.query } 快速合并参数;批量预测接口用 async predict(...ids) 接收任意数量的 ID,一行代码完成参数收拢。

场景三:AI 标注工具的前端交互优化

阶段:数据标注工具开发

解决的问题:标注面板需要频繁操作 DOM 状态,大量状态变量互相污染导致标注框错位。改用 let/const 严格约束变量作用域后,状态冲突大幅减少;用 class 封装不同标注类型的面板组件,用解构赋值读取标注坐标数据,交互逻辑清晰可复用。

具体案例:某数据公司的图片标注工具,用 const 定义所有配置常量,标注坐标用 const { x, y, w, h } = currentBox 一步取出,矩形框、多边形、关键点三种标注模式各封装为一个 class,切换模式只需 new RectPanel() / new KeypointPanel(),项目可维护性显著提升。

Python 演示

python53 行
# -*- coding: utf-8 -*-# ES6 Features Detector - 检测JS代码中的ES6语法使用情况# 直接运行即可看到内置示例的检测报告, 也可把真实JS代码传给 detect_es6() import re # 特征名称与正则匹配规则FEATURES = {    "let": r"\blet\s+[A-Za-z_$]",    "const": r"\bconst\s+[A-Za-z_$]",    "arrow": r"=>",    "template": r"`",    "destructure": r"(?:\{\s*[A-Za-z_,\s]+\}\s*=|\[\s*[A-Za-z_,\s]*\]\s*=)",    "spread": r"\.\.\.",    "class": r"\bclass\s+[A-Za-z_$]",    "import": r"\bimport\s+[A-Za-z_{}\$]",}  def detect_es6(code):    """统计各类ES6特性出现的次数, 返回字典"""    return {        name: len(re.findall(pattern, code))        for name, pattern in FEATURES.items()    }  def format_report(result):    """把统计结果格式化成可读报告"""    lines = ["ES6 Feature Report", "=" * 24]    total = 0    for name in sorted(result, key=lambda k: -result[k]):        count = result[name]        if count:            lines.append(f"- {name:10s}: {count}")            total += count    lines.append("=" * 24)    lines.append(f"Total ES6 usage: {total}")    return "\n".join(lines)  if __name__ == "__main__":    demo_js = '''    const api = "/ai/predict";    let score = 0;    const getInfo = (name) => `hello ${name}`;    const [a, b] = [1, 2];    const nums = [1, 2, 3];    const max = Math.max(...nums);    class Model { constructor() { this.ready = true; } }    import fs from "fs";    '''    print(format_report(detect_es6(demo_js)))
目的:这是一个“ES6 特征探测器”,扫描一段 JavaScript 代码文本,统计 let、const、箭头函数、模板字符串、解构赋值、扩展运算符、class、import 等 ES6 语法的出现次数,帮你快速了解一个项目或一段代码的现代化程度。
  1. FEATURES = {...}定义每个 ES6 特性的“传感器”(正则表达式),每种语法对应一种匹配规则。
  2. re.findall(pattern, code)在代码文本中扫描并统计该语法出现了多少次,相当于一个一个数出新厨具的数量。
  3. detect_es6(code)主检测函数,输入 JS 代码字符串,输出一个包含各类特性次数的字典。
  4. format_report(result)把统计结果整理成对齐的美观报告,按数量降序排列,最下面给出总量。
  5. demo_js内置一段含多种 ES6 特性的示例代码,你可以换成任意真实代码文件的内容直接使用。

常见误区

误区我之前一直以为用 const 声明变量后,这个变量就彻底锁死了,任何修改都会报错,所以连 const 声明的数组我都不敢 push。

正解const 锁的是“绑定”而不是“值”。它保证变量名不能再指向别的值(不能重新赋值),但变量指向的对象或数组内部是可以变化的。就像密封罐的盖子锁死了,你不能再往罐子里换装另一种调料,但罐子里的食材本身可以夹出来吃。const arr = [1] 后 arr.push(2) 完全合法,而 arr = [2] 才会报错。

为何常见:初学者看到“const 常量”的字面意思,加上很多教程强调“不可变”,就自然理解为整个值都不可变,忽略了 JavaScript 中“绑定”与“值”这两个层面的区分。

误区我一直以为箭头函数就是省了几个字符的 function,写法短一点,其他都一样,所以项目里顺手全换成箭头函数了。

正解箭头函数有三个关键区别:第一,this 在定义时词法绑定,不会随调用者改变;第二,它是匿名函数且没有 prototype,不能作为构造函数使用 new;第三,它没有自己的 arguments 对象。这些差异会导致行为完全不同。就像智能厨具和传统灶台虽然都能做菜,但工作逻辑完全不同——前者按出厂设置运行,后者取决于你用它的方式。

为何常见:从表面看箭头函数确实只是“瘦身版” function,不深入理解 this 绑定机制和原型链,很难察觉到行为层面的本质差异,直到在事件回调或对象方法中踩坑。

误区网上说 let 和 const 没有变量提升,我一直以为它们在声明之前使用会像访问不存在的变量一样返回 undefined。

正解事实上 let/const 同样存在提升,但被“暂时性死区”(TDZ)保护——声明之前的区域不可访问,一旦访问立即抛出 ReferenceError 而不是返回 undefined。这像罐子的标签已经挂上去了,但罐子还没放到架上,你去拿就扑个空且会报警。因此“提升”的不是“变量可用”,而是“变量已被登记但不可触碰”。

为何常见:许多教程为了简化和 var 对比,用“没有变量提升”来概括,导致很多人误以为 TDZ 区域访问会得到 undefined,实际报错与不报错是面试和调试中非常关键的差异。

误区我以为解构赋值就是 `const [a, b] = arr` 和 `const {x} = obj` 两种,其他地方都用不上。

正解解构赋值的用途远不止这两处:它可以用在函数参数上(直接解构传入对象);可以用在函数返回值上(一行接住多个返回值);可以用来交换变量;还可以用在 for 循环里(for (const {id} of list))。凡是“等号左边写模式、右边给数据源”的场景都适用。就像分类抽屉的取料逻辑,只要一侧提供食材、一侧声明标签,就能完成按需取用。

为何常见:教材通常先讲数组和对象两个基础形态,学习者容易形成“解构 = 那两种写法”的惯性思维,忽略了它是一种通用的赋值模式。

误区我总是以为只要把代码交给 Babel 转译成 ES5,就万事大吉了,什么新特性在老浏览器都能跑。

正解Babel 只能转译语法层面的内容(箭头函数、模板字符串、class 等),对于 API 层面的能力(Promise、Map、Array.from 这些新内置对象和方法),语法转译无法凭空创造,必须额外引入 polyfill 来补足。就像翻译器能把“开抽屉”翻译成旧语言,但旧灶台本身没有抽屉这个零件,你得额外装一个。正确做法是 Babel 负责语法转译,再按需引入 @babel/polyfill 或 core-js 补 API。

为何常见:人们容易把“转译”理解为“完全兼容”,忽略了语法与运行时 API 是两个不同层面,直到线上在 IE 中白屏才发现区分。

面试题

Q1. 以下哪种变量声明方式存在“暂时性死区”(TDZ)?

单选题 · easy

  • A.var
  • B.let← 正确答案
  • C.全局变量 x = 1
  • D.function 声明

答案:B。let 和 const 存在暂时性死区——声明之前的区域访问变量会抛出 ReferenceError。相当于密封罐的标签先挂上但罐子还没放上架,此时伸手去拿会扑空并报警。var 虽然提升但没有 TDZ,访问只会得到 undefined。

Q2. 箭头函数与普通 function 最大的区别是什么?

单选题 · easy

  • A.箭头函数写法更短
  • B.箭头函数不能有返回值
  • C.箭头函数的 this 继承外层作用域且不能当构造函数← 正确答案
  • D.箭头函数不能传参

答案:C。箭头函数的核心差异有两点:this 是词法绑定(定义时就确定,不随调用者改变),且没有 prototype 不能 new。写法短只是表面现象。就像智能厨具按出厂设置运行,不受操作者影响。

Q3. 以下哪些写法是合法的解构赋值?

多选题 · mid

  • A.const {a, b} = obj;← 正确答案
  • B.const [x, y] = arr;← 正确答案
  • C.const [a, b] = {a: 1, b: 2};
  • D.const {x = 10} = obj;← 正确答案

答案:ABD。A 是对象解构按名取值,B 是数组解构按位取值,D 是带默认值的对象解构,三者都合法。C 不合法——左侧是数组模式,右侧必须提供可迭代对象,普通对象没有迭代器,不能用于数组解构。就像你不能用“按顺序取”的标签去取一个没有顺序的冰箱抽屉。

Q4. 执行以下代码,最终控制台输出什么?

单选题 · mid

  • A.3 3 3
  • B.0 1 2← 正确答案
  • C.1 2 3
  • D.抛出 ReferenceError

答案:B。let 为每次循环创建独立的块级作用域,每个 setTimeout 回调捕获的都是当次迭代的 i,所以输出 0、1、2。如果用 var 声明,所有回调共享同一个变量,最终输出 3 3 3。这正体现了密封罐“各归各位”——每次循环都是独立的小罐子。

Q5. const 声明的数组,以下哪个操作是允许的?

单选题 · mid

  • A.const arr = [1]; arr = [2];
  • B.const arr = [1]; arr.push(2);← 正确答案
  • C.const arr; arr = [1];
  • D.const arr = null; arr = [1];

答案:B。const 锁的是变量名与值的绑定关系,不允许重新赋值,所以 A 和 D 都会报错;C 中 const 声明时必须初始化,不能先声明后赋值。B 的 push 是修改数组内部,属于允许的操作——就像密封罐盖子锁死了,但罐子里的食材仍然可以动。

Q6. 关于模板字符串,以下说法正确的是?

多选题 · hard

  • A.可以使用 ${} 嵌入变量或表达式← 正确答案
  • B.支持直接书写多行字符串← 正确答案
  • C.${} 内部可以嵌套使用模板字符串← 正确答案
  • D.模板字符串会自动执行内部包含的 HTML 脚本

答案:ABC。模板字符串支持插值、多行、嵌套模板,A/B/C 都正确。D 错误——模板字符串只是普通字符串,不会自动执行 HTML 或脚本,它是“磁吸小黑板”,只负责内容展示,不具备执行能力。

Q7. 扩展运算符 ... 的正确作用是什么?

单选题 · easy

  • A.删除数组中的元素
  • B.将数组或可迭代对象展开为多个独立元素← 正确答案
  • C.将两个字符串连接成一个
  • D.创建一个新的作用域

答案:B。扩展运算符的核心动作是“展开”——把数组、对象等可迭代结构拆成独立元素,可以用在数组合并、对象拷贝、函数传参等场景。它不负责删除、连接字符串或创建作用域。就像把整袋食材倒进锅里,而不是做其他事。

Q8. 关于 class 语法,以下说法正确的是?

多选题 · mid

  • A.class 是 ES6 提供的语法糖,底层仍是构造函数和原型链← 正确答案
  • B.class 声明会提升并自动初始化
  • C.子类构造函数中必须先调用 super() 才能使用 this← 正确答案
  • D.class 内部定义的方法默认挂载在原型对象上← 正确答案

答案:ACD。A 正确,class 的底层机制还是构造函数加原型链;C 正确,super() 先保证父类初始化;D 正确,类体中的方法自动放在 prototype 上供实例共享。B 错误——class 和 let/const 一样存在暂时性死区,声明前使用会报错,不会初始化。

Q9. 非严格模式下,以下代码中箭头函数内的 this 指向什么?

单选题 · hard

  • A.obj 对象
  • B.undefined
  • C.window← 正确答案
  • D.抛出 TypeError

答案:C。箭头函数的 this 在定义时就由外层作用域决定,这里定义时外层是全局作用域,因此 this 指向 window(非严格模式下)。调用者 obj 不会影响箭头函数的 this。这就像智能厨具的出厂设置不因操作者改变。若想得到 obj.name,必须使用普通函数 getName() { return this.name; }。

Q10. 要让 ES6+ 代码兼容老浏览器,工程上最通用的做法是什么?

单选题 · mid

  • A.让用户手动升级浏览器
  • B.用 Babel 转译为 ES5,并按需引入 polyfill← 正确答案
  • C.用 ESLint 把所有新语法标记为错误
  • D.用 TypeScript 直接编译为机器码

答案:B。Babel 负责把 ES6 语法翻译成 ES5 等价代码,polyfill 补全新 API 的能力,是行业标准方案。浏览器升级不受开发者控制;ESLint 只检查不转换;TypeScript 编译目标是 JavaScript 而非机器码。这就像用翻译器加补丁让老灶台也能做新菜。