1. 项目概述与核心思路
最近在逛超市的时候,看着货架上琳琅满目的商品和它们五花八门的价签,我突然想到一个事儿:那些标着“每公斤XX元”或者“每磅XX元”的单价,到底是怎么算出来的?对于消费者来说,这个单价是判断“划不划算”最直接的依据。但如果我们自己动手,用代码来构建一个这样的计算器,是不是既能解决实际问题,又能顺带把最新的技术给玩一遍?这个想法让我决定动手做一个“单位价格计算器”。
这个项目的核心很简单:用户选择商品、重量单位和具体的价格/重量组合,点击按钮,程序就能算出每单位(比如每公斤、每磅)的价格。但我的“私心”在于,我想用这个看似简单的应用,作为一次对JavaScript ES2020所有七个新特性的集中实践和深度探索。前端用最纯粹的Vanilla JavaScript,后端用Node.js和Express.js搭建,最后部署到Heroku上。整个过程,我会把Promise.allSettled()、可选链操作符(?.)、空值合并运算符(??)、globalThis、动态导入(import())、String.prototype.matchAll()和BigInt这七个特性,一个不落地用进去,并且会详细拆解每个特性“为什么”要在这里用,以及“怎么用”才最地道。
对于前端开发者来说,保持技术敏感度是职业生命线。ECMAScript规范每年都在更新,ES2020(也就是ES11)带来的这七个特性,每一个都切中了我们日常开发中的痛点。通过这个具体的项目,我希望不仅能展示这些特性怎么用,更能讲清楚它们背后的设计意图和适用场景,让你下次在遇到类似问题时,能立刻想到:“哎,这个场景用ES2020的那个新特性正合适!”
2. 环境搭建与项目初始化
2.1 技术栈选型与工具准备
这个项目在技术选型上遵循“简洁够用”的原则。前端没有引入任何框架(React, Vue, Angular等),就是为了保持纯粹,让我们能聚焦于JavaScript语言本身的新特性。后端选择了Node.js配合Express.js,这是构建轻量级RESTful API最快速、最经典的方式之一。部署平台选用Heroku,主要是看中它对Node.js应用的原生友好和极简的部署流程,能让我们把精力集中在代码逻辑而非运维配置上。
在开始之前,你需要确保本地环境已经就绪:
- Node.js与npm:建议安装最新的LTS版本。你可以去Node.js官网下载安装包,或者使用nvm(Node Version Manager)这类工具进行版本管理,这在实际工作中非常有用,可以方便地在不同项目间切换Node版本。
- Heroku CLI:这是与Heroku平台交互的命令行工具。你需要先在Heroku官网注册一个免费账户,然后根据官方指南安装CLI。安装成功后,在终端运行
heroku login命令完成登录。 - 代码编辑器:VS Code、WebStorm、Sublime Text等任选其一,我个人习惯用VS Code,插件生态丰富。
- 浏览器开发者工具:现代浏览器(Chrome, Firefox, Edge)自带的开发者工具是调试JavaScript的利器,我们后面会频繁用到它的“网络”(Network)和“控制台”(Console)面板。
注意:虽然ES2020特性很新,但并非所有浏览器(尤其是旧版本)都完全支持。在生产环境中,如果需要考虑广泛的浏览器兼容性,务必使用Babel等转译器将代码转换到兼容的ES版本,或者通过
core-js等库提供polyfill。本项目为了演示清晰,默认在支持ES2020的现代浏览器环境中运行。
2.2 项目结构与初始化
我们可以从零开始创建项目,但为了快速进入主题,我建议直接克隆我已经准备好的项目仓库。打开你的终端,执行以下命令:
git clone https://github.com/thawkin3/unit-price-calculator.git cd unit-price-calculator进入项目目录后,你会看到如下的基础结构:
unit-price-calculator/ ├── public/ │ ├── index.html │ ├── styles.css │ └── main.js ├── server/ │ └── server.js ├── package.json └── README.mdpublic/目录存放所有前端静态资源:index.html是应用入口,styles.css是样式(为了专注JS,样式写得比较基础),main.js是我们前端逻辑的核心,所有ES2020特性的演示都将在这里发生。server/目录下是后端的Express服务器代码。package.json文件定义了项目依赖和启动脚本。
接下来,我们需要安装项目依赖。在项目根目录下运行:
npm install这个命令会根据package.json中的定义,下载Express等必要的后端依赖包。
安装完成后,你可以先在本地运行起来看看效果:
npm start通常,npm start会执行node server/server.js。如果一切顺利,终端会提示服务器已在某个端口(比如3000)启动。打开浏览器,访问http://localhost:3000,你应该能看到一个简单的单位价格计算器界面。
本地测试没问题后,就可以准备部署了。在项目根目录下,依次执行以下Heroku命令:
heroku create git push heroku master heroku openheroku create会随机生成一个应用名并在Heroku上创建对应的应用。git push heroku master将你的代码推送到Heroku的远程仓库并触发自动构建、部署。最后heroku open会自动在浏览器中打开你刚刚部署的应用。第一次部署可能会花一两分钟时间,耐心等待即可。
3. ES2020七大特性深度解析与应用
3.1 Promise.allSettled(): 更优雅的并行请求处理
当用户打开我们的计算器页面时,应用需要从后端API获取三组数据:产品列表、价格列表和产品描述。这三个请求是相互独立的,我们可以同时发起它们以提升页面加载速度。传统的Promise.all()在这里会有一个问题:如果三个请求中有一个失败了(比如网络问题或服务器错误),那么整个Promise.all()会立即被拒绝(reject),你拿不到其他两个可能已经成功的请求结果。
这在用户体验上是不友好的。用户可能更希望看到“产品列表和描述加载成功了,但价格暂时没拿到,显示个加载失败提示”,而不是整个页面因为一个接口挂掉就白屏或卡住。Promise.allSettled()正是为了解决这个痛点而生的。
在我的main.js中,初始化数据加载是这样写的:
const fetchProductsPromise = fetch('/api/products').then(response => response.json()); const fetchPricesPromise = fetch('/api/prices').then(response => response.json()); const fetchDescriptionsPromise = fetch('/api/descriptions').then(response => response.json()); Promise.allSettled([fetchProductsPromise, fetchPricesPromise, fetchDescriptionsPromise]) .then(data => { // 处理响应,无论单个请求成功与否 if (data?.[0]?.status === 'fulfilled' && data?.[1]?.status === 'fulfilled') { const products = data[0].value?.products; const prices = data[1].value?.prices; const descriptions = data[2].value?.descriptions; // ... 用这些数据更新UI } else { // 处理部分失败的情况,例如显示错误信息 console.error('部分数据加载失败:', data); } }) .catch(err => { // 这里捕获的是 allSettled 自身极少发生的错误,而非子Promise的拒绝 console.error('请求过程发生意外错误:', err); });关键点解析:
- 返回值:
Promise.allSettled()返回的Promise永远不会被拒绝(除非你传的参数不是可迭代对象)。它总是会解决(resolve)为一个数组,数组的每个元素都是一个对象,描述对应Promise的最终状态。 - 状态对象:每个状态对象都有
status属性,值为'fulfilled'(已兑现)或'rejected'(已拒绝)。如果状态是'fulfilled',对象会包含一个value属性,保存成功的结果;如果是'rejected',则包含一个reason属性,保存失败的原因。 - 与Promise.all()的对比:这是最重要的区别。
Promise.all()是“一损俱损”,只要有一个失败,整个就失败。Promise.allSettled()是“各自为政”,等待所有Promise都有最终结果(settled),无论成功失败。在处理多个独立并行任务(如多个API调用、多个文件读取)且不希望一个失败导致全盘皆输时,Promise.allSettled()是更安全、更合理的选择。
3.2 可选链操作符 (?.):告别冗长的防御性代码
在处理从Promise.allSettled()返回的data数组时,我写了这样一行判断:data?.[0]?.status。这里的?.就是可选链操作符。在ES2020之前,为了安全地访问深层嵌套的属性,我们不得不写很多重复的、丑陋的条件判断。
假设我们有一个用户对象user,想获取他的街道地址,老代码是这样的:
const user = { name: 'Alice', address: { street: '123 Main St', city: 'Somewhere' } }; // 旧的写法:冗长且易错 const streetOldWay = user && user.address && user.address.street; // 或者用三元运算符,同样啰嗦如果user或user.address是null或undefined,上面的代码可以防止Cannot read property 'street' of undefined这样的运行时错误,但写法非常不优雅。
有了可选链,一切都变得简洁明了:
const street = user?.address?.street; // '123 Main St' const fakeProp = user?.fakeProp?.fakeChild; // undefined它的工作逻辑是:如果?.前面的值是null或undefined,表达式会立即短路,返回undefined,而不会尝试去访问后面的属性。这极大地简化了深层对象属性访问时的安全校验代码。
在我的项目里,data是从一个异步操作返回的,其结构可能存在不确定性。使用data?.[0]?.status,我可以安全地检查第一个Promise的结果状态,而无需担心data本身是否为数组、或者data[0]是否存在。如果中间任何一环是nullish(null或undefined),表达式会平静地返回undefined,然后我的if条件判断会处理这种情况。
实操心得:可选链操作符不仅可以用在属性访问上,还可以用在函数调用和数组访问上。例如:
obj.someMethod?.()可以安全地调用一个可能不存在的方法;arr?.[index]可以安全地访问数组元素。这几乎消除了因访问未定义属性/方法而导致的TypeError,是提升代码健壮性的利器。
3.3 空值合并运算符 (??):明确的默认值设定
在应用中,我允许用户选择偏好使用公斤(kg)还是磅(lb)作为重量单位,并将这个偏好保存在浏览器的localStorage里。当应用初始化时,我需要读取这个偏好。对于首次访问的用户,localStorage里没有这个值,读取出来会是null。这时,我需要一个默认值(比如默认使用公斤)。
你可能会想用逻辑或运算符||来解决:
const doesPreferKilograms = JSON.parse(localStorage.getItem('prefersKg')) || true;但这里有个陷阱。||操作符会在左侧操作数为假值(falsy)时返回右侧操作数。JavaScript中的假值包括:false,0,''(空字符串),null,undefined,NaN。如果用户明确地选择了“使用磅”,即存入了false,那么JSON.parse('false')的结果是false。false || true的结果是true!这完全违背了用户的明确选择。
??运算符的行为更符合直觉:它只在左侧操作数是nullish(null或undefined)时,才返回右侧操作数。
const doesPreferKilograms = JSON.parse(localStorage.getItem('prefersKg') ?? 'true');这段代码的意思是:尝试从localStorage读取'prefersKg'。如果读到的值是null或undefined(即键不存在),则使用默认字符串'true';否则,就使用读到的值。这样,即使用户存的是false,也会被正确解析和使用。
一个更清晰的对比示例:
const value1 = false ?? 'default'; // false const value2 = false || 'default'; // 'default' const value3 = 0 ?? 100; // 0 const value4 = 0 || 100; // 100 const value5 = '' ?? 'hello'; // '' const value6 = '' || 'hello'; // 'hello' const value7 = null ?? 'default'; // 'default' const value8 = undefined ?? 'default'; // 'default'当你需要区分“假值”和“未定义/空值”时,??是你的最佳选择。这在处理配置项、函数参数默认值时非常有用。
3.4 globalThis:跨越环境的全局对象访问
为了读取localStorage,我写了这样一行代码:globalThis.localStorage?.getItem?.('prefersKg')。这里出现了两个新特性:可选链和globalThis。
globalThis提供了一个标准化的方式,在任何JavaScript环境中访问全局对象。在以前,这很让人头疼:
- 在Web浏览器中,全局对象是
window(对于主线程)或self(对于Web Worker)。 - 在Node.js中,全局对象是
global。 - 在严格模式下,
this在函数内部可能是undefined。
为了写一段能在多种环境下运行的通用代码,你不得不做环境检测:
// 旧的、繁琐的写法 const getGlobal = function() { if (typeof self !== 'undefined') { return self; } if (typeof window !== 'undefined') { return window; } if (typeof global !== 'undefined') { return global; } throw new Error('无法找到全局对象'); }; const myGlobal = getGlobal();现在,只需要使用globalThis:
// 在浏览器中 console.log(globalThis === window); // true (在主线程中) // 在Node.js中 console.log(globalThis === global); // true // 通用的访问 const storage = globalThis.localStorage; // 在浏览器中获取 localStorage,在Node中为 undefined在我的代码里,使用globalThis.localStorage是一种更规范和面向未来的写法。虽然在这个特定的浏览器前端项目中,直接写localStorage或window.localStorage也能工作,但使用globalThis表明这段代码意识到了不同的运行时环境,更具可移植性。结合可选链?.进行特性检测(globalThis.localStorage?.getItem?.),可以优雅地处理Node.js等没有localStorage的环境。
3.5 动态导入 (import()):按需加载的现代方案
当用户选好商品、单位和价格组合,点击“计算”按钮时,应用才需要加载执行具体计算逻辑的模块。这是一个典型的“按需加载”或“懒加载”场景。在ES2020之前,我们通常依赖打包工具(如Webpack)的代码分割功能来实现。现在,动态导入import()成为了语言标准的一部分。
在我的main.js中,计算按钮的点击事件处理函数里是这样的:
calculateButton.addEventListener('click', () => { import('./calculate.js') .then(module => { // 模块加载成功,使用其导出的方法 const unitPrice = module.calculateUnitPrice(selectedProduct, selectedWeightAndPrice); displayResult(unitPrice); }) .catch(err => { // 处理模块加载失败或执行中的错误 console.error('计算模块加载失败:', err); displayError('无法执行计算,请刷新重试。'); }); });动态导入的核心优势:
- 返回Promise:
import()函数返回一个Promise,这使得它可以完美地融入基于Promise的异步编程模式,用.then()和.catch()处理成功和失败。 - 真正的按需加载:只有在代码执行到
import()语句时,浏览器才会发起网络请求去获取目标模块。你可以在浏览器的“网络”面板中清晰地看到,初始页面加载时没有calculate.js的请求,点击按钮后才会出现。这减少了初始包的体积,加快了首屏加载速度。 - 作用域:动态导入的模块会独立编译,其顶层变量不会污染全局作用域。
一个更实际的场景:假设你的应用有一个“生成复杂报表”的功能,其依赖的图表库和数据处理逻辑非常庞大。你可以这样组织代码:
// 报表页面主文件 document.getElementById('generateReport').addEventListener('click', async () => { // 用户点击时才加载庞大的报表模块 const reportModule = await import('./heavyReportModule.js'); reportModule.generateComplexReport(); });这样,99%的不需要生成报表的用户,就无需为这个功能下载那几百KB甚至上MB的代码。
注意事项:动态导入的模块路径通常是相对于当前模块的。在打包工具(如Webpack、Vite)中,
import()还会自动成为代码分割点,生成独立的chunk文件。确保你的服务器或CDN能正确返回这些按需加载的.js文件。
3.6 String.prototype.matchAll(): 强大的正则表达式全局匹配
为了计算单价,我需要从用户选择的字符串(如"$200 for 10 kg")中提取出价格(200)、重量(10)和单位(kg)。我使用了正则表达式和matchAll()方法:
const matchResults = [...weightAndPrice.matchAll(/\d+|lb|kg/g)]; // 假设 weightAndPrice = "$200 for 10 kg" // matchResults 会是: [ ['200'], ['10'], ['kg'] ] // 那么: // const price = matchResults[0][0]; // '200' // const weight = matchResults[1][0]; // '10' // const unit = matchResults[2][0]; // 'kg'为什么不用传统的match()或exec()?
String.prototype.match():当正则表达式带有全局标志g时,它返回一个包含所有匹配子串的数组,但不包含捕获组信息。这对我需要提取数字和单位的具体值来说不够用。RegExp.prototype.exec():在循环中调用可以获取每个匹配的完整信息(包括捕获组),但写法比较繁琐。
String.prototype.matchAll()完美地解决了这个问题:它针对一个字符串和带有g标志的正则表达式,返回一个迭代器(iterator),每次迭代都返回一个与RegExp.prototype.exec()返回值格式相同的匹配结果数组,包含了完整的匹配信息和捕获组。
看一个更复杂的例子来理解捕获组:
const regexp = /t(e)(st(\d?))/g; // 匹配'test'后面跟一个可选数字,并有多个捕获组 const str = 'test1test2'; // 使用 matchAll const matches = str.matchAll(regexp); for (const match of matches) { console.log(match); } // 输出: // [ 'test1', 'e', 'st1', '1', index: 0, input: 'test1test2', groups: undefined ] // [ 'test2', 'e', 'st2', '2', index: 5, input: 'test1test2', groups: undefined ] // match[0]: 完整匹配 'test1' // match[1]: 第一个捕获组 'e' // match[2]: 第二个捕获组 'st1' // match[3]: 第三个捕获组 '1' // index: 匹配开始的索引通过matchAll(),我可以一次性获得所有匹配的详细信息,无需手动循环调用exec(),代码更清晰、更函数式(通过扩展运算符...转换为数组)。
踩坑提醒:
matchAll()要求传入的正则表达式必须具有全局标志g,否则会抛出TypeError。这是因为它被设计为一次性获取所有全局匹配。如果你的正则不需要全局匹配,使用match()或exec()即可。
3.7 BigInt:应对超大整数运算
最后,在计算单价时,我虽然用了普通的数字运算,但特意提到了BigInt。单价计算本身(如20000 / 10)不太可能超出JavaScript安全整数范围,但设想一下,如果你的电商平台要计算一批价值数十亿的加密货币或精密仪器的单价呢?普通JavaScript数字(基于IEEE 754双精度浮点数)在超出Number.MAX_SAFE_INTEGER(即2^53 - 1,约9千万亿)时,就会失去精度。
BigInt是一种新的基本数据类型,用来表示任意精度的整数。创建BigInt有两种方式:
- 在整数字面量后面加
n:const huge = 9007199254740993n; - 调用
BigInt()函数:const huge = BigInt(9007199254740993);
在我的计算模块calculate.js中,可以这样进行“防御性”编程,以应对未来可能的大数:
export function calculateUnitPrice(priceStr, weightStr) { // 假设从字符串解析出极大的数值 const price = BigInt(parseInt(priceStr, 10)); // 转换为BigInt const weight = BigInt(parseInt(weightStr, 10)); // 进行除法运算。BigInt运算结果仍是BigInt,且除法会向下取整。 const unitPriceBigInt = price / weight; // 如果最终需要以普通数字显示,且确定结果在安全范围内,可以转换回来 // 但转换可能丢失精度,如果超出安全范围 const unitPrice = Number(unitPriceBigInt); return isFinite(unitPrice) ? unitPrice : unitPriceBigInt.toString(); // 返回数字或字符串表示 }BigInt使用须知:
- 类型:
BigInt和普通的Number是两种不同的类型,不能混合运算。1n + 2会报错,需要显式转换:1n + BigInt(2)。 - 运算:支持
+,-,*,/,%,但除法/的结果会向零取整(即丢弃小数部分)。 - 比较:可以与
Number进行宽松相等(==)比较,但不能进行严格相等(===)比较,因为类型不同。大小比较(>,<等)可以正常工作。 - JSON:
JSON.stringify()不能序列化BigInt,会抛出错误。需要自定义序列化逻辑。
虽然在这个简单的单价计算器里用BigInt有点“杀鸡用牛刀”,但理解它对于处理金融、科学计算或大型ID生成等领域的数值问题至关重要。
4. 项目部署、优化与问题排查
4.1 部署到Heroku的细节与优化
将Node.js应用部署到Heroku通常非常顺畅,但了解其机制能帮你避免一些坑。当你执行git push heroku master时,Heroku会做以下几件事:
- 检测语言:它查看你的项目根目录是否有
package.json,从而识别为Node.js项目。 - 安装依赖:运行
npm install(或yarn install)安装dependencies中的所有包。 - 构建:如果
package.json中有"scripts"下的"build"命令(例如"build": "webpack --mode production"),Heroku会执行它。这对于前端资源优化(压缩、打包)非常关键。 - 启动:Heroku会寻找
"scripts"下的"start"命令来启动应用。在我们的项目中,就是node server/server.js。
优化建议:
- 指定Node版本:在
package.json中增加"engines"字段,锁定Node.js和npm的版本,确保环境一致性。"engines": { "node": "18.x", "npm": "9.x" } - 环境变量管理:切勿将敏感信息(如数据库密码、API密钥)硬编码在代码中。Heroku提供了配置变量的功能(在应用设置页或通过CLI命令
heroku config:set KEY=VALUE)。在代码中通过process.env.KEY访问。 - 静态文件服务:我们的Express服务器通过
app.use(express.static('public'))来提供前端文件。确保public目录结构正确,且index.html在其中。 - 日志查看:部署后如果应用出错,使用
heroku logs --tail命令实时查看日志,这是排查问题最直接的方式。
4.2 前端代码结构与模块化实践
虽然本项目为了演示ES2020特性,将大部分逻辑写在了main.js,但在实际项目中,良好的模块化是必须的。利用ES6模块(import/export)和动态导入,我们可以将代码组织得更好。
重构建议:
- 按功能拆分模块:
api.js:封装所有与后端API通信的函数(fetchProducts,fetchPrices等)。domUtils.js:封装所有DOM操作函数(populateProductDropdown,displayResult,displayError等)。state.js:管理应用状态(appState对象)。calculate.js:计算逻辑(已存在)。main.js:作为入口文件,组织模块并初始化事件监听。
- 使用动态导入实现路由懒加载(如果应用复杂):如果是单页面应用(SPA),可以为不同页面创建单独的模块,在路由变化时动态导入。
// 假设有关于页面 document.getElementById('about-link').addEventListener('click', async (e) => { e.preventDefault(); const aboutModule = await import('./pages/about.js'); aboutModule.render(); history.pushState(null, '', '/about'); });
4.3 常见问题排查与调试技巧
在开发和集成这些新特性时,你可能会遇到一些问题。以下是一些常见场景及解决方法:
“Uncaught SyntaxError: Unexpected token ‘.’” 或类似错误
- 可能原因:浏览器不支持可选链操作符(
?.)或空值合并运算符(??)。 - 解决方案:使用Babel进行转译。安装
@babel/core,@babel/preset-env,并配置.babelrc文件,将preset-env的targets设置为需要支持的浏览器版本。或者,在构建工具(如Webpack)中集成Babel loader。
- 可能原因:浏览器不支持可选链操作符(
“Promise.allSettled is not a function”
- 可能原因:运行环境(如旧版Node.js或浏览器)不支持
Promise.allSettled。 - 解决方案:添加polyfill。可以直接引入
core-js的polyfill:import 'core-js/features/promise/all-settled';。或者,在package.json中配置@babel/preset-env的useBuiltIns: 'usage',Babel会自动按需引入polyfill。
- 可能原因:运行环境(如旧版Node.js或浏览器)不支持
动态导入的模块找不到(404错误)
- 可能原因:模块路径错误,或者服务器没有正确配置以返回
.js文件。 - 解决方案:
- 检查
import()中的路径是相对于当前JS文件的。如果main.js在/public/目录,import('./calculate.js')会在/public/calculate.js查找。 - 确保Express的静态文件中间件正确配置了
public目录。 - 如果使用打包工具,动态导入的模块可能会被重命名(例如
calculate.[hash].js)。需要确认打包后的路径映射。
- 检查
- 可能原因:模块路径错误,或者服务器没有正确配置以返回
matchAll()报错“TypeError: String.prototype.matchAll called with a non-global RegExp argument”- 原因:传给
matchAll()的正则表达式没有g标志。 - 解决:确保正则表达式以
/.../g结尾。如果不需要全局匹配,考虑使用match()或exec()。
- 原因:传给
Heroku部署后应用崩溃(Application Error)
- 排查步骤:
heroku logs --tail:查看详细错误日志。最常见的原因是:- 端口绑定错误:Heroku会动态分配端口,通过
process.env.PORT获取。确保你的服务器监听的是process.env.PORT || 3000。 - 依赖安装失败:检查
package.json中dependencies是否都正确,网络是否通畅。 - 启动脚本错误:检查
package.json中的"start"脚本是否能正确启动服务器。
- 端口绑定错误:Heroku会动态分配端口,通过
- 检查Procfile(如果有):Heroku默认使用
npm start,但如果你定义了Procfile,它会覆盖默认行为。 - 本地模拟生产环境测试:在本地运行
npm run build(如果有)和npm start,看是否能正常启动。
- 排查步骤:
ESLint或代码编辑器报语法错误(对新特性)
- 原因:ESLint或编辑器的语法解析器版本过低。
- 解决:更新ESLint及相关parser(如
babel-eslint或@babel/eslint-parser)到最新版本,并在ESLint配置文件中设置parserOptions: { ecmaVersion: 2020 }。
通过这个项目,我们不仅构建了一个实用的工具,更完成了一次对JavaScript最新语言特性的深度遍历。从处理异步操作的Promise.allSettled(),到让代码更简洁安全的?.和??,再到跨环境的globalThis、提升性能的动态导入、处理文本的利器matchAll(),以及应对大数的BigInt,ES2020的这七大特性实实在在地解决了我们日常开发中的诸多痛点。我的建议是,不要仅仅停留在“知道”,而是像这个项目一样,找一个具体的场景去“用起来”。在用的过程中,你才会真正理解它们的妙处和边界。下次当你写代码时,遇到需要安全访问属性、设置默认值、或者处理多个并行异步任务时,不妨停下来想想:“ES2020有没有更好的工具可以帮我?”