ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

shim与polyfill区别详解:前端浏览器兼容的核心概念与实战避坑

2026/10/2 15:09:27 拓冰建站 浏览量
shim与polyfill区别详解:前端浏览器兼容的核心概念与实战避坑 1. 概念拆解先把 shim 和 polyfill 的底层逻辑理清楚前端开发干久了几乎每个人都会遇到这样的场景你写的Promise.all、Array.prototype.includes在 Chrome 里跑得飞起结果 QA 拿了一台老掉牙的 iOS 机型一测页面直接白屏。然后你去翻代码发现项目里引了一堆不知道该叫什么的兼容库有人管它叫 shim有人管它叫 polyfill还有人两个词混着用。面试的时候被问到“shim 和 polyfill 有什么区别”脑子里的概念其实也一直是模糊的。这事儿我琢磨了很久也踩过不少坑今天把这些经验掰开揉碎了说清楚。1.1 shim 到底是什么shim 这个词直译过来是“垫片”意思就是在两块东西之间塞一个填充物让它们能配合到一起。在前端语境下shim 是一个很宽泛的概念只要你的代码在某个环境中缺少某个能力你写一段额外的代码把这个能力补上让程序不至于崩掉这段额外代码在广义上都可以叫 shim。举个例子。在 IE8 时代console.log是不存在的准确说只有开了开发者工具才有console对象而且 IE8 下console.log是 undefined 的你的代码里有一堆调试日志用户在 IE8 上打开页面直接抛Script error。那时候的解决方案就是写一个简易的 console shimif (!window.console) { window.console { log: function() {}, warn: function() {}, error: function() {}, info: function() {} }; }这段代码干了什么它给环境补了一个不存在的能力。它没有去改变任何现有 API 的行为也没有遵循某个特定的标准接口它就是纯粹地“补缺”。这种应用的场景还包括给旧浏览器补上JSON.stringify、补上requestAnimationFrame、补上XMLHttpRequest的封装等等。核心点在于shim 解决的是“这个环境里根本没有这个能力”的问题。1.2 polyfill 又是什么polyfill 这个概念是 Remy Sharp 在 2010 年前后提出的原意是“用一段代码模拟标准 API 的实现让老浏览器也能用上新 API”。它的定义比 shim 要严格得多polyfill 必须遵循标准规定的 API 形态和语义。也就是说polyfill 不是拍脑袋想出一个实现就行它必须和标准规范对齐——函数名一致、参数顺序一致、返回值一致、错误状态一致。举一个最经典的例子Object.create的 polyfill当年 IE8 完全不支持这个方法if (!Object.create) { Object.create function(proto, properties) { if (typeof proto ! object typeof proto ! function) { throw new TypeError(Object prototype may only be an Object or null); } function F() {} F.prototype proto; var obj new F(); if (properties ! undefined) { Object.defineProperties(obj, properties); } if (proto null) { obj.__proto__ null; } return obj; }; }注意几个细节传入的 proto 如果是基本类型string、number标准规范要求抛TypeErrorproperties参数要调用Object.defineProperties去处理。这些细节如果不照着标准做你的 polyfill 在生产环境里就会产生微妙的 bug——比如某些框架内部依赖Object.create(null)来创建“没有原型链污染”的纯字典对象如果你的 polyfill 没有正确处理proto null的情况对象的原型链就是脏的hasOwnProperty判断会出问题。所以可以这么说polyfill 是 shim 的一种特例但它多了“必须符合标准语义”这个硬性要求。如果我们画个圈所有的 polyfill 都是 shim但反过来shim 不一定是 polyfill。这就是两者的第一层核心差异。1.3 两者的关系同族不同种把 shim 和 polyfill 放到一个坐标轴上理解会更直观。横向是“环境缺什么”纵向是“你怎么补的”。如果环境缺的恰好是一个标准 API比如 ES5 的Array.prototype.map你补了一个严格遵循规范的定义这个就是 polyfill。如果环境缺的是一个非标准能力比如旧的浏览器不支持某个厂商特有的 API或者你的业务代码需要挂一个全局调试横器你补了一个自己的实现这个只能叫 shim。还有一种常见情况你给一段“接口设计上就不是标准 API”的库做兼容层。比如项目里有一个旧的下载插件新浏览器里这个插件不工作了你写一个适配层把旧插件暴露的window.downloadFile(url)接口桥接到新的fetch的 Blob 下载上。这层桥接代码是 shim不是 polyfill因为它补的接口不是 ECMAScript 或 W3C 标准定义的。从面试官的角度来看如果你能讲出“polyfill 是 shim 的子集polyfill 的核心约束是标准语义对齐”这句话基本上概念关就过了。如果再能带一句“shim 是通用术语泛指一切为缺失能力补位的代码”那就更清楚了。2. 核心差异详解为什么搞混它们会出事2.1 判定标准是补标准还是补能力判断一段代码到底是 shim 还是 polyfill首先看它补的是什么。如果我需要补的是Array.prototype.find这个 API 在 ES6 规范里有明确要求回调函数返回第一个满足条件的元素否则返回 undefined回调接收三个参数当前值、索引、原数组thisArg可以指定上下文。你的实现如果严格按照这套来那它就是一个 polyfill。但如果我要补的是fetch这就是另一个故事了。fetch是 WHATWG 标准的一部分它的 API 形态是规范的——参数input、init、返回 Promise、Response 对象的结构……所以whatwg-fetch这个库可以说是 polyfill。可如果你为了兼容 IE9写了一个ajax函数包装 XMLHttpRequest对外暴露自己设计的签名比如ajax.get(url, success, error)这就只是一个兼容层实现是 shim不是 polyfill——因为你补的接口是自定义的不是标准规范规定的。关键点来了很多团队做浏览器兼容时把 shim 当 polyfill 用或者反过来觉得反正都是补兼容随便加。这会引发什么问题项目依赖判断。某个第三方库的文档写着“需要 Promise polyfill”你心里想的是“我加了没问题”但实际上你加的是自己写的一段“名不副实”的调研代码——接口形态对不上库内部调用的Promise.resolve(value)在你的 shim 里压根没有实现运行时直接报错。2.2 API 对齐shim 没义务polyfill 必须shim 和 polyfill 在“是否必须对齐标准 API”上的态度天差地别。shim 可以只满足项目内部的使用契约它提供的接口是“这次开发够用就行”的polyfill 则必须把标准的每个行为细节都考虑到位。举一个我在实际项目中踩过的例子。有一次我为了兼容一个老版本 Android WebView手写了一个Promise兼容方案早期做混合应用时很常见。我的实现核心大概是function Promise(executor) { this._callbacks []; // ... 简化 } Promise.prototype.then function(onFulfilled) { // 存一下回调 return this; };这段代码在同步场景下能跑但放到真实业务里全是坑then没有返回新的 Promise链式调用直接断resolve之后then回调没有异步执行标准要求微任务finally、catch、静态方法all、race一个都没有。我自以为写了一个 Promise 的兼容但实际上它只满足了我那一小块业务代码的调用方式严格来说它只能算一个 shim——因为我根本没有按照Promise/A规范去实现。后面我老老实实换成了promise-polyfill这个库代码量多了好几倍但所有依赖 Promise 的库axios、一个内部 SDK瞬间都稳定了。这个教训深刻地说明了“polyfill 必须对齐标准”的意义你写的不是“凑合能用”的代码而是在模拟一个完整的规范实现第三方代码不会因为你只实现了 30% 的规范就降低对它的要求。2.3 语义层级命名是小事行为是大事还有一点特别容易被忽略polyfill不仅要求“有那个函数”还要求“函数的行为和标准一致”。具体来说有这几个维度需要关注参数的默认值和边界行为。比如Array.from的第二个参数mapFn是可选的且如果传的不是函数必须抛 TypeError异常路径的处理。标准定义某个情况下抛什么错误、抛什么类型的错误polyfill 也必须一致。作用域和 this 的绑定。Array.prototype.includes被调用时this如果是数组对象外的其他对象标准要求怎么处理polyfill 就要怎么处理。性能特征。尽管不要求完全一致但理想的 polyfill 至少要避免明显的性能退化。这些差异不是咬文嚼字。比如早年很多Array.isArraypolyfill 是用Object.prototype.toString.call(value) [object Array]来判定的这个方法在绝大多数情况下没问题但在跨 frame 的场景下依然能正确识别。反过来如果你手写一个“判断是不是数组”的 shim用value instanceof Array在跨窗口场景下会失效因为不同 window 的 Array 构造函数不是同一个。面试官如果在这上面追问一句“你怎么保证你的实现是安全的”你有没有往这几个维度想过一眼就能看出来。3. 实操场景哪种情况该用哪种方案3.1 工具链自动补全babel/preset-env 与 core-js 的组合现在的项目很少手写 polyfill基本都是交给 Babel 来管。你的.babelrc或babel.config.js里配置了babel/preset-env然后根据browserslist配置去自动决定“哪个语法需要编译、哪个 API 需要 polyfill”。这里面有两个核心选择值得弄清楚。第一个是useBuiltIns: usage与entry的区别。usage是 Babel 分析你的代码里实际用到了哪些 API然后按需引入对应的 core-js polyfill这个方案打出来的包体积更小entry则是在入口文件里根据 browserslist 一次性引入所有可能需要的 polyfill虽然省心但体积明显偏大。我现在的做法是优先usage并且显式配置corejs: 3因为 core-js 3 对实例方法如Array.prototype.includes的 polyfill 支持比 2 完善很多。第二点是语法转换和 API polyfill 是两码事。babel/preset-env默认只做语法转换把箭头函数转 ES5、把 const 转 varAPI 层面需要 core-js 来补。把它们混为一谈是新人容易踩的坑——你看到 Babel 把代码里的async/await转成了regeneratorRuntime心想“完了这个我也得手工引”实际上 babel/plugin-transform-runtime 就帮你把 regenerator 引好了不需要你自己写。3.2 直接引入现成 polyfill 的正确姿势如果项目不走 Babel或者你需要给一个纯运行时的旧环境补能力市面上有这些常用库值得记住场景推荐方案说明ES5 / ES6 APIPromise、Array 方法、Object 方法等core-js覆盖面最广模块化引入可按需打包fetchwhatwg-fetch严格遵循 fetch 标准配合 Promise polyfill 使用IntersectionObserverintersection-observer官方标准 API 的 polyfill 实现HTML 元素兼容旧 IE 的 HTML5 标签html5shivIE8 及以下自定义兼容层自己写 shim接口可自定义不必遵循标准这里的关键姿势是“校验再覆盖”if (!window.fetch) { // 引入 whatwg-fetch } else { // 使用原生 fetch }有些同学喜欢用“强制覆盖”的方式不管支持不支持都引库进去覆盖原生实现。这很危险因为你不能保证 polyfill 和原生实现 100% 一致。在行为不一致时你用一个第三方实现去替代浏览器原生能力等于给自己埋雷。正确的做法永远是优先使用原生能力缺失时才 polyfill。3.3 经典案例手写一个 polyfill从改写旧项目说起假设我们需要兼容旧环境中的Array.prototype.includes。标准的语义是从数组的第一个元素开始线性扫描比较SameValueZeroNaN等于自身存在就返回true。手写实现可以参考if (!Array.prototype.includes) { Object.defineProperty(Array.prototype, includes, { value: function(searchElement, fromIndex) { // 处理 this 不是数组的情况 var array Object(this); var length array.length 0; if (length 0) return false; var from fromIndex undefined || fromIndex null ? 0 : Number(fromIndex); if (from 0) from Math.max(length from, 0); if (from length) return false; var currentElement; for (var i from; i length; i) { currentElement array[i]; // SameValueZero 语义NaN 等于自身 if (searchElement currentElement || (searchElement ! searchElement currentElement ! currentElement)) { return true; } } return false; }, writable: true, configurable: true, enumerable: false }); }这个实现里有几个细节值得细品用Object(this)处理“调用者不是数组”的情况标准允许在类数组对象上调用用length 0做无符号右移处理负数和超出 32 位整数范围的情况fromIndex为负数时按标准要与数组长度相加且不能小于 0NaN 判断是 SameValueZero 语义的核心x ! x和y ! y同时为 true 时说明两个都是 NaN。这比很多开源库的第一版 polyfill 都严谨得多。但注意这只是讲原理实际项目中建议直接使用 core-js 里现成的实现不要轻易在生产环境维护一个手写的 polyfill——维护成本高还容易健忘。手写一遍的意义在于理解标准细节而不是替代现成库。3.4 什么时候真的需要自己写 shimpolyfill 有标准可循那什么场景下值得自己写 shim我遇到过的典型情况有如下几类。第一类是旧浏览器支持中某种“伪标准”需求。比如老版本 Android WebView 没有Intl但产品要求日期显示为YYYY-MM-DD格式这时候你可以引入intl-polyfill也可以只写一个与全局Date相关的工具函数来格式化日期。后者是一个缓存工具功能大致是 shim 性质的。第二类是第三方接口的兼容桥接。你接了一个老项目的内部 SDK旧页面通过window.SDK.foo()调用接口新项目里你换用了new-sdk/core为了不让老页面重写业务你写一层全局的window.SDK { foo() { return newSdk.foo(); } }。这层代码本质是 shim——不是在补标准能力而是把旧的调用方式和新实现桥接起来。第三类是适配业务上自定义的全局能力。比如做一个项目要求window.APP_CONFIG在页面加载前就存在但 CDN 加载的顺序有时候不太可控于是你在入口放了一个“确保全局配置容器存在”的守卫代码window.APP_CONFIG window.APP_CONFIG || {};这也可以理解为一种非常轻量的 shim。所以你会发现shim 的场景通常比 polyfill 更贴近业务代码本身它更灵活也更碎片化不像 polyfill 那样有一个标准库可以去“收编”。4. 避坑指南shim 和 polyfill 使用中的高频陷阱4.1 坑一polyfill 时机和顺序错了等于没加很多人引了 polyfill 但页面还是报错排查下来是顺序问题。polyfill 必须在业务代码执行之前生效。如果你的入口 HTML 是这样写的script srcapp.js/script script srcpolyfill.js/script那 app.js 里如果一开始就用到了Promise而 polyfill 又是在后面才覆盖的window.Promise那报错已经发生了。正确姿势是把 polyfill 的 script 放在最前面或者通过构建工具把 polyfill 打进一个独立的 vendor 文件并优先加载。在我的一个混合应用项目里外层壳子先初始化 JS 环境内层 H5 页面代码加载时有大量的 SDK 初始化操作。我把所有测试 polyfill 的代码放到了 HTML 的head里并且保证在其他脚本之前同步执行——这里不能用async或defer因为这两个会让脚本延后执行可能错过业务代码的运行窗口。4.2 坑二把“部分的”实现当成 polyfill然后还去覆盖原生我见过一个团队在fetch兼容方案里图省事写了一个$.ajax风格的调用方式返回处理对外挂了个window.fetch的名字。这个方法只处理了 GET/POST没有处理credentials、headers、body等参数返回也不是标准Response对象只是反手一个字符串。然后更绝的操作是它们在没有检测原生fetch是否存在的情况下直接覆盖了它导致 Chrome 里也被这个“劣质 shim”接管了。后面的结果大家应该能猜得到一个内部模块正常请求时没问题一用response.ok判断状态就全部走错分支排查了半天。这就是“把 shim 当 polyfill 用”的最典型恶果。记住两点第一覆盖原生能力必须谨慎优先检测再决定是否覆盖第二如果你实现不了完整的标准语义不要试图给外部库提供“看似标准”的接口。4.3 坑三包体积失控polyfill 全量引入core-js 3 全套引入的体积大概几百 KBgzip 前对移动端影响不可忽视。尤其是低端 Android 手机上解析大体积的 JS 文件也会掉帧卡顿。我在一个老项目里就见过入口文件里直接把整个 core-js 通过import core-js引入了首包体积增加了 200 多 KB浏览器解析时间涨了几百毫秒。解决方法是按需引入。使用babel/preset-env时把useBuiltIns配成usage如果是手写入口可以按实际需要引子模块比如import core-js/es/promise; import core-js/es/array/includes; import core-js/es/object/assign;我是习惯于用 Babel 的usage来做自动按需的毕竟手写模块列表容易漏。但要注意如果项目里既有 Babel 编译代码又有直接在页面里写的原生脚本比如服务端模板中的内联 JSBabel 是分析不到它们的——这种情况就需要在入口文件里手动引入某些 polyfill 作为兜底。4.4 坑四packages 重复加载多个 polyfill 互相覆盖如果你的项目里把core-js和babel-runtime同时用起来了而它们都尝试覆盖同一个 API会不会冲突大概率不会因为它们都做了能力检测但事情也有例外。如果你引了whatwg-fetch又引了 axios 自己包携带的一个 fetch 兼容层这个兼容层做了一些自定义行为顺序不对就会出现覆盖问题。我不止一次在处理项目依赖时发现node_modules里有两个不同的 polyfill 实现同一个 API但行为有细微差异比如对网络错误抛出的 Promise reject 类型不同。这种时候业务代码表面上看没报错但总有奇怪的竞态问题。靠“哪个后加载就覆盖哪个”来猜行为早晚出事故。我的建议是项目里对同一个 API 只保留一个 polyfill 来源。常见做法是用core-js作为 ES API 的唯一下层whatwg-fetch作为网络 API 的补充两者职责划分清晰。如果必须在业务代码里手工引入其他兼容实现一定要加能力检测不要无条件覆盖if (!window.Response) { // 引入兼容代码 }4.5 坑五只盯着“浏览器环境”做兼容很多人的思维定式是 polyfill 都是为了兼容旧浏览器但实际前端不止跑在浏览器里。小程序 WebView、Electron 老版本、一些企业内部的 Chromium 定制壳子都可能缺 API。之前做一个桌面端内嵌页面Electron 老版本里居然缺ResizeObserver直接用系统内置的 Chrome 跑是有的但内嵌壳子那个版本没有。排查了半天。所以做兼容判断时不要以“浏览器厂商”为标准而要以“运行时能力”为标准。统一写法是if (ResizeObserver in window) { // 原生逻辑 } else { // 降级方案 }这也是为什么我建议团队里的兼容策略尽量做成“能力检测 按需 polyfill”的方式而不是硬编码“如果是 IE 就走 A如果是 Chrome 就走 B”。环境在不断变化但 API 的缺失是我们可以感知的。4.6 坑六忽略 polyfill 对性能的影响最后说一个容易被忽略的问题polyfill 常常用“低效”的方式实现“高效”的原生 API。比如Array.frompolyfill 的实现逻辑依赖于循环性能虽然还不至于太离谱但Object.assign的 polyfill 使用递归 遍历 key 的方式在对象特别大时会明显比原生慢。对于大数据量处理的场景性能问题会被放大。我自己见过一次离谱的情况在老 iPad 上跑一个数据处理页面里面用了Array.from把类数组转成数组然后做了大量排序和过滤操作整体耗时翻了三倍。排查后发现主要是 polyfill 的Array.from和原生版的性能差距导致。后续的策略是如果只是小数据量随便 polyfill但如果是大量循环处理需要优先保证原生实现同时还要权衡数据处理量大小。5. 自己构建一个最小化的兼容方案实操案例前面讲了不少理论这节我带大家完整走一遍“从零构建一个兼容方案”的思路这个方案可以直接复制到项目里做一个基线。5.1 第一步明确你必须要支持的环境很多项目嘴上的兼容范围是“IE10”但实际上并没有去验证过到底哪些 API 需要补。我的建议是先用browserslist定义目标环境然后把这个配置放到package.json或独立的.browserslistrc文件里ie 10 chrome 49 android 4.4 ios 9为什么要这样做因为 Babel 和 core-js 会根据这个列表自动推导出需要编译语法和补充 API 的范围你不必自己一个个去记“Android 4.4 支持什么不支持什么”。5.2 第二步配置 Babel 与 core-js附完整配置示例// babel.config.js module.exports { presets: [ [ babel/preset-env, { targets: ie 10, chrome 49, android 4.4, ios 9, useBuiltIns: usage, corejs: { version: 3, proposals: true }, modules: false } ] ] };这里说几个值得注意的点useBuiltIns: usage是让 Babel 分析代码并自动注入 polyfill比手动引 core-js 方便corejs.proposals表示是否需要支持还未完全定稿的新提案一般不建议开除非业务真的用到了modules: false意味着模块语法交给打包器webpack、rollup处理在交给 webpack 的场景下这个配置是对的。5.3 第三步对于 Babel 无法覆盖的场景手动补兜底我在实践过程中发现总存在 Babel 覆盖不到的场景动态引用、运行时拼接的字符串代码、以及项目里直接以外部引入方式执行的脚本。所以我在项目入口手动引入了额外兜底的 polyfill// polyfills/index.js在 webpack 入口里放到最前面 import core-js/es/promise; import core-js/es/array/flat-map; import whatwg-fetch; // 处理 fetch之所以用core-js/es/array/flat-map而不是全量引 core-js是因为这个 API 我的业务代码里真实使用了并且有些场景 Babel 的“usage”不会分析到例如封装的工具库。这种“自动按需 手动兜底”的策略既能控制体积又能保证兼容。5.4 第四步检验兼容方案是否真的能生效配置完之后不要直接上生产我建议在本地做一个快速检测// 在浏览器控制台或页面加载完成的日志中 var checks { Promise: typeof Promise ! undefined, fetch: typeof fetch ! undefined, includes: typeof Array.prototype.includes ! undefined, Object.assign: typeof Object.assign ! undefined }; console.table(checks);然后你用模拟老环境的工具比如 BrowserStack、SauceLabs、或者本地的 Chrome DevTools 里的“传感器/设备模拟”功能去切到目标老环境看这些能力项是否全部为 true。如果还有 false 的就去检查对应的 polyfill 是否被引入了、是否在正确的位置生效。我不想把这步简化成“只要不报错就算成功”。你的页面没有报错不代表所有 API 都对。旧环境里很多错误是“调用到某行才开始报错”的你检查到的能力项全绿色才能相对安心地放开跑业务代码。6. 面试与职业成长把兼容性知识变成你的优势6.1 同一道面试题面试官在考察什么前端面试里“讲一下 shim 和 polyfill 的区别”是一道高频题。面试官问这个问题表面上想听概念辨析但多得是想通过题目延伸看你的实际经验。所以你可以这样组织回答先说定义“shim 是一种通用的兼容垫片泛指为缺失能力补位的代码polyfill 是 shim 的特例专门针对标准 API 做模拟实现。”再对比“polyfill 约束更严格必须符合标准规范定义shim 则相对自由接口可自定义。”然后举一个实例你实际项目中用 core-js 解决了什么、为什么不用手写实现。最后延伸“polyfill 还需要注意时机、顺序、性能、不要覆盖原生实现”这些点。一个能把你从“背概念”者区分开来的细节是提到useBuiltIns配置时的实际经验另一个细节是能解释Object.createpolyfill 中proto null的特殊处理。这些细节如果没有实际操作过是不可能随口说出来的。所以平时积累项目中的兼容代码的具体行为真的比背八股文有用。6.2 前端兼容性变化趋势要不要一直关注说句实在话随着浏览器版本迭代IE 慢慢退出舞台理论上需要 polyfill 的场景已经比五年前少很多了。但我不认为这个知识点可以完全忽略。原因有二一是企业内部的系统硬软件环境复杂很多客户的机器停留在相对旧的浏览器版本这是一块长期存在的业务现实二是在小程序、WebView、Electron、甚至一些嵌入式浏览器的场景中API 缺失问题依然频繁发生只是换了环境名称而已。所以我认为不必因为“趋势”就放弃对 polyfill 的理解但也不用把兼容当成所有项目的默认前提。适合的做法是在项目立项时先明确目标环境再决定兼容策略。如果产品目标用户就是现代浏览器用户就没必要一上来就挂一大包 polyfill如果产品面向金融、政务、企业大客户那兼容方案反而是项目能否交付的关键一环。6.3 分享一个我的老项目实战记录最后分享一个我 2022 年做过的实际案例一个面向政企客户的管理后台客户要求必须能在 Windows 7 上的 IE11 环境运行内部还被一层壳子包裹导致真实的浏览器内核版本比系统看到的还要老。整个项目我干了这几件事先把browserslist配成ie 11用babel/preset-envcore-js3做 ES API 补齐入口位置手动引入whatwg-fetch和core-js/es/promise补齐 Babel usage 模式分析不到的动态场景单独抽了一个compatibility-check.js的小工具在页面加载后输出当前环境的能力报告用来测试和内部环境联调时快速定位。核心教训是兼容性不是一次性投入它需要伴随项目持续维护、反复测试。环境一变、依赖一升级之前好用的 polyfill 策略可能就会失效。所以我在项目里养成了一个习惯——每隔一段时间就查看一下browserslist的推荐数据评估当前支持的浏览器范围是否需要调整关联的 polyfill 是否真的还需要。我个人体会最深的一点是shim 和 polyfill 的区分不只是为了回答面试题更是为了在选择工具和设计策略时做到心中有数——你补的是一个“标准能力”还是一个“自定义能力”决定了你的实现要考虑多深的边界情况。大多数前端把兼容做成“能用就行”但如果你对这两个词背后的规范敬畏足够深你写的每一行兼容代码都会更稳健排查问题也会快得多。