尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

ES6新特性全面解析:let/const、深拷贝、Promise与工程化实践

发布时间:2026/9/26 20:13:17

资讯中心
01
ARTICLE

ES6新特性全面解析:let/const、深拷贝、Promise与工程化实践

ES6新特性全面解析:let/const、深拷贝、Promise与工程化实践
在ES6出来的那几年前端圈子讨论最多的就是“这门新语法到底改了什么”。说实话当时很多老项目还在用var满屏飞、回调地狱层层嵌套大家对新语法既期待又担心。后来ES6全面落地所有现代浏览器和Node.js都原生支持它也成了面试必考、项目必用、源码必读的核心基础。这篇笔记不是API手册的搬运工而是一个实战多年的人回过头来把ES6里最该吃透的知识点、最容易踩的坑、以及深拷贝、Map这类高频使用场景做个彻底梳理希望给正在学ES6的朋友一条可以直接跟着走的路。无论你是刚入行的新人还是写了几年var想系统升级的老手这篇内容都值得你花点时间完整过一遍。1. 先聊聊ES6到底解决了什么问题很多初学者学ES6的时候只记住了“新写法”却不理解“旧写法为什么不够用”。这不怪大家因为网上大量教程都是直接丢语法不讲背后的动机。但学一门新规范如果不知道它要解决什么痛点就很容易出现“学了用不上、用了不知道自己为什么要这样写”的尴尬。1.1 为什么说ES6是JavaScript的分水岭ES6正式名称为ECMAScript 2015是2015年发布的JavaScript语言标准。在它之前JavaScript已经发展了近二十年但语言本身的很多“历史包袱”一直没动过变量声明只有var一个方式词法作用域和块级作用域纠缠不清函数内部this指向飘忽不定回调函数是异步编程的唯一姿势模块化只能靠社区约定或者CommonJS。ES6一次性引入了大量新语法、新内置对象和新的编程范式包括但不限于let/const、解构赋值、模板字符串、箭头函数、Map/Set、Promise、class、import/export等等。这套语法体系的引入让JavaScript从“能用但别扭”的语言变成了“工程化友好”的语言。过去写一个简单的异步流程要靠嵌套回调现在有了Promise和async/await过去复制一个对象要用手动遍历现在有了Object.assign和展开运算符过去模块化依赖各自团队的约定现在有标准模块语法。可以这么说ES6是JavaScript从脚本语言走向工程化语言的真正转折点。1.2 我对ES6学习路线的建议我的建议是不要按照API顺序一个个背而是按业务场景去学先掌握变量声明与块级作用域解决“变量搞混”的痛点然后学解构和模板字符串解决“取数据、拼字符串太啰嗦”的痛点接着学箭头函数和参数增强解决“函数写法复杂”的痛点再学数组/对象新方法和Map/Set解决“数据操作不顺手”的痛点之后是深拷贝解决“引用类型复制错误”的痛点最后攻克异步编程和模块化。按照这个顺序走下来你不仅能记住语法还会知道每个语法解决什么问题用起来才会自然。2. 变量声明与作用域let/const才是真正的“块级思维”2.1 let 与 var 的核心区别先说一个非常经典的例子console.log(a); // undefined var a 1;var声明的变量会发生变量提升也就是JavaScript引擎在代码执行前会把var声明“提升”到作用域顶部所以上面这段代码不会报错只是打印undefined。这种效果容易造成“先使用后声明”的错觉实际项目中非常容易埋雷。换成let之后同样的写法console.log(b); // ReferenceError: Cannot access b before initialization let b 1;这里报错的原因不是let没有提升而是let存在“暂时性死区”Temporal Dead Zone简称TDZ。简单理解就是从代码块开始到声明语句执行之前的这段范围内该变量处于“已绑定但不可访问”的状态。这个设计的价值在于它强制要求你在使用变量之前先完成声明从语言层面杜绝了“悬空变量”的使用。另外一个核心区别是作用域。var是函数级作用域let是块级作用域。块级作用域简单说就是{}花括号包裹的范围包括if、for、while等语句块。最常见的经典面试题也与此相关for循环里用var声明索引和用let声明索引的区别。for (var i 0; i 3; i) { setTimeout(() console.log(i)); // 3 3 3 } for (let j 0; j 3; j) { setTimeout(() console.log(j)); // 0 1 2 }var的情况下循环结束后只有一个全局的i而let在每次循环中会创建一个新的绑定闭包捕获的是每一次循环的独立值。这也是let在实际项目中价值最大的体现。实操心得老项目改造时不要无脑把var全部替换成let要重点排查那些在循环里用了闭包、依赖函数内var提升特性的代码。先把高风险的闭包场景改掉再逐步推进。2.2 const 的正确用法与常见误区const声明一个常量意味着“这个变量的绑定不能被重新赋值”而不是“这个值完全不可变”。比如const obj { name: ES6 }; obj.name JavaScript; // 可以正常修改 // obj {}; // 会报错Assignment to constant variable很多新人刚学的时候看到const对象还能改属性就以为const没用。实际上const保护的是“指针的指向”而不是“指针指向的内容”。你使用const声明一个对象的时候你承诺的是“这个变量永远指向这个对象”至于对象内部怎么变化是对象自己的事情。如果你需要彻底锁定对象的所有属性那要用Object.freeze但Object.freeze也只做浅层冻结嵌套对象还需要递归处理。日常项目里我的习惯是默认所有变量都用const声明只有确定需要重新赋值的场景才用let。这样有一个好处你不能意外地覆盖一个不该变的变量代码的意图也更清晰。另外const声明时必须给初始值不能先声明后赋值。2.3 我建议的项目内变量声明规范结合我的项目经验大致可以总结为下面这几条实践规范所有引用类型变量优先用const除非你能证明它会被重新指向新对象。循环递增、累加器这类需要重新赋值的使用let。永远不要在一个文件里混用var和let/const哪怕老代码里有var新增代码也不要再用它。声明位置尽量靠近使用位置作用域范围越小越好不要让一个变量活着的时间和范围超出它实际被需要的场景。有一个真实场景我印象很深同事在一个大函数顶部声明了一堆let变量结果底部改了一个值中间某处判断结果全变了。排查这种问题非常痛苦。如果把变量声明放在具体使用它的块里这类问题基本能从结构上杜绝。3. 解构赋值与模板字符串代码瘦身神器3.1 数组与对象解构的常见姿势解构赋值是ES6里“用户体验”最好的语法之一它让你从数组或对象中取数据的方式变得极其干净。先看数组解构const [first, second, ...rest] [10, 20, 30, 40, 50]; console.log(first); // 10 console.log(second); // 20 console.log(rest); // [30, 40, 50]这里...是剩余操作符rest它会抓取剩下的所有元素组成新数组。注意它必须放在数组解构的最后一位。这个写法在交换变量时也特别方便let a 1, b 2; [b, a] [a, b]; console.log(a, b); // 2 1不用临时变量一行代码搞定三个步骤的逻辑非常优雅。对象解构在业务代码里的使用频率更高尤其是接口返回的对象取字段时const user { name: 张三, age: 30, address: { city: 北京, district: 朝阳 } }; const { name, age } user; console.log(name, age); // 张三 30对象解构还支持重命名这在处理接口字段名不一致时特别有用const { name: userName, age: userAge } user;还可以给默认值const { gender unknown } user;如果user.gender是undefined就会用默认值unknown。需要特别注意的是默认值只有在属性严格等于undefined时才生效如果是null默认值不会被触发。还有一个非常实用的嵌套解构配合技巧处理深层数据时能省下大串中间变量const { address: { city } } user; console.log(city); // 北京这里要先确保user.address是存在的否则会报错。实战中如果数据结构不稳定建议先用一个空对象兜底再解构比如const { userInfo {} } response; const { nickname } userInfo;3.2 模板字符串在真实业务中的三种妙用模板字符串用反引号包裹它可以做普通字符串拼接待办的事还自带换行保留能力。第一类常见场景是HTML片段拼接以前写一大串div classitem name /div现在直接这样const html div classitem span classname${name}/span span classprice${price}/span /div ;无论是可读性还是维护性都提升了一大截多行文本不再需要手动加\n。第二类场景是拼接带条件的字符串。模板字符串里可以放三元表达式甚至直接放函数调用const statusText ${status 1 ? 已启用 : 已禁用} - ${formatTime(updateTime)};第三类用法有一定技巧性带标签的模板字符串Tagged Template。你可以在模板字符串前加一个函数名函数会收到“字符串片段数组”和“插入的值数组”这样能实现一些模板引擎的底层逻辑。很多第三方库内部就是这么干的。举个例子function highlight(strings, ...values) { return strings.reduce((acc, str, i) { const v values[i - 1]; return acc (v ? mark${v}/mark : ) str; }); } const name ES6; const msg highlight今天学习了${name}; console.log(msg); // 今天学习了markES6/mark虽然日常业务里用得不多但看懂这个机制你阅读某些源码时就会轻松很多。注意模板字符串中的插值表达式会先被求值再转为字符串如果里面放的是一个对象默认会调用toString()得到的往往是[object Object]。拼接非字符串类型时建议先显式处理一下。4. 箭头函数、默认参数与Rest参数函数写法的进化4.1 箭头函数的this绑定机制箭头函数大概是ES6里争议最大的一个特性。它有两个显著特点写法简洁以及不绑定自己的this。先说写法// 传统写法 function add(a, b) { return a b; } // 箭头函数写法 const add (a, b) a b;参数只有一个时还可以省略括号函数体只有单个表达式时可以省略花括号和return。这些规则让代码非常紧凑。但真正重要的是this绑定问题。普通函数里this取决于“怎么被调用”的箭头函数里的this取决于“在哪里定义”的它沿用了外层作用域的this。所以箭头函数本质上是词法this。举个业务中常见的例子class Timer { constructor() { this.seconds 0; } start() { // 传统函数写法会因为this指向变化而报错 setInterval(() { this.seconds; }, 1000); } }在setInterval的回调里普通函数this会指向全局对象而箭头函数直接继承start()方法里的this所以this.seconds不会丢失。这就是箭头函数被大量用在事件回调、定时器回调、数组遍历回调里的原因。箭头函数不能当作构造函数使用也没有arguments对象。如果你需要动态获取函数入参要么用普通函数要么用Rest参数。4.2 默认参数与Rest参数配合使用ES6之前给函数写默认值基本靠的是||或者三元判断容易把0、、false这些合法值也误判成“没传”。ES6的默认参数只在参数为undefined时才生效function greet(name 陌生人, greeting 你好) { return ${greeting}${name}; } console.log(greet()); // 你好陌生人 console.log(greet(张三)); // 你好张三 console.log(greet(张三, 早上好)); // 早上好张三默认参数不仅能写字符串还可以是表达式也可以引用前面的参数function createUser(name, role normal, tag ${name}_${role}) { return { name, role, tag }; }Rest参数用来收集“剩余的”所有参数它是一个真正的数组而旧的arguments只是一个类数组对象还得转换一下才能用map、filter这些方法function sum(...numbers) { return numbers.reduce((acc, n) acc n, 0); } console.log(sum(1, 2, 3, 4)); // 10Rest参数配合默认参数使用时Rest必须放最后且一个函数里只能有一个Rest参数。这套组合在实际项目中用来封装工具函数非常舒服比如写一个日志函数前面若干参数是配置剩下的都算作日志内容。5. ES6 Map与Set别再用对象当万能容器热搜词里有“es6 map”这确实是个高频话题。很多人在ES6里最陌生的对象之一就是Map因为它跟普通的Object长得太像了新手会疑惑“为什么要多一个Map出来”。其实Map和Object的设计目标完全不同。5.1 Map 与普通对象的本质差异普通对象有一个明显的限制键只能是字符串或者Symbol。你就算给对象设置一个数字键引擎也会先把它转成字符串。而Map的键可以是任意类型包括对象、函数、甚至NaN。这个差异在缓存场景里特别有价值const cacheMap new Map(); const keyObj { id: 1 }; cacheMap.set(keyObj, 对应的缓存数据); console.log(cacheMap.get(keyObj)); // 对应的缓存数据对象的键会被强制转成字符串两个内容相同的对象作为键时后面的会把前面的覆盖掉。这个问题让对象无法可靠地做“以对象为键”的缓存。而Map按引用识别键每个对象都是独一无二的身份所以能精确命中。看一个经典面试题为什么object不适合做Map的替代品Map可以直接通过map.size拿到元素数量而对象只能自己数Object.keys(obj).length。Map的迭代顺序是“插入顺序”对象虽然现代引擎也基本有序但传统上并不保证。Map提供了原生方法get/set/has/delete/clear代码可读性和健壮性都更高。频繁增删键值对时Map在大多数引擎里性能表现更好。实际项目中如果数据是一个纯粹的“键值对集合”而且键的类型不限于字符串那么Map永远是更合理的选择。即使键都是字符串当你需要频繁查数量、循环、删除时Map也会让代码更简洁。5.2 Set 与数组的去重、交集、并集、差集操作Set是ES6里另一个人气很高的数据结构它保证“元素不重复”这在很多场景里直接解决了数组去重的问题const nums [1, 2, 2, 3, 4, 4, 5]; const uniqueNums [...new Set(nums)]; console.log(uniqueNums); // [1, 2, 3, 4, 5]这里的[...new Set(nums)]是展开运算符结合Set的经典写法。注意Set的去重是基于严格相等SameValueZero判断的所以NaN和NaN会被视为相同而{}和{}是两个不同的对象不会被去重。除了去重Set还能很方便地实现集合运算const setA new Set([1, 2, 3, 4]); const setB new Set([3, 4, 5, 6]); // 交集A中有且B中也有的 const intersection new Set([...setA].filter(item setB.has(item))); console.log([...intersection]); // [3, 4] // 差集A中有但B中没有的 const difference new Set([...setA].filter(item !setB.has(item))); console.log([...difference]); // [1, 2] // 并集合并去重 const union new Set([...setA, ...setB]); console.log([...union]); // [1, 2, 3, 4, 5, 6]每次检查set.has(item)的时间复杂度是O(1)而数组的includes是O(n)所以在大数据量场景下用Set做集合运算性能优势非常明显。5.3 WeakMap 和 WeakSet 在实战中的价值WeakMap和WeakSet也是ES6家族成员它们的键或元素必须是对象而且弱引用。所谓“弱引用”指的是只要没有其他强引用指向这个键对象垃圾回收器就可以回收它WeakMap中的对应条目也会被清除。这个特性特别适合两个场景。第一个是私有数据存储比如在不改变对象结构的前提下给对象关联额外信息const _privateData new WeakMap(); class Example { constructor(value) { _privateData.set(this, value); } getValue() { return _privateData.get(this); } }this对象作为键存在_privateData里外部代码无法直接访问到这个内部数据。第二个场景是对象关联的缓存比如记录某个DOM元素的自定义信息当DOM被移除后WeakMap里的条目也会自动回收不会造成内存泄漏。WeakSet和WeakMap一样支持弱引用但它是元素的集合适合做“标记”用途比如标记某个对象已经被处理过const processed new WeakSet(); function process(obj) { if (processed.has(obj)) return; // 处理逻辑 processed.add(obj); }注意WeakMap和WeakSet都不可迭代也没有size属性因为它们的内容是不确定的随时可能被垃圾回收。这是“弱引用”必须要做出的取舍不必觉得功能缺失是缺陷。6. 从浅拷贝到深拷贝ES6时代的拷贝实践热搜词里“es6深拷贝”排名很高说明这是大家实际写代码时遇到最多的痛点之一。我在项目评审里见过太多因为浅拷贝而引发的数据串扰问题所以这部分值得单独拿出来好好聊。6.1 为什么浅拷贝还在到处“埋雷”先明确两个概念。浅拷贝是复制“值的引用”如果源对象里某个属性值本身是对象或数组浅拷贝得到的新对象和源对象会共享这一层引用。深拷贝是递归复制所有层级的属性让新旧对象之间完全隔离。日常开发中最常见的浅拷贝写法是展开运算符const original { list: [1, 2, 3], info: { name: ES6 } }; const copy { ...original }; copy.info.name changed; console.log(original.info.name); // changed改的是copy.info结果original.info也变了因为没有复制到深层。这种问题在有复杂配置对象、状态管理、或者把数据传给子组件时非常容易发生。一旦审查不严格就会“改一处坏一片”。另一个常见场景是函数入参。如果你直接把一个对象传给函数函数内部改了它外部对象的对应内容也会跟着变。有时候这是你想要的效果有时候则是灾难。为了避免“意外副作用”函数接收外部对象时应当根据业务意图决定是否需要先做一次拷贝。6.2 基于ES6 API实现深拷贝的几种方案对比深拷贝的实现方案有很多我用一个表格把主流方案对比一下方便你按场景选择方案优点缺点适用场景JSON.parse(JSON.stringify(obj))一行代码写法简单无法处理undefined、函数、Symbol、循环引用Date会变成字符串RegExp会变成空对象纯JSON结构的数据数据不包含函数和特殊类型时递归手写深拷贝可控性强可自定义处理各种类型需要自己处理边界条件代码量较大面试、学习原理、特殊类型定制场景结构化克隆structuredClone浏览器原生API支持Date、RegExp、Map、Set等较新的API老环境可能不支持不支持函数和Symbol循环引用会被复制现代浏览器环境数据含有内置对象时第三方库如lodash的cloneDeep功能最全最稳定需要引入依赖项目里已经有lodash时最常用的是一行代码的JSON方案但它有非常明显的局限。看这段代码const obj { name: ES6, fn: () console.log(hello), symbolKey: Symbol(key), date: new Date(), value: undefined }; const copy JSON.parse(JSON.stringify(obj)); console.log(copy); // { name: ES6, date: 2025-01-01T... }函数、Symbol、undefined全都没了Date也变成了字符串。所以这个方案只适合这种纯数据对象const data { userId: 1024, tags: [a, b, c], settings: { theme: dark, notify: true } }; const dataCopy JSON.parse(JSON.stringify(data));structuredClone是后来浏览器原生提供的能力它能处理更多类型const originalMap new Map([[key, value]]); const clonedMap structuredClone(originalMap); console.log(clonedMap.get(key)); // value它对循环引用和内置类型都有完整的支持是“JSON大法”之外更现代的选择。6.3 深拷贝的边界情况与我的避坑清单我曾经在一个数据导入功能里用过手写递归深拷贝踩了几个坑之后把边界情况都总结出来了。这里分享给你循环引用对象里某个属性直接或间接指向自己。用递归手写时必须维护一个WeakMap记录已拷贝过的对象否则会无限递归导致栈溢出。数组与对象的区分拷贝时要用Array.isArray(value)判断避免数组被拷贝成普通对象丢失length和数组方法。Date、RegExp、Map、Set等内置对象需要单独构造不能直接原样复制。原型链一般业务场景深拷贝不要求保留原型但如果框架代码里会有基于类的实例直接拷贝会丢失原型方法。性能递归深度过深的对象几百层可能造成性能问题生产环境建议用第三方库。下面是我在实际项目中用的一个手写版深拷贝的核心结构它已经能覆盖大部分业务场景function deepClone(target, hash new WeakMap()) { if (target null || typeof target ! object) { return target; } if (target instanceof Date) return new Date(target); if (target instanceof RegExp) return new RegExp(target); if (hash.has(target)) return hash.get(target); const clone Array.isArray(target) ? [] : {}; hash.set(target, clone); Object.keys(target).forEach(key { clone[key] deepClone(target[key], hash); }); return clone; }这个实现里最重要的一行就是hash.set(target, clone)。它用WeakMap把“原对象”和“拷贝对象”的映射记录下来了一旦遇到循环引用直接从映射里取回上次拷贝的结果就不会陷入无限递归。这就是为什么深拷贝的边界问题不是“写不写得出来”而是“有没有把所有边界情况想清楚”。7. Promise与async/await异步流程控制的进化7.1 Promise的三种状态与链式调用ES6把异步编程从回调时代带进了Promise时代。Promise代表一个尚未完成但预期会完成的操作它有三种状态pending进行中、fulfilled已成功、rejected已失败。状态一旦从pending变为fulfilled或rejected就不可再变。最基本的使用方式const p new Promise((resolve, reject) { setTimeout(() { const success true; if (success) { resolve(操作成功); } else { reject(new Error(操作失败)); } }, 1000); }); p.then(result console.log(result)) .catch(error console.error(error));Promise的链式调用解决了“回调地狱”问题。以前的多层嵌套回调写成这样后就能保持扁平getUser() .then(user getOrders(user.id)) .then(orders getDetails(orders[0].id)) .then(details console.log(details)) .catch(error console.error(error));注意每个.then里都可以返回一个新的值或一个新的Promise返回值会被下一个.then接收。这种数据“流水线”式传递方式让异步流程的阅读体验接近同步代码。还有一个容易忽视的点.catch会捕获整条链路上所有前面的错误所以只要在链条末尾放一个.catch就能统一处理所有异常。7.2 async/await让异步代码“看起来像同步”async/await是ES2017加入的语法但它是基于Promise的语法糖。async函数会返回一个Promise函数内部使用await等待一个Promise完成并且会让出线程不阻塞页面。看一个直观对比async function fetchData() { const user await getUser(); const orders await getOrders(user.id); console.log(orders); } fetchData().catch(error console.error(error));顺序和逻辑都非常直观不用再连缀.then()。await只能用在async函数里这是硬性规则。另外await的结果也可以是普通值不一定非得是Promise引擎会把它包装成已完成的Promise。错误处理方面我习惯用try/catch包裹async function loadPageData() { try { const data await request(/api/page); render(data); } catch (error) { showError(error.message); } }如果多个请求彼此独立就不应该用await排队执行那样会浪费并发性能。这种场景应该用Promise.allconst [user, config, banners] await Promise.all([ fetchUser(), fetchConfig(), fetchBanners() ]);三个请求并行发出总耗时接近最慢的那个请求而不是三个请求的时间总和。7.3 我在业务中常用的异步模式实际业务里异步逻辑远比教程里的例子复杂分享两个我总结出的实用模式。第一个是“带超时的请求”。接口偶尔会迟迟不返回但用户不能一直等。用Promise.race配合一个定时器function withTimeout(promise, timeoutMs 8000) { const timeout new Promise((_, reject) { setTimeout(() reject(new Error(请求超时)), timeoutMs); }); return Promise.race([promise, timeout]); } try { const data await withTimeout(request(/api/data)); console.log(data); } catch (error) { console.error(error.message); }Promise.race只要有一个Promise先完成就采用它的结果。超时在8秒内先到就会走拒绝分支。第二个是“串行执行一批任务并收集结果”。比如上传多个文件每次只能上传一个全部传完再汇总async function uploadFiles(files) { const results []; for (const file of files) { const result await upload(file); results.push(result); } return results; }在for...of循环里用await代码是串行的不会像forEach配合await那样因为回调不等待而并行执行。这是一个非常容易踩的坑forEach里写await外部函数根本不会等它执行完。8. ES6模块化import/export的工程化意义8.1 为什么CommonJS满足不了现代前端在ES6模块化之前Node.js使用的是CommonJS规范靠require和module.exports导入导出。这种方式在服务端运行良好但在浏览器端无法直接使用因为浏览器运行时不支持同步的文件读取。前端工程化早期必须借助打包工具才能把CommonJS模块转换成浏览器可执行的代码。ES6的import/export是语言层面的官方模块语法做到了静态化。所谓“静态化”指导入和导出的关系在编译阶段就能确定这让打包工具可以做“摇树优化”Tree Shaking即只打包那些真正被使用到的代码减小最终产物体积。而CommonJS的require是运行时调用导入关系没法在编译阶段静态分析。8.2 import/export的使用细节与坑命名导出使用起来最直接// utils.js export const formatTime (date) date.toLocaleTimeString(); export const formatNumber (num) num.toLocaleString(); // 使用 import { formatTime, formatNumber } from ./utils;如果只想精确导入其中一个也可以在导入部分用as重命名import { formatTime as format } from ./utils;默认导出比较适合一个模块只导出一个主要功能的情况// logger.js export default function log(message) { console.log(message); } // 使用 import log from ./logger;一个常见混淆是默认导出和命名导出的混用// 同时使用默认导出和命名导出是允许的 export default log; export const LEVEL info;导入时默认导入和命名导入可以配合使用。但如果写组合导入容易在重构时因为忘记某个命名而导致打包报错。我的建议是如果模块里有多个工具函数全部用命名导出如果一个模块核心职责是导出一个类或一个函数优先默认导出。保持统一风格团队协作时心智负担最小。还有两个容易忽视的点导入语句里的路径必须包括文件扩展名尤其是现代浏览器原生ESM和部分打包工具的strict模式以及import会被提升到当前模块顶部所以你写在哪一行都行但一定要让代码结构清晰、路径可读。实操心得当你拆分一个大文件时先列出这个模块的公共API导出的东西再反向去写内部实现。先想清楚“外部会用到什么”再动手拆分模块边界会清晰很多代码复用性也会更好。9. 常见问题速查表与我的排坑经验这个部分就直接上干货我在学习和一线开发中遇到的高频问题、排查思路和解法都整理出来了。建议收藏备用。9.1 我实测踩过的高频问题先说几个堪称“新人必踩”的问题。第一个是const对象属性被修改。很多人以为const对象就不能改了结果改了属性发现没报错就怀疑自己写错了。前面已经解释过const管的是绑定关系不是对象内容。第二个是typeof判断不出数组。数组用typeof检测会返回object所以判断是否数组要用Array.isArray(arr)不要用typeof。第三个是数组解构时undefined和越界。解构不存在的数组下标拿到的是undefined不是默认值。默认值只对undefined生效。第四个是JSON.parse(JSON.stringify())丢失函数和undefined。这个前面已经详细说过了属于深拷贝系列的经典坑改写业务之前一定要先确认数据里没有这些特殊类型。第五个是箭头函数的this被过度使用。比如在需要动态this的事件监听器里用普通函数反而更合适。别把箭头函数当万能药。第六个是Map和Object的取属性方式搞混。Map要用.get()和.set()不是直接用点号或者中括号访问属性名。第七个是Set去重后忘记转回数组。很多人去重后直接返回了Set结果下游代码当成数组用导致length是undefined。第八个是Promise.all遇到一个失败就全部失败。如果你希望某个请求失败不影响其他结果可以改用Promise.allSettled。第九个是async/await误用场景。在forEach里使用await不会按预期串行执行要改用for...of。9.2 排查ES6问题的三条经验第一先确认运行环境对ES6语法的支持情况。虽然现在主流环境都支持但如果你在老旧浏览器或低版本Node环境里运行比如某些内嵌WebView或者老款移动设备部分语法可能直接报错。排查这类问题先看浏览器控制台有没有SyntaxError或者TypeError“依赖于现代语法特性”的报错通常指向环境兼容性。第二统一使用严格模式。ES6的模块默认就是严格模式所以你不必手动加use strict。严格模式下很多旧式写法会直接报错比如给未声明的变量赋值、删除不可删除的属性等。这能帮你尽早暴露问题而不是让错误悄悄蔓延。第三善用调试工具和断点。很多人用console.log打一堆日志但面对复杂对象时最好用console.dir展开查看对象结构或者直接在Sources面板打断点一步步看每个变量的实时值。排查引用类型问题时注意观察“同一份引用”导致的连带修改经验不足的人经常会漏掉这个细节。这些坑里面我踩得最惨的是forEachawait那次。需求是轮流请求一个分页接口每次把结果合并我一开始写成items.forEach(async item { await fetch(...) })结果函数立刻返回页面数据一直加载不出来。排查半天才发现是因为forEach的回调不会等待异步任务。换成for...of之后问题立刻消失。这个经历让我养成一个习惯凡是涉及异步操作的任务先想清楚“这个循环体要不要等上一次完成”再去选循环方式。10. 收尾我的ES6学习体会作为一个已经用ES6写了多年业务代码的人我的感受是ES6真正改变的不仅是语法更是一种思维习惯。它让JavaScript的代码变得更清晰、更可靠、也更接近工程化语言的标准。学习时不要只背API一定要拿着真实业务场景去练比如把一个老模块用新语法重构一遍把一段回调改成async/await把一组普通对象改成Map操作。只有当你真正经历过“旧写法带来的痛点”才会理解新语法设计的巧妙之处。最后再分享一个小技巧学习ES6的时候自己准备一个notes.md文件每学会一个知识点就写下一个真实业务场景的例子用文字解释为什么要用这种写法以及不用会踩什么坑。等你积累到一定量级再回看就会发现自己对这个语言的理解已经远超“会用”的层面。这篇笔记里的很多内容本质上就是我那个notes.md的更新迭代。希望它对正在学ES6的你也有同样的帮助。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。