ES2016(ES7)小而美:includes和指数运算符深度指南

发布时间:2026/9/15 10:31:19
ES2016(ES7)小而美:includes和指数运算符深度指南 ES7ES2016可能是我见过最容易被“误会”的一个JavaScript版本。你说它没存在感吧面试题里三天两头出现Array.prototype.includes你说它重要吧整份规范拢共就俩新特性和后面ES2017一口气塞进async/await、Object.values()、String.padStart()的大礼包完全不是一个量级。但恰恰是这种“小而精”的更新特别适合拿来理解JavaScript语言往后的演进节奏。这篇文章我就以实际开发者的视角把ES2016这两个特性拆开揉碎顺带把配套的工程化落地方案也一并讲清楚。无论你是刚学完ES6准备往新版本看的初学者还是在项目里要做语法升级的老手都能从这份总结里拿到可以直接用的东西。1. 项目概述与整体设计思路1.1 一次“小而美”的规范更新到底解决了什么问题ES2016也就是大家习惯叫的ES7在2016年6月正式定稿它是JavaScript改用“年份命名法”的第一个版本。有意思的是整份规范最终只收录了两个新特性Array.prototype.includes和指数运算符**。第一次看到这个清单的人多半会愣一下觉得这也太“寒酸”了但如果你回头看TC39的工作流程就会明白这不是偷懒而是刻意为之——从ES2015这样憋了六年大招的巨型版本过渡到“每年都能安全落地新功能”的稳定节奏必须有人先走一步用一次小体量发布来验证这套流程跑不跑得通。这两个特性其实都算不上“发明创造”更像是把社区里反复出现的需求给正式扶正了。includes解决的问题非常具体判断一个数组里有没有某个值这个操作太常见了但老办法indexOf用起来总有点别扭一个是语义不直观另一个是它压根判断不了NaN。而**运算符则是把Math.pow()这个函数调用直接提升成了语言层面的语法糖。两者看起来都不起眼但恰恰因为它们足够克制才能以零争议的姿态快速走完提案流程在当年6月就发布定稿。1.2 为什么ES2017、ES2018反而比它“火”ES2016真的过时了吗很多人容易产生一个错觉觉得ES2016出了等于没出反正后面版本特性更多更亮眼。但实际写代码的时候你会发现includes和**的使用频率一点也不低尤其是includes它在处理权限判断、状态过滤、搜索匹配这些场景里出镜率极高。我自己的项目里ES2016的这两个特性几乎每天都在用反而某些ES2017、ES2018的大特性可能一整周都碰不到一次。另外还有一个容易混淆的点Object.values()、Object.entries()、async/await这些看起来“好像也是ES7”的东西实际上全部属于ES2017ES8。包括很多老文章里误把String.prototype.padStart归到ES7里也是同样的错误。所以读这篇文章之前先把认知校准一下ES2016 includes**就这两样后面那些是隔壁ES2017的事。2. 核心特性解析Array.prototype.includes2.1 从API设计看includes的设计哲学远不止“语法糖”这么简单Array.prototype.includes的完整签名是arr.includes(valueToFind, fromIndex)第一个参数是要查找的值第二个参数是起始搜索索引。它返回一个布尔值表示数组中是否存在该元素。这个API看起来就是indexOf的一个“布尔版”但仔细品一下它其实是把“找位置”和“判断存在性”这两件事彻底分开了。indexOf干了太多活找不到要返回-1找到了要返回下标然后你还得在调用方把-1这种反人类的返回值再翻译成“不存在”。而includes干脆不关心位置直接给出“是或否”的答案。从代码可读性角度来说if (arr.includes(x))比if (arr.indexOf(x) ! -1)直观太多了读代码的人不需要再心算一遍“-1是什么来着”。在设计哲学上includes有两个值得单独拎出来说的点。第一它对NaN的处理是正确的数组里有NaN时用includes能查到但indexOf永远返回-1这在处理一些包含脏数据的场景时非常致命。第二includes对稀疏数组的空位是按undefined处理的这一点和indexOf一致不会因为数组里有空洞就直接跳过导致漏判。2.2 从零手写一个includes顺便看看fromIndex的各种边界情况既然说它是语法糖那我自己动手实现一个同功能的函数能更直观地看清它的边界行为。一个相对完整的polyfill版本是这样的function includes(arr, valueToFind, fromIndex) { if (arr null) { throw new TypeError(this is null or not defined) } const o Object(arr) const len o.length 0 if (len 0) { return false } const n Number(fromIndex) || 0 const k Math.max(n 0 ? n : len n, 0) while (k len) { if (o[k] valueToFind || (valueToFind ! valueToFind o[k] ! o[k])) { return true } k } return false }注意看几个核心逻辑fromIndex如果是负数会从数组末尾开始倒数定位比如arr.includes(a, -2)会从倒数第二个元素往右查如果fromIndex的绝对值大于数组长度会被Math.max(..., 0)强行拉回0如果fromIndex本身就大于等于数组长度循环体压根不会执行直接返回false。至于valueToFind ! valueToFind这个看起来诡异的条件就是在用“自己不等于自己”来识别NaN这是indexOf时代做NaN判断的经典手法。看完这段代码你就明白includes之所以比indexOf好用不是凭空变出来的魔法而是在规范层面上就把这些坑都堵死了。2.3 includes的“看不见”的细节对象比较与引用陷阱有一点需要特别注意includes做的是“同值零算法”比较基本类型比较值但引用类型只比较引用地址。这意味着如果你有一个对象数组用includes去查一个“长得一模一样但不同引用”的对象结果永远是falseconst users [{ name: Alice }, { name: Bob }] users.includes({ name: Alice }) // false这个坑我在项目里踩过一次。当时是从接口拉了一组选中的对象ID本想把它们和全量列表里的对象做匹配直接写了list.includes(selectedObj)结果怎么匹配都是空。后来排查了半天才意识到接口返回的selectedObj是重新解析出来的新对象和列表里那个对象在内存中根本不是同一个引用。这种情况下正确的做法是改用some()list.some(item item.id selectedObj.id) // true所以includes适合判断的是原始类型数组或者是明确复用同一个引用的场景。如果你的比对逻辑是“按内容相等”那还是老老实实用some加比较函数别指望includes帮你做深比较。3. 核心特性解析指数运算符与赋值扩展3.1 指数运算符的语法细节和运算优先级这可能才是真正的雷区2 ** 10等于1024这个写法取代了Math.pow(2, 10)简洁程度肉眼可见。但如果你以为它只是Math.pow的语法替换那接下来这个例子可能会让你愣一下2 ** 3 ** 2 // 512而不是64为什么不是64因为指数运算符是右结合的表达式会先计算右边的3 ** 2得到9再计算2 ** 9得到512。这一点和数学上的习惯一致但写代码的时候很容易凭直觉认为它应该从左往右算。类似的优先级问题还有很多官方文档里明确写了指数运算符的优先级要高于乘法但低于一元运算符2 * 3 ** 2 // 18先算 3 ** 2 得9再乘2 -2 ** 2 // 报错SyntaxError (-2) ** 2 // 4必须加括号-2 ** 2报错这事尤其需要警惕。因为一元负号的优先级比指数运算符高规范里不允许直接把-和**这样结合强制要求你写括号。我记得有个同事在算法题里写坐标映射的时候踩过这个坑编译直接红一片排查了好一会儿才反应过来是优先级的问题。另外**运算符也支持对应的赋值写法**比如循环里做逐步衰减let scale 2 scale ** 3 // scale 83.2 从Math.pow到**实际项目中到底省了什么光说语法简洁可能还不够直观给你看一个实际的例子。假设你在做一个复利计算器年利率是5%本金10000元存10年原来的写法是const total 10000 * Math.pow(1 0.05, 10)换成指数运算符之后const total 10000 * (1 0.05) ** 10后者读起来几乎和数学表达式一模一样。再比如写判断两个坐标点距离的代码时const distance Math.sqrt((x2 - x1) ** 2 (y2 - y1) ** 2)这种写法在可读性上的提升是实打实的写的人不用再纠结Math.pow那串冗长的函数调用看的人一眼就能抓住公式结构。还有一点容易被忽略**的性能并不比Math.pow差。现代JavaScript引擎对**做了专门的优化在某些场景下甚至比Math.pow调用更快。当然这个差距微乎其微除非你在做大量科学计算否则不用把性能当作选型依据。真正值得关心的还是代码可读性和表达力在这个维度上**毫无疑问是更优的选择。3.3 指数运算符的隐性坑位精度问题与非常规底数这里必须提一嘴浮点精度问题。很多人以为**是整数运算实际上它完全遵循IEEE 754浮点数规则。你在控制台敲一下0.1 ** 2得到的结果是0.010000000000000002和0.01不相等。这不是**的bug而是所有浮点数运算的通病。另外要注意负数底数和分数指数的组合比如(-8) ** (1/3)理论上等于-2但在JavaScript里会得到NaN。原因是JS的指数运算本质上是通过对数函数实现的而负数取对数在实数范围内没有定义。如果你需要计算这类分数次幂建议还是绕回Math.pow或者自己实现特殊逻辑。这些边界情况文档里不写但实际写代码总会撞上提前记住能省不少排查时间。4. 实操过程与典型场景落地4.1 权限角色判断includes让代码从“绕弯子”回归“说人话”权限判断是includes最典型的应用场景。假设登录用户的角色存在一个数组里现在要判断它是否有管理员权限老写法是const role editor const roles [admin, editor, visitor] if (roles.indexOf(role) ! -1) { // 有权限 }换成includes之后const role editor const roles [admin, editor, visitor] if (roles.includes(role)) { // 有权限 }这两段代码逻辑一模一样但第二段的意图表述明显更接近自然语言。如果一个项目里有几十处权限判断全部改用includes之后整体代码的可维护性会提升一个档次。同理黑名单过滤、搜索关键词匹配、表单选项校验只要是“数组中是否包含某个值”的判断都可以无脑用includes。不过这里有个性能细节值得说一下。如果数组规模很小比如角色就三五个includes和Set的性能差异是可以忽略不计的。但如果你要在一个几千上万条记录的数组里做高频查重老老实实先转成Set再用has()Set.prototype.has的时间复杂度是O(1)而includes本质是线性遍历O(n)。在我自己做的数据清洗工具里用Set重构后处理两万元素去重从40毫秒降到了不到5毫秒这个优化效果是很直观的。4.2 表驱动与状态机指数运算符在业务代码里的巧用**写业务场景似乎没includes那么频繁但有一个我非常喜欢的应用方式是表驱动计算。比如做游戏数值系统人物的伤害减免公式是“基础伤害的n次方衰减”你可以这样写const baseDamage 1000 const debuffLevel 3 const finalDamage baseDamage * 0.85 ** debuffLevel再比如设计一个“卡片升级费用”系统费用随着等级的提升以指数增长const upgradeCost level Math.floor(100 * 1.2 ** level)这类公式用**表达最为清晰而且改参数非常方便想调整成长曲线只需要改底数和系数。如果你在写图表坐标轴刻度、声音频率计算、颜色亮度映射这类偏“数学”的逻辑**也比一串串Math.pow嵌套看起来清爽得多。4.3 工程化落地实践Babel插件、TypeScript配置与Node环境虽然ES2016已经很老了现在的主流浏览器和Node.js版本基本都原生支持这两个特性但如果你还要兼容IE或老版本安卓WebView那该做的编译和polyfill工作一样都少不了。拿Babel来说最省事的方案是使用babel/preset-env配合useBuiltInsnpm install --save-dev babel/preset-env npm install --save core-js然后在.babelrc里这样配置{ presets: [ [ babel/preset-env, { useBuiltIns: usage, corejs: 3, targets: { ie: 11 } } ] ] }useBuiltIns: usage会让Babel按需引入polyfill只把你代码里用到但目标环境不支持的API给打进包。如果你不想引入整个core-js针对includes这种量级的小特性也可以自己写个几行的polyfill照着第2.2节那个版本改造就行。指数运算符**是语法层面的东西没法用polyfill垫出来只能做语法转换这个工作babel/plugin-transform-exponentiation-operator插件就能完成它会自动把x ** y转换成Math.pow(x, y)。TypeScript项目也要注意lib配置项。如果你的tsconfig.json里target设的是es5而lib里没有手动加es2016或更高版本编译器可能会对includes报类型错误。我在一个老项目里就遇到过这个问题明明运行环境支持但编译报错找不到includes定义后来在compilerOptions.lib里加上es2016才消停。4.4 一份可以直接抄的ES2016新特性知识速查表这里整理一份速查表方便你在团队内部做分享或者自己复习的时候快速对照特性示例等价替代方案注意事项Array.prototype.includes[1, 2, NaN].includes(NaN)indexOf无法检测NaN引用类型只比较引用地址不按内容比较includes 的 fromIndex[1, 2, 3].includes(3, -1)无负数从末尾倒数大于等于数组长度则直接false指数运算符2 ** 10Math.pow(2, 10)右结合2 ** 3 ** 2得512指数赋值运算符x ** 2x Math.pow(x, 2)注意负底数需要加括号如(-2) ** 2浮点精度0.1 ** 2无浮点运算有精度误差比较时需用epsilon综合下来ES2016这两个特性的学习成本约等于零但收益是长期且稳定的。5. 兼容性分析与多版本类比5.1 现存JavaScript引擎与运行时环境兼容速查先看一组兼容性数据。我基于长期维护老项目的经验整理了下面这几个常见环境的支持情况环境最低支持版本Chrome / Edge52 完全支持Firefox48 完全支持Safari10 完全支持Node.js7.0.0 完全支持6.x需要开启harmony标志IE11不支持需要polyfill与Babel转换这个兼容性表在2025年看已经是“全员绿灯”的状态了。Node.js从7.0.0开始就原生支持includes和**如今主流的16、18、20版本更是完全没有任何兼容压力。唯一需要上心的是老旧的浏览器内核比如某些银行客户用的旧版IE或老式安卓TV设备这些场景跑线上代码就必须打包编译不能指望原生支持。5.2 不同语言版本更新节奏的横向对比ES2016 vs JDK 8 vs C新标准最后聊点有意思的横向对比。你在搜索ES2016新特性的时候很可能同时刷到“JDK8新特性”“C新特性”这类热词。这三者虽然都是编程语言的版本迭代但节奏和风格差异很明显。JDK 8在2014年发布引入了Lambda表达式、Stream API、Optional这些革命性内容它对Java社区的影响和ES2015对JavaScript社区的影响是一回事都属于“憋大招型”的版本。而C11更是把现代C的语法树重新犁了一遍自动类型推导、右值引用、智能指针每一刀都砍在要害上。ES2016和它们相比本质上是完全不同的演进哲学Java和C倾向于一次性发布一个包含大量互相关联特性的重量级版本让开发者花一整年去消化而JavaScript从ES2016开始则认准了“每年少量可落地的特性”这条路。你不能说哪个更好只能说不同语言的生命周期和社区形态决定了不同的发布策略。JavaScript拥有世界上最庞大的开发者基数如果每年都搞一次JDK8级别的发布那TC39委员会和Babel生态都会不堪重负下游框架的适配也会永远追不上。理解了这层背景再回头看ES2016这两个“小特性”心态就会平和很多。它不是无所谓的一次例行更新而是JavaScript语言从“大版本焦虑”走向“稳定输出”的转折点。之后的ES2017、ES2018、ES2019之所以能保持稳定的发布频率很多流程经验都是从ES2016这次尝试里积累出来的。5.3 项目升级决策建议能直接用就直接用别为了兼容委屈自己在实际项目里做技术选型时我的建议是除非你的目标用户群体明确要求兼容IE11或更老的WebView否则直接使用includes和**没问题。理由有两个一是转译配置本身就轻量Babel两步就搞定没必要为了规避两个特性给自己立一堆“不写新语法”的规矩二是现在的代码审查工具和工程化流水线已经非常成熟ESLint的env配置里加上es2021再配一套ecmaVersion写起来根本不会有任何心理负担。我见过一些团队非常谨慎升级target字段时把es2016、es2017全列上但代码里还在用indexOf ! -1的写法纯粹是自己给自己找麻烦。技术的价值在于提高表达效率既然标准已经定了这么多年兼容性也早就不是问题那就放心用、大胆写。6. 常见问题与避坑心得6.1 最容易踩进去的五个隐藏陷阱第一个坑是includes的fromIndex传负数时好多人的第一直觉以为是“从倒数第n个元素开始到倒数第1个”结果实际行为是“从倒数第n个元素的位置向右查到数组末尾”。所以[1, 2, 3].includes(3, -1)是true因为-1定位到索引2向右查到了3。但如果你想“从倒数第n个元素向左查”includes做不到得自己用slice手动处理。第二个坑是对象数组前面已经说过includes按引用比较相同内容的对象查不到。出于保险凡是遇到对象数组的存在性判断直接写some不要犹豫。第三个坑是**和一元负号连用会直接甩语法错误比如-3 ** 2这是规范层面的禁止不是运行时行为。如果你要表达“负三的平方”必须写(-3) ** 2如果你要表达“负的三的平方”那就写-(3 ** 2)。第四个坑是假装includes能解决所有“判断存在”的需求。对于字符串虽然String.prototype.includes也存在但它的语义是“子串包含”而不是“字符序列精确匹配”。写hello.includes(ell)会返回true如果你需要做的是“字符列表里是否包含某个字符”记得用数组而不是字符串。第五个坑和性能有关。在大数组上高频使用includes会导致明显的卡顿尤其是数据清洗和去重场景。解决办法很简单用Set。下面给一个对比示例// 慢3万条数据里查1000个元素 const arr Array.from({ length: 30000 }, (_, i) i) const targets Array.from({ length: 1000 }, (_, i) i * 2) targets.filter(t arr.includes(t)).length // 很慢 // 快先转Set再has const set new Set(arr) targets.filter(t set.has(t)).length // 快到起飞复杂度从O(n*m)降到了O(nm)数据量越大差距越夸张。这个优化技巧通用于所有使用includes做高频查询的场合。6.2 一份可以直接背下来的常见问题排查表现象可能原因解决方案includes查找NaN返回false数组里根本没有NaN或你误用了indexOf确认数据源改用includes它支持NaN判断对象数组用includes查不到目标引用地址不同改用some(item item.id target.id)-2 ** 2编译报错一元负号与指数运算符的优先级冲突加括号(-2) ** 20.1 ** 2 ! 0.01IEEE 754浮点精度问题用Math.abs(a - b) Number.EPSILON比较大数组上includes明显卡顿线性查找复杂度O(n)改用Set加has()低版本浏览器报includes is not a function缺少polyfilluseBuiltIns: usage自动垫入core-jsTypeScript报找不到includes方法lib配置不够在tsconfig.json的lib里加es20166.3 一个真实案例复盘数据清洗工具中的性能从40ms优化到5ms我之前写过一个处理CSV数据的内部工具里面有一段代码是过滤掉已经处理过的记录ID。刚开始图省事用一个数组存历史ID然后history.includes(currentId)做判断。数据量小的时候完全没问题后来用户导入的CSV文件越来越大ID记录到了两万条以后整个页面开始有明显的卡顿感每次过滤要40毫秒上下。后来我用Set重构const historySet new Set(historyIds) const remaining allRecords.filter(record !historySet.has(record.id))同样的数据量耗时直接掉到5毫秒以内。这个案例完美说明了includes的适用边界低频、小数组随便用高频、大数组先转Set再操作。这也算是我自己在实践中反复踩过之后总结出来的一个通用原则放到任何“数组里查值”的场景都成立。7. 写在最后的实操心得ES2016的新特性虽然只有两个但背后透露出来的思路很值得借鉴“少而精”地推进语言演进每一点改进都解决真实开发痛点不追求大而全的轰动效应。这种设计哲学也影响了我的编码习惯在项目里写工具函数时优先考虑“一个函数只把一件事做好”如果现有的API已经能覆盖需求就不额外封装一层多余的抽象。我个人在实际开发中最受益的一点是includes让我重新审视了大量的条件判断逻辑。以前写“判断是对的还是错的”总喜欢用! -1这种处处透着别扭的方式现在includes就像一个专门为“判断存在性”设计的开关代码读起来明快多了。至于指数运算符虽然日常业务代码里用得不如includes频繁但在算法题、数据处理、图表计算这些小角落里它总能在关键时刻让公式表达变得干净利落。最后再分享一个小技巧如果你的团队在代码规范上还没有对ES2016这两个特性做明确要求可以拉一次代码扫描把仓库里所有indexOf ! -1的写法搜出来逐个评估能否换成includes。通常一次重构能让很多模块的可读性上一个台阶而这个改造的成本几乎为零。ES2016或许不是最闪耀的一次JavaScript版本更新但它绝对是最适合作为“性能/可读性双收益改造”切入点的版本。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询