跳到主要内容
Polyfill 解决前端兼容性:core-js 包结构与配置策略 | 极客日志
JavaScript Node.js 大前端
Polyfill 解决前端兼容性:core-js 包结构与配置策略 综述由AI生成 core-js 通过注入旧版本支持的 API 实现新特性兼容。文章介绍了 core-js 的引入方式(全量、单个 API、pure 模式)、源码结构、与 Babel 配合使用(preset-env entry/usage)、configurator 强制控制以及核心兼容性数据工具(core-js-compat/builder)。旨在帮助开发者优化打包体积并提升浏览器兼容性。
全栈工匠 发布于 2026/4/9 更新于 2026/5/23 15 浏览简介
Babel 是一个使用 AST 转译 JavaScript 语法,提高代码在浏览器兼容性的工具。但有些 ECMAScript 并不是新的语法,而是一些新对象、新方法等,这些并不能使用 AST 抽象语法树来转义。因此 Babel 利用 core-js 实现这些代码的兼容性。
core-js 是一个知名的前端工具库,里面包含了 ECMAScript 标准中提供的新对象/新方法等,而且是使用旧版本支持的语法来实现这些新的 API。这样即使浏览器没有实现标准中的新 API,也能通过注入 core-js 代码来提供对应的功能。
像这种通过注入代码实现浏览器没有提供的 API 特性,叫做 Polyfill。这个单词的本意是填充材料,在 JavaScript 领域中,这些注入的代码就类似'填充材料'一样,帮助我们提高代码的兼容性。另外 core-js 还提供了一些还在提议中的 API 的实现。
core-js 使用方式
使用前后对比
要想看到 core-js 使用前后的效果对比,首先需要确定某个特性和对应的执行环境,在这个环境中对应的特性不存在。我本地是 Node.js v18.19.1 版本,这个版本并没有实现 Promise.try 这个方法,因此我们就用这个方法进行实验。首先是没有引入 core-js 的场景:
Promise .try (()=> { console .log ('demo' )})
可以看到没有引入 core-js,直接使用 Promise.try 时,会因为没有该方法而报错。然后再试试引入 core-js 的效果:
require ('core-js' ) Promise .try (()=> { console .log ('demo' )})
可以看到引入 core-js 后,原本不存在的 API 被填充了,我们的代码可以正常执行并拿到结果了。这就是 core-js 提高兼容性的效果。
单个 API 引入
core-js 不仅可以直接引入全部语法,还可以仅引入单个 API,比如某个对象或某个方法。首先看下只引入 Promise 对象:
然后再看下直接引入对象中的某个方法:
require ('core-js/full/promise/try' ) Promise .try (()=> { console .log ('demo' )})
不注入全局对象 前面展示的场景,core-js 都是将 API 直接注入到全局,这样使用这些 API 就如环境本身支持一样,基本感受不到区别。但如果我们不希望直接注入到全局时,core-js 也提供了使用方式:
const promise =require ('core-js-pure/full/promise' ); promise.try (()=> { console .log ('demo' )}) Promise .try (()=> { console .log ('demo2' )})
可以看到,使用 core-js-pure 这个包之后,可以直接导出我们希望要的 API,而不直接注入到全局。此时直接使用全局对象方法依然报错。而 core-js 这个包虽然也能导出,但它还是会直接注入全局,我们看下例子:
const promise =require ('core-js/full/promise' ); promise.try (()=> { console .log ('demo' )}) Promise .try (()=> { console .log ('demo2' )})
因此,如果希望仅使用导出对象,还是需要使用 core-js-pure 这个包。core-js-pure 也可以仅导出对象方法:
const try2 =require ("core-js-pure/full/promise/try" ); Promise .try = try2; Promise .try (()=> { console .log ("demo!" );});
因为导出的对象方法不能独立使用,因此在例子中我们还是将其注入到 Promise 对象后使用。
特性分类引入 core-js 中包含非常多 API 特性的兼容代码,有些是已经稳定的特性,有些是还处在提议阶段的,不稳定的特性。我们直接引入 core-js 会把这些特性全部引入,但如果不需要那些不稳定特性,core-js 也提供了多种引入方式:
core-js 引入所有特性,包括早期的提议
core-js/full 等于引入 core-js
core-js/actual 包含稳定的 ES 和 Web 标准特性,以及 stage3 的特性
core-js/stable 包含稳定的 ES 和 Web 标准特性
core-js/es 包含稳定的 ES 特性
这里我们举两个例子尝试下。首先由于 ECMAScript 标准一直在更新中,有些特性现在是提议,未来可能就已经被列入正式特性了。因此这里的例子需要明确环境和 core-js 版本。这里我们使用 Node.js v18.19.1 和 [email protected] 版本,以写这篇文章的时间为准。
首先第一个特性是:数组的 lastIndex 属性,这是一个 stage1 阶段的 API,这里针对不同的引入方式进行尝试:
首先当不引入 core-js 时,因为不支持这个 API,所以输出 undefined。core-js/full 支持 stage1 阶段的 API,可以正确输出结果。但 core-js/actual 仅支持 stage3 阶段的 API,因此还是不支持这个 API。
然后我们再看下另外一个 API,数组的 groupBy 方法。这是一个 stage3 阶段的 API:
可以看到,不引入 core-js 时不支持,引入了 core-js/actual(包含 stage3 阶段的 API)后支持并能输出正确的结果。core-js/stable 中不支持又报错了。
core-js 源码结构 前面描述了很多 core-js 的引入方式,这里我们看一下源码结构,看看 core-js 内部是如何组织的。
core-js 源码目录 core-js ├─actual │ ├─array │ │ ├─at.js │ │ ├─concat.js │ │ └─... │ ├─set │ │ └─... │ └─... ├─es │ └─... ├─features │ └─... ├─index.js └─...
首先列出 core-js 源码目录的示意图,可以看到 core-js 内部有很多目录,对应前面的各种引入方式。这里我们列出每个目录的内容:
actual 包含稳定的 ES 和 Web 标准特性,以及 stage3 的特性
es 包含稳定的 ES 特性
features 没有说明,猜测和 full 类似
full 所以特性包括早期提议
internals 包内部使用的逻辑
modules 实际特性的代码实现
proposals 包含提议的特性
stable 包含稳定的 ES 和 Web 标准特性
stage 按照 stage 阶段列出提议特性
web 包含 Web 标准特性
configurator.js 是否强制引入逻辑,后面会描述
index.js 内容为导出 full 目录,因此导入 core-js 等于导入 core-js/full
层层引用 在目录中 actual, es, full, stable, es 是我们已经介绍过的。另外还有 web 目录仅包含 web 标准的特性,features 和 full 类似(index.js 中直接导出 full 目录)。
proposals 目录包含提议的特性,以特性名来命名文件名。而 stage 目录中包含 0.js, 1.js, 2.js 等等,是根据 stage 阶段来整理的,方便整理和引入对应阶段的特性。
这样整理目录虽然清晰,但这些目录中的特性都是重复的,不可能在每个目录中把特性都实现一遍。因此上面这些目录的文件中,存放的都是实现的引用,并不是特性代码实现本身。真正的实现在 modules 目录中。modules 目录中是以特性名作为命名的文件,文件有固定的前缀名:es.表示 ES 标准;esnext.表示提议中的标准;web.表示 web 标准。
这里以我们上面提到过的两个特性为例,看看引用路径,首先是 Promise.try:
使用者引入 core-js/full/promise/try.js
引入 actual/promise/try.js
引入 actual/promise/try.js
引入 stable/promise/try.js
引入 es/promise/try.js
最终引入 modules/es.promise.try.js
使用者引入 core-js/actual/array/group-by.js
最终引入 modules/esnext.array.group-by.js
可以看到,core-js 内部的特性是经过层层引入,最终引入具体的实现代码的。
core-js-pure 与 core-js-bundle 除了 core-js 之外,core-js-pure 与 core-js-bundle 这两个包也提供了兼容性。core-js-pure 内部的目录结构与 core-js 一致,只不过 core-js-pure 不将特性注入到全局。core-js-bundle 比较特殊,它是将 core-js 代码经过打包后再提供,它的结构如下:
core-js-bundle ├─index.js ├─minified.js ├─minified.js.map └─...
其中 index.js 是打包过后的特性集合代码,minified.js 是经过压缩混淆后的代码。core-js-bundle 只能全部引入并注入到全局,不能引入部分目录或者导出某个属性。
打包和浏览器效果
创建 Webpack 示例 首先创建一个 Webpack 项目,方便后续打包查看效果。首先执行:
const arr =["jz" ,"plp" ]; console .log (arr.lastIndex );
在 package.json 文件的 scripts 中增加命令:'build': 'webpack'。最后是 Webpack 配置文件 webpack.config.js:
const path =require ('path' );const HtmlWebpackPlugin =require ('html-webpack-plugin' ); module .exports ={mode :'production' ,
命令行运行 npm run build,即可使用 Webpack 打包。在 dist 目录中生成了两个文件,一个是 main.js,里面是打包后的 js 代码;index.html 可以让我们在浏览器查看效果。由于我们没有引入 core-js,浏览器没有预置 lastIndex 这个提议中的特性,因此输出 undefined。
core-js 打包 这里引入 core-js,然后打包查看效果。首先是全量引入:
require ("core-js" );const arr =["jz" ,"plp" ]; console .log (arr.lastIndex );
此时浏览器输出 1,表示 core-js 注入成功,lastIndex 特性生效了。但是我们查看 main.js,发现居然有 267KB。这是因为它把所有特性都引入了。
如果引入 require("core-js/full/array"),此时新特性也可以生效。因为只引入了数组相关特性,因此 main.js 的大小为 59.3KB,比全量引入小很多。
如果引入 require("core-js/full/array/last-index"),此时新特性也可以生效。因为只引入了这一个特性,因此 main.js 的大小为 12.2KB。
Babel 与 core-js 从前面打包的例子中可以看到,core-js 整个打包进项目中是非常巨大的,可能比你正常项目的大小还要更大。这样明显会造成占用资源更多,页面加载时间变慢等问题。一个解决办法是,只引入我们代码中使用到的特性,以及我们要适配的浏览器版本中不兼容的特性,用不到的特性不打包进代码中。Babel 就提供了这样的功能。
创建 Babel 示例 require ('core-js' );const jzplp =1 ;
在 package.json 文件的 scripts 中增加命令:'babel': 'babel src --out-dir lib'。最后是 Babel 配置文件 babel.config.json:
{ "presets" : [ [ "@babel/preset-env" , { "targets" : { "chrome" : "100" } } ] ] }
targets 中表示我们需要兼容的浏览器版本。执行 npm run babel,生成结果再 lib/index.js 中,内容如下。可以看到未对 core-js 做任何处理。
"use strict" ;require ('core-js' );const jzplp =1 ;
preset-env 配置 entry @babel/preset-env 是一个 Babel 预设,可以根据配置为代码增加兼容性处理。前面创建 Babel 示例时已经增加了这个预设,但是没有增加 core-js 配置。这里我们加一下:
{ "presets" : [ [ "@babel/preset-env" , { "targets" : { "chrome" : "100" } , "useBuiltIns" : "entry" , "corejs" : "3.47.0" } ] ] }
这里增加了 corejs 版本和 useBuiltIns 配置,值为 entry。配置这个值,会使得@babel/preset-env 根据配置的浏览器版本兼容性,选择引入哪些 core-js 中的特性。这里再执行命令行,结果如下:
"use strict" ;require ("core-js/modules/es.symbol.async-dispose.js" );require ("core-js/modules/es.symbol.dispose.js" );
可以看到 core-js 被拆开,直接引入了特性本身。在配置 chrome: 100 版本时,引入的特性为 215 个。我们修改配置 chrome: 140 版本时,再重新生成代码,此时引入的特性为 150 个。可以看到确实时根据浏览器版本选择不同的特性引入。这对于其它 core-js 的引入方式也生效:
我们引入 core-js/stable,可以看到生成代码中不引入 esnext 特性了。在配置 chrome: 100 版本时,引入的特性为 71 个,配置 chrome: 100 版本时,引入的特性为 6 个。同样的,如果引入换成 core-js/full/array,就会只引入数组相关特性,而且也是根据浏览器兼容版本引入。
preset-env 配置 usage @babel/preset-env 的 useBuiltIns 配置值为 usage 时,Babel 不仅会跟根据配置的浏览器版本兼容性,还会根据代码中实际使用的特性来选择引入哪些 core-js 中的特性。首先是 Babel 配置:
{ "presets" : [ [ "@babel/preset-env" , { "targets" : { "chrome" : "100" } , "useBuiltIns" : "usage" , "corejs" : "3.47.0" } ] ] }
然后是要处理的代码,注意配置 usage 时是不需要手动引入 core-js 的。我们配置不同的 Chrome 浏览器版本,看看输出结果如何:
首先可以看到,引入 core-js 中的特性数量变得非常少了,代码中没有用到的特性不再引入。其次不同的浏览器版本引入的特性不一样,因此还是会根据浏览器兼容性引入特性。我们再修改一下源代码试试:
可以看到,源代码中增加了 Promise.try,引入的特性也随之增加了对应的 core-js 特性引入。因此,使用@babel/preset-env 的 usage 配置,可以保证兼容性的同时,最小化引入 core-js 特性。另外这个配置并不会自动引入提议特性,如果需要则额外配置 proposals 为 true。
@babel/polyfill @babel/polyfill 是一个已经被弃用的包,推荐直接使用 core-js/stable。查看@babel/polyfill 源码,发现他就是引入了 core-js 特性与 regenerator-runtime 这个包。regenerator-runtime 也是一个兼容性相关的包,可以帮助添加 generatore 和 async/await 相关语法。作为替代可以这样引入:
import 'core-js/stable' ;import 'regenerator-runtime/runtime' ;
@babel/runtime @babel/runtime 就像自动引入版的 core-js-pure。它还是根据代码实际使用的特性来注入 core-js 特性,但它不注入到全局,而是引入这些 API 再调用。这里我们使用@babel/plugin-transform-runtime 插件,里面包含了@babel/runtime 相关逻辑。首先看下 Babel 配置:
{ "plugins" : [ [ "@babel/plugin-transform-runtime" , { "corejs" : 3 } ] ] }
可以看到,虽然没有直接引入 core-js-pure,但效果是一样的。打开@babel/runtime-corejs3 这个包查看,里面实际上就是导出了 core-js-pure 中的特性。例如:
core-js/configurator 强制控制 如果希望在正常引入 core-js 时,对于部分特殊属性进行引入或者不引入的控制,就需要用到 core-js/configurator。这个工具可以配置三种选项:
useNative: 当环境中有这个特性时不引入,当确定没有时才引入
usePolyfill: 明确引入这个特性
useFeatureDetection: 默认行为,和不使用 core-js/configurator 一致
useNative 不引入 首先试试不引入特性,这里我们使用 Promise 这个特性为例。首先是不引入 core-js 的效果,可以看到全局 Promise 对象被我们改掉了。
const jzplp ={}; Promise = jzplp; console .log (Promise , Promise === jzplp);
然后在中间引入 core-js 试试。可以看到我们改掉的 Promise,被 core-js 给改回去了。
const jzplp ={}; Promise = jzplp;require ("core-js/actual" ); console .log (Promise , Promise === jzplp);
这时候,如果不希望 core-js 改掉我们自定义的 Promise,可以利用 useNative 配置,强制 core-js 不引入这个特性。看结果 core-js 引入之后,我们自定义的 Promise 依然存在。
const configurator =require ("core-js/configurator" );configurator ({useNative :["Promise" ],});const jzplp ={}; Promise = jzplp;require ("core-js/actual" ); console .log (Promise , Promise === jzplp);
usePolyfill 强制引入 想要验证 usePolyfill 的效果,需要找一个环境中本来存在的特性,core-js 即使引入也不会修改的特性。Promise 不行,因为 core-js 引入时会对这个 Promise 增加子特性。Promise.try 也不行,因为原来环境中不存在。这里试一下 Promise.any,这是环境中本来就存在的特性:
console .log (Promise .any );constjzplp=()=> {}; Promise .any = jzplp; console .log (Promise .any , Promise .any === jzplp);
可以看到,Promise.any 原来就存在,但是被我们修改成了新函数。再引入 core-js 试试:
console .log (Promise .any );constjzplp=()=> {}; Promise .any = jzplp;require ('core-js' ); console .log (Promise .any , Promise .any === jzplp);
引入了 core-js 之后,结果没有变化。这说明 core-js 并不会修改我们自定义的函数。这时候就可以试一下 usePolyfill 的效果了:
const configurator =require ("core-js/configurator" );configurator ({usePolyfill :["Promise.any" ],}); console .log (Promise .any );constjzplp=()=> {}; Promise .any = jzplp;require ('core-js' ); console .log (Promise .any , Promise .any === jzplp);
可以看到,Promise.any 又被改为了真正起效果的函数,这说明 usePolyfill 的强制引入特性是有效的。
core-js 中的特性选择 前面我们体验了 Babel 根据浏览器兼容性,选择不同的 core-js 特性引入,那么不同浏览器兼容哪些特性的数据是从哪里获取呢?core-js 本身就提供了这个功能。
core-js-compat core-js-compat 提供了不同浏览器对应特性的兼容性数据。它有好几个参数,这里先列举一下含义:
targets: Browserslist 格式的浏览器兼容配置
modules: 需要设置兼容性配置的模块,可以是 core-js/full,也可以是某个特性,甚至是正则
exclude: 需要排除的模块
version: 使用的 core-js 版本
inverse: 反向输出,即输出不需要兼容的特性列表
const compat =require ("core-js-compat" );const data =compat ({targets :"> 10%" ,modules :["core-js/actual" ],version :"3.47" ,}); console .log (data);
compat 会根据我们设置的浏览器兼容性配置,输出特性列表,包含两个字段:list 是一个特性名称列表;targets 是一个 Map 结构,key 为特性名,值为可以兼容的浏览器。假设我们把上面的 targets 改成 > 50%,此时会输出空值:
const compat =require ("core-js-compat" );const data =compat ({targets :"> 50%" ,modules :["core-js/actual" ],version :"3.47" ,}); console .log (data);
我们增加 exclude,排除部分属性,可以看到特性数量大大减少:
const compat =require ("core-js-compat" );const data =compat ({targets :"> 10%" ,modules :["core-js/actual" ],exclude :["esnext" ],version :"3.47" ,}); console .log (data);
const compat =require ("core-js-compat" );const data =compat ({targets :"> 10%" ,modules :["core-js/actual" ],version :"3.47" ,inverse :true }); console .log (data);
因为输出的是不需要引入 core-js 兼容的特性,所以特性数量非常多,而且 targets 中没有列出支持的浏览器版本。
core-js-builder 前面介绍的 core-js-compat 是接收参数之后,输出 core-js 的特性列表数组。而 core-js-builder 接收类似的参数,直接输出引用 core-js 的代码。我们首先列举一下参数:
targets: Browserslist 格式的浏览器兼容配置
modules: 需要设置兼容性配置的模块,可以是 core-js/full,也可以是某个特性,甚至是正则
exclude: 需要排除的模块
format: 'bundle'输出打包后的源码;'cjs'和'esm'输出对应格式的引用代码
filename: 输出的文件名
const builder =require ("core-js-builder" );asyncfunctionfunJzplp ( ){const data =awaitbuilder ({targets :"> 30%" ,modules :["core-js/actual" ],format :'bundle' ,}); console .log (data);}funJzplp ();
可以看到,builder 函数输出了非常长的代码,内容实际为输出的特性经过打包之后的结果代码。再试一下'cjs'和'esm',输出的是对应木块的引用代码:
如果设置了 filename,core-js-builder 会创建该名称的文件,并将代码写入到文件中。
总结 这篇文章描述了 core-js 相关包的代码内容和使用方式。core-js 实际上就是提供了 JavaScript 中一些 API 特性的兼容实现方式。它与实现语法兼容的 Babel 一起,可以做到大部分 JavaScript 的兼容性。当然 core-js 和 Babel 也不是万能的,它们都有各自无法转义和兼容的语法和特性。
参考 相关免费在线工具 Keycode 信息 查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
Escape 与 Native 编解码 JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
JavaScript / HTML 格式化 使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
JavaScript 压缩与混淆 Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
Base64 字符串编码/解码 将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
Base64 文件转换器 将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online