中频
1. 请阐述CommonJS和ESModule的区别、ESModule的优势,以及静态分析的用途
八股链接
之前的高频里有讨论过ES6里新增的模块化,这里再详细讨论一下
「万字进阶」深入浅出 Commonjs 和 Es Module
首先,CommonJS 和 ESModule 的出现,都是为了解决 JavaScript 早期缺乏模块化机制带来的两个核心问题:变量的全局污染 和 依赖关系难以管理。
CommonJS 是 Node.js 早期采用的模块规范。它的特点是同步加载模块,通过 require() 导入,通过 module.exports 或 exports 导出。由于 Node.js 运行在服务端,模块文件都存储在本地磁盘,读取速度快,同步加载不会造成明显性能问题,所以非常适合服务器环境。
而 ESModule 是 ES6(ES2015)正式引入的官方模块标准。它支持静态导入(import ... from)和动态导入(import(),返回一个 Promise),模块可以异步加载,不会阻塞浏览器的主线程,因此更适用于网络环境下的前端开发。
现代开发中,我们更推荐使用 ESModule,主要原因有两点:
第一,它支持静态分析——也就是说,在代码运行之前,工具就能分析出模块之间的依赖关系、哪些变量被导出、哪些函数从未被使用。这为 Tree-shaking 提供了基础,像 Webpack、Vite、Rollup 这类打包工具可以自动“摇掉”未使用的代码,显著减小打包体积。
第二,静态分析还能提升开发体验。比如编辑器(VSCode)可以提前给出更准确的代码提示、类型检查、自动补全,甚至在编译阶段就发现拼写错误或未定义变量,提高开发效率和代码质量。
所以,虽然 CommonJS 在 Node.js 中仍有广泛使用(尤其在旧项目中),但 ESModule 凭借其静态结构、更好的优化能力和对现代工具链的支持,已经成为前端开发的事实标准。
2. JavaScript中null和undefined的区别?
八股链接
null 和 undefined 都可以表示‘无值’或‘空’的状态,但它们的语义和使用场景有所不同。
undefined 的含义:
- 表示一个变量已声明但尚未赋值,或者一个对象属性不存在,或者函数没有返回值。
- 它是 JavaScript 引擎在变量初始化过程中默认赋予的值,代表‘未定义’或‘缺失值’。
- 例如:
let x; console.log(x); // undefined
null 的含义:
- 表示一个有意设置的‘空值’或‘无对象’引用。
- 它通常用于一个变量将来要保存对象引用,但目前还没有合适的对象,于是程序员主动赋值为
null,表示‘这里本该有东西,但现在为空’。
- 所以
null 是一种人为的、明确的空状态,而 undefined 更像是系统默认的‘未初始化’状态。
关键区别示例(来自 MDN):
typeof undefined; // "undefined"
typeof null; // "object" (历史遗留 bug,应为 "null")
null === undefined; // false (类型不同)
null == undefined; // true (在 == 下被认为是‘相等的无值’)
null === null; // true
!null; // true (null 被视为 falsy 值)
Number.isNaN(1 + null); // false (null 转为 0)
Number.isNaN(1 + undefined); // true (undefined 转为 NaN
总结来说:undefined 是系统默认的‘未赋值’,而 null 是程序员主动设置的‘空值’。在实际开发中,我们通常用 === 来严格区分它们,避免类型转换带来的意外。
3. Javascript ==和===有什么不同?
八股链接
JavaScript 基础系列之 == 和 === 的区别,理解隐式类型转换步骤
MDN-==
MDN-===
==会先进行类型转换再判断
===只有在类型和值都相等时,两边才相等
4. Map和Object的区别
八股链接
JS 项目中究竟应该使用 Object 还是 Map?| 项目复盘
Object的key只能是string、number和Symbol类型;Map的key可以是任何类型
Map的存储顺序保持插入时的顺序,Obejct按插入顺序,但是顺序是可预测的;
在存储大量数据并操作时,使用map性能更优且操作更丰富
在JSON传输时,JSON默认支持Object;Map需要序列化反序列化
遍历时,Object可以使用for...in,但是会遍历到原型链上的属性;Map可以使用for...of
const obj = { a: 1, b: 2 };
for (const key in obj) {
console.log(key, obj[key]);
}
const map = new Map([['a', 1], ['b', 2]]);
for (const [key, value] of map) {
console.log(key, value);
}
5. for…in 和for…of有什么区别?
八股链接
for...in 和 for...of有什么区别
MDN-for...in
MDN-for...of
for-in遍历可枚举属性,包括原型链上的属性,遍历对象的键
for-of遍历可迭代属性,遍历对象的值
6. 请说明Proxy和Object.defineProperty的区别
八股链接
MDN-Object.defineProperty()
MDN-Proxy
二者都是为了为了实现对对象行为的自定义控制,为对象提供一层外部代理
在操作粒度上,defineProperty只能针对单个属性进行定义和修改;Proxy可以为整个对象创建代理,并拦截一些操作并执行自定义操作
在功能范围上,defineProperty主要控制属性的描述符,比如是否可写(writable)、是否可枚举(enumerable)、是否可配置(configurable),以及设置getter和setter方法;Proxy提供了更广泛的陷阱(traps),可以拦截多种操作类型,比如基本的get和set用于拦截读取和修改操作、has用于拦截in操作等
7. 对象继承的方式有哪些?
在之前学习对象继承的原理时,没有具体去学习继承的方式,这里记录一下
八股链接
- 原型链继承:通过让子类原型指向父类实例,通过原型链实现属性和方法的继承,但是存在引用类型共享的问题,并且不能向父类构造函数传参,使用构造函数继承可以解决这两个问题
function Parent() {
this.name = 'Parent';
}
Parent.prototype.sayHello = function () {
console.log("Hello from Parent");
}
function Child() {
this.type = 'Child';
}
//让子类原型指向父类实例
Child.prototype = new Parent();
//修正构造函数指向
Child.prototype.constructor = Child;
const child = new Child();
child.sayHello();
- 构造函数继承:在子类的构造函数中调用父类的构造函数,并使用call或apply改变this指向,将父类的属性和方法复制到子类实例上,这样子可以解决引用类型共享的问题,并且可以向父类构造函数传参,但是缺点是不能继承父类原型上的属性和方法
function Parent(name) {
this.name = name;
this.hobbies = ["reading", "music"];
}
function Child(name, age) {
//在子类的构造函数中调用父类构造函数并使用call改变this指向
Parent.call(this, name);
this.age = age;
}
const child1 = new Child("Alice", 10);
const child2 = new Child("Bob", 20);
child1.hobbies.push('game');
console.log("child1:", child1);
console.log("child2:", child2);
- 组合式继承:结合原型链继承和构造函数式继承的优点,缺点是父类构造函数要调用两次
function Parent(name) {
this.name = name;
this.hobbies = ["reading", "music"];
}
function Child(name, age) {
//在子类的构造函数中调用父类构造函数并使用call改变this指向
Parent.call(this, name);
this.age = age;
}
const child1 = new Child("Alice", 10);
const child2 = new Child("Bob", 20);
child1.hobbies.push('game');
console.log("child1:", child1);
console.log("child2:", child2);
- 寄生式组合继承:使用Object.create创建一个父类原型的副本,解决两次调用父类构造函数的问题
function inheritPrototype(Child, Parent) {
const prototype = Object.create(Parent.prototype); // 创建父类原型的副本
prototype.constructor = Child;
Child.prototype = prototype;
}
function Parent(name) {
this.name = name;
this.hobbies = ['reading', 'music'];
}
Parent.prototype.sayHello = function () {
console.log(`Hello, I'm ${this.name}`);
};
function Child(name, age) {
Parent.call(this, name); // 构造函数继承
this.age = age;
}
inheritPrototype(Child, Parent); // 关键:寄生组合
Child.prototype.showAge = function () {
console.log(`I'm ${this.age} years old`);
};
const child = new Child("Alice", 10)
console.log(child);
child.sayHello();
child.showAge();
- class:ES6引入的语法糖
class Parent {
constructor(name) {
this.name = name;
}
sayHello() {
console.log(`Hello, I'm ${this.name}`);
}
}
class Child extends Parent {
constructor(name, age) {
super(name); // 调用父类构造函数
this.age = age;
}
showAge() {
console.log(`I'm ${this.age} years old`);
}
}
const child = new Child('Alice', 10);
child.sayHello(); // Hello, I'm Alice
8. 请列举数组去重的一些方法
八股链接
- 使用set
性能最好,代码最简洁,适用于基本类型去重。
const arr = [1, 2, 2, 3, 3, 4, 5, 5, 5, 6];
const uniqueArr = [...new Set(arr)];
console.log(uniqueArr);
- 使用filter+indexOf
//filter + indexOf
const arr = [1, 2, 2, 3, 3, 4, 5, 5, 5, 6];
const uniqueArr = arr.filter((item, index) => {
//判断元素当前的位置和它第一次出现的位置是否相同,是则保留
return arr.indexOf(item) === index;
})
console.log(uniqueArr);
- 使用reduce+includes
//reduce + includes
const arr = [1, 2, 2, 3, 3, 4, 5, 5, 5, 6];
const uniqueArr = arr.reduce((acc, current) => {
//判断当前累加器中是否有当前遍历的元素,如果不包含,则添加到累加器
if (!acc.includes(current)) {
acc.push(current);
}
return acc;
}, []);
console.log(uniqueArr);
2和3的方案内部都是遍历,时间复杂度高
- 使用map(适合手动设计去重逻辑)
const arr = [1, 2, 2, 3, 3, 4, 5, 5, 5, 6];
const uniqueArr = [...new Set(arr)];
console.log(uniqueArr);
9. 手写 bind 函数
八股链接
直接在这里记录(bind call apply)的全部手写了
参考带你手写实现call、bind、apply!有详细思路!【前端必备技巧】
/**
* 自定义实现Function.prototype.call 方法
* 允许以指定的this上下文和参数调用函数
* @param {Object} context - 需要绑定的this上下文对象
* @param {...any} args - 传递给函数的参数列表
* @returns {*}
*/
//在Function原型上添加方法
Function.prototype.myCall = function (context, ...args) {
//检查调用者是否为函数
if (typeof this !== 'function') {
throw new TypeError("调用者必须是函数");
}
//设置上下文对象为传入值,默认为globalThis
context = context || globalThis;
//使用Symbol创建唯一的键,防止覆盖context上已有属性
let fn = Symbol("key");
//临时添加一个属性,值为调用myCall的函数
context[fn] = this;
//调用该函数并传入参数,使用result接收结
const result = context[fn](...args);
//删除临时属性
delete context[fn];
//返回函数执行结果
return result;
}
//测试
const test = {
name: "xxx",
hello: function () {
console.log(`hello,${this.name}!`);
},
add: function (a, b) {
return a + b;
},
};
const obj = { name: "world" };
test.hello.myCall(obj); //hello,world!
test.hello.call(obj);//hello,world!
console.log(test.add.myCall(null, 1, 2));//3
console.log(test.add.call(null, 1, 2));//3
/**
* 自定义实现 Function.prototype.apply 方法
* 允许以指定的 this 上下文和参数数组调用函数
* @param {Object} context - 需要绑定的 this 上下文对象
* @param {Array} args - 传递给函数的参数数组
* @returns {*} 函数执行结果
*/
Function.prototype.myApply = function (context, args) {
// 检查调用者是否为函数
if (typeof this !== 'function') {
throw new TypeError("调用者必须为函数");
}
// 如果 args 存在但不是数组,则抛出错误
//遇到undefined和null不会报错,在下方设置成默认空数组处理
if (args && !Array.isArray(args)) {
throw new TypeError("第二个参数必须为数组");
}
context = context || globalThis;
//创建Symbol作为唯一键
const fn = Symbol("key");
//添加临时属性,值为调用该方法的函数
context[fn] = this;
//调用该函数,传入参数,如果为undefined或null,使用默认空数组
const result = context[fn](...(args || []));
//删除临时属性
delete context[fn]
//返回调用结果
return result;
}
const test = {
name: "xxx",
hello: function () {
console.log(`hello,${this.name}!`);
},
};
const obj = { name: "world" };
test.hello.myApply(obj); //hello,world!
test.hello.apply(obj); //hello,world!
const arr = [2, 3, 6, 5, 1, 7, 9, 5, 0]
console.log(Math.max.myApply(null, arr));//9
console.log(Math.max.apply(null, arr));//9
/**
* 自定义实现 Function.prototype.bind 方法
* 返回一个新的函数,永久绑定指定的 this 上下文和部分参数
* @param {Object} context - 需要绑定的 this 上下文对象
* @param {...any} args - 预设的参数列表
* @returns {Function} 绑定后的新函数
*/
Function.prototype.myBind = function (context, ...args) {
// 检查调用者是否为函数
if (typeof this !== 'function') {
throw new TypeError("调用者必须为函数");
}
context = context || globalThis;
// 保存原函数引用
const _this = this;
return function fn(...innerArgs) {
// 如果作为构造函数调用(new),this instanceof fn 为 true
if (this instanceof fn) {
// 用 new 调用原函数,参数为预设参数和新传入参数
return new _this(...args, ...innerArgs);
}
// 普通函数调用,绑定 context
return _this.apply(context, args.concat(innerArgs));
}
}
//测试
const test = {
name: "xxx",
hello: function (a, b, c) {
console.log(`hello,${this.name}!`, a + b + c);
},
};
const obj = { name: "world" };
let hello = test.hello.myBind(obj, 1);
hello(2, 3)//hello,world! 6
console.log(new hello(2, 3));
//hello,undefined! 6
10. 请解释对Symbol的理解
八股链接
JS-Info-Symbol
Symbol 是 ES6 引入的一种新的基本数据类型,用于表示独一无二的值,具有唯一性和不可枚举性(因此无法被 for...in、Object.keys() 等遍历到)。
使用 Symbol() 或带有描述的 Symbol('desc') 创建的值都是独一无二的,哪怕两个 Symbol 的描述相同,它们也是不同的,== 和 === 比较结果都为 false。
但是,可以使用 Symbol.for(key) 在全局注册表中创建或获取一个共享的 Symbol。只要传入的键(key)相同,返回的就是同一个 Symbol,此时 === 比较为 true。这实现了跨模块、跨域的 Symbol 共享。
Symbol 在实际开发中主要用于:
- 避免命名冲突:为对象添加属性时,确保不会覆盖已有属性,特别适合库开发。
- 模拟私有属性:虽然不是真正的私有(仍可通过
Object.getOwnPropertySymbols() 或 Reflect.ownKeys() 访问),但能有效“隐藏”内部属性,防止被意外遍历或修改。
此外,JavaScript 还提供了一些内置 Symbol,用于自定义语言行为,比如:
Symbol.iterator:定义对象的默认迭代器,使其可被 for...of 遍历。
Symbol.toStringTag:自定义 Object.prototype.toString() 的返回值。
Symbol.hasInstance:自定义 instanceof 的行为。
11. 数组的遍历方法有哪些?
八股链接
这里也是涉及到数据结构了,后面统一再复习
这里粘一下AI回答
🎯 面试回答:数组遍历方法的演进与选择
在 JavaScript 中,数组的遍历方法有很多种,它们的发展其实反映了语言从“能用”到“好用”再到“函数式、声明式”的演进过程。我来按逻辑顺序梳理一下:
for 循环(传统基础)
const arr = [1, 2, 3];
for (let i = 0; i < arr.length; i++) {
console.log(arr[i]);
}
- ✅ 优点:最原始、最灵活,兼容性好,可以随时
break、continue、return。
- ❌ 缺点:代码冗长,容易出错(比如
i++ 写错),可读性差。
- 🧩 解决什么问题:最基本的遍历需求,适用于需要精确控制索引的场景。
但写多了就发现:每次都要写 i = 0; i < len; i++,太重复了!
for...in(遍历对象属性)
for (let index in arr) {
console.log(arr[index]);
}
- ⚠️ 问题:它其实是为对象设计的,遍历的是“可枚举属性”,可能会遍历到原型链上的属性,而且顺序不保证。
- 🚫 不推荐用于数组,尤其是当数组被扩展了属性时,会出问题。
所以我们需要一个“专门为数组设计”的语法。
for...of(ES6,为可迭代对象而生)
for (let item of arr) {
console.log(item);
}
- ✅ 优点:
- 语法简洁,直接获取元素值。
- 支持
break、continue、return。
- 不仅能遍历数组,还能遍历
Map、Set、arguments、字符串等可迭代对象。
- ✅ 解决了
for...in 的问题:只遍历数组的“数值索引”部分,不遍历原型属性,顺序也保证。
- 🧩 适用场景:需要遍历值、且可能中途跳出的场景,是
for 循环的现代替代。
但如果我们只是想“对每个元素做点事”,有没有更语义化的方式?
forEach(函数式,声明式)
arr.forEach(item => {
console.log(item);
});
- ✅ 优点:
- 更函数式,语义清晰:“对每个元素执行某个操作”。
- 代码更简洁。
- ❌ 缺点:
- 不能
break 或 continue(内部是函数调用,return 只是跳出当前回调)。
- 性能略低于
for 和 for...of。
- 🧩 解决什么问题:让代码更声明式,强调“做什么”而不是“怎么做”。
但如果我们想从遍历中“生成”一个新数组呢?
map(转换数据)
const doubled = arr.map(item => item * 2);
- ✅ 优点:
- 返回一个新数组,不修改原数组(函数式编程推崇)。
- 语义明确:映射,把原数组“转换”成新数组。
- 🧩 解决了什么问题:
forEach 只能“执行操作”,而 map 能“生成新数据”。
那如果我想“筛选”出符合条件的元素呢?
filter(筛选数据)
const evens = arr.filter(item => item % 2 === 0);
- ✅ 优点:语义清晰,返回符合条件的新数组。
- 🧩 适用场景:数据过滤,比如从用户列表中筛选出活跃用户。
那如果我想“聚合”数据,比如求和、找最大值呢?
reduce(聚合数据,万能方法)
const sum = arr.reduce((acc, item) => acc + item, 0);
- ✅ 优点:
- 极其强大,几乎可以实现所有遍历逻辑(
map、filter 都能用 reduce 实现)。
- 适合复杂聚合逻辑。
- ❌ 缺点:初学者理解成本较高。
- 🧩 解决什么问题:将数组“归约”为一个值,是函数式编程的核心方法。
但有时候我们只是想“判断”数组是否满足某个条件。
some / every(条件判断)
// 是否至少有一个偶数
const hasEven = arr.some(item => item % 2 === 0);
// 是否全是偶数
const allEven = arr.every(item => item % 2 === 0);
- ✅ 优点:
- 语义极强,代码可读性高。
- 短路求值:
some 找到一个就停,every 找到一个不满足就停,性能好。
- 🧩 解决什么问题:替代
for 循环中的 if + break 判断逻辑。
那如果我想“找”某个元素呢?
find / findIndex(查找元素)
// 找第一个偶数
const firstEven = arr.find(item => item % 2 === 0);
// 找第一个偶数的索引
const index = arr.findIndex(item => item % 2 === 0);
- ✅ 优点:语义清晰,
find 返回元素,findIndex 返回索引。
- 🧩 解决什么问题:替代
for 循环中“找元素 + break”的模式。
✅ 总结:如何选择?
| 需求 | 推荐方法 |
|---|
简单遍历,可能 break | for...of |
| 执行副作用(如打印、发请求) | forEach |
| 转换数组 | map |
| 筛选数组 | filter |
| 聚合计算 | reduce |
| 判断是否“有”满足条件的元素 | some |
| 判断是否“全部”满足条件 | every |
| 查找元素或索引 | find / findIndex |
🎯 面试加分点
for 和 for...of 支持 break,forEach 等高阶函数不支持。
map、filter、reduce 是函数式编程的“三剑客”,推崇不可变性。
for...in 不推荐遍历数组,for...of 是更好的选择。
reduce 是最强大的,但 map、filter 更语义化,优先使用它们。
12. JavaScript 中async/await 和Promise和generator有什么区别?
八股链接
这篇文章讲解了async/await语法糖的基础使用,然后引出了如何使用generator+promise来实现一个类似的效果,前面的大致我都能看懂,但是最后设计的一个全面的方案,我能按流程走一遍,但是具体实现还是很复杂的,后面再慢慢去理解吧,主要在这里学习一下generator,知道这个语法糖本质上可以用generator+promise来实现
7张图,20分钟就能搞定的async/await原理!为什么要拖那么久?
这个问题的话,我们先来讲async/await,它是一个用同步的方式执行异步操作的一个语法糖,在一个函数的function关键字前添加一个async关键字,那么这个函数将会返回一个Promise,在aysnc里可以使用await关键字,用于处理一个Promise的返回结果
Promise是JS异步编程的解决方案,通过new Promise并传入成功和失败的回调函数,它有三种状态,PENDING,FULFILLED,REJECTED,并且可以通过then、catch来处理结果
generator是一种特殊的函数,它返回一个对象用于管理函数执行过程,在function关键字后添加*后,这个函数就成为了迭代器函数,在这个函数里可以有多个暂停点,通过yield关键字来标记,核心的方法是next(),yield会执行它后面的代码,并作为next()的返回值,next的返回值是一个对象,对象里有value和done属性,value用于存储yield后面代码的执行结果,done用于表示流程是否完成,在next()里可以传入参数,作为yield的返回值
详细generator的信息参阅JS-Info-generator
13. 数组扁平化的实现有哪些方法?
八股链接
首先可以使用flat方法,接受一个值控制扁平的深度,即depth 参数表示要展开的嵌套层数
其次可以使用递归的思路
普通递归
const arrs = [1, [2, [3, [4, [5, 6]]]]];
function toFlattern(arrs) {
const flatArr = [];
arrs.forEach((arr) => {
if (Array.isArray(arr)) {
flatArr.push(...toFlattern(arr))
} else {
flatArr.push(arr);
}
})
return flatArr;
}
console.log(toFlattern(arrs))
使用reduce递归
const arrs = [1, [2, [3, [4, [5, 6]]]]];
function toFlattern(arrs) {
const flatArr = [];
arrs.forEach((arr) => {
if (Array.isArray(arr)) {
flatArr.push(...toFlattern(arr))
} else {
flatArr.push(arr);
}
})
return flatArr;
}
console.log(toFlattern(arrs))
使用栈的思路
function flatten(arr) {
const res = [];
const stack = [...arr];
while (stack.length) {
const val = stack.pop();
if (Array.isArray(val)) {
stack.push(...val);
} else {
res.unshift(val);
}
}
return res;
}
const nestedArray = [1, [2, [3, 4], 5], 6];
const flattenedArray = flatten(nestedArray);
console.log(flattenedArray);
14. 请说明es6新增数据类型weakMap和weakSet出现的原因,以及在什么情况下使用weakMap
八股链接
之前在手写深拷贝的时候用到了weakMap解决循环引用的问题
weakMap是一个只能使用对象作为键的存储键值对的数据结构,weakSet是一个只能存储对象的数据结构
它们的出现是为了解决JS中对对象的强引用导致垃圾回收无法正常工作导致内存泄漏的问题
被它们存储的对象,只要没有其他强引用,就可以被垃圾回收器回收,也就是它们对对象是弱引用
主要可以应用在对一些对象添加缓存,这样子当对象不再引用时可以正常回收
还可用于比如说一个DOM元素,在没被移除前可以使用weakMap存储一些相关信息,当DOM被移除后可以正常被垃圾回收
同时也可以用于对私有属性的封装
低频
1. requestAnimationFrame是宏任务还是微任务?
八股链接
既不是宏任务也不是微任务,而是浏览器渲染周期相关的一个方法,接受一个回调函数,执行频率通常与屏幕刷新率相同(通常为60hz),介于重排和重绘之间,也就是执行完重排之后紧跟rAF,然后进行重绘
MDN:window.requestAnimationFrame() 方法会告诉浏览器你希望执行一个动画。它要求浏览器在下一次重绘之前,调用用户提供的回调函数。
这篇文章写的不错,虽然我没仔细看,因为这个比较冷门,所以以后有机会回来看看吧
2022年了,真的懂requestAnimationFrame么?
2. 解释Babel是什么,有什么作用?
八股链接
Babel是一个Javascript代码的编译器,它可以将一些使用新语法和新特性编写的js代码转换为向后兼容的代码,使其能在一些旧版浏览器和环境中使用
通过将代码解析成AST语法树,再遍历AST并使用配置插件进行转换,最后将AST语法树重新生成为向后兼容的js代码。
Babel 是一个 JavaScript 编译器(transpiler),它的主要作用是将使用现代 JavaScript 语法(如 ES2015+)编写的代码,转换为向后兼容的版本(如 ES5),以便在当前和旧版浏览器或环境中正常运行。
它的工作流程大致分为三步:
- 解析(Parse):将源代码解析成抽象语法树(AST);
- 转换(Transform):遍历 AST,通过插件机制对特定语法进行转换(例如将
const/let 转为 var,箭头函数转为普通函数等);
- 生成(Generate):将转换后的 AST 重新生成为目标 JavaScript 代码。
Babel 的强大之处在于它的插件化和可配置性。我们可以通过配置 @babel/preset-env 这样的预设,让 Babel 根据目标浏览器环境自动决定需要转换哪些特性,还可以结合 core-js 实现 polyfill,补全缺失的 API(如 Promise、Array.from 等)。
此外,Babel 还支持 JSX、TypeScript 等非标准语法的转换,因此它不仅是语法兼容的工具,更是现代前端工程化体系中的核心一环。