我们天天写的 ES6+ 语法,Babel 转成了什么?让我们从 Babel 转译产物的视角,重新认识下每天都在用的那些现代 JavaScript 语法。

1. 箭头函数:不只是更短的 function,而是 this 的穿越术
我们先从一个最简单的箭头函数开始:
javascript
const user = {
name: 'weedsfly',
greet: () => {
console.log(this.name);
}
};
Babel 会把它转成什么?在 ES5 环境里根本没有箭头函数,所以它只能用普通函数模拟。但普通函数有自己的 this,怎么让 this 指向外层作用域?答案就是:缓存 this,用闭包传进去。
javascript
"use strict";
var _this = void 0; // 顶层 this 是 undefined
var user = {
name: 'weedsfly',
greet: function greet() {
console.log(_this.name); // 引用外层的 _this
}
};
核心就两步:
- 在箭头函数定义的位置,把外层的 this 存到一个变量里(比如
_this) - 箭头函数体内部所有对 this 的引用,全部替换成这个变量
所以箭头函数的 this 并不是什么"绑定",而是静态替换------它在定义时就写死了外层的 this,不管你之后怎么调用它,this 都不会变。
一个更典型的例子:React 类组件中的箭头函数方法:
javascript
class MyComponent extends React.Component {
handleClick = () => {
this.setState({ clicked: true });
};
}
Babel 转译后(简化):
javascript
function MyComponent() {
var _this = this;
this.handleClick = function() {
_this.setState({ clicked: true }); // 引用构造函数里的 _this
};
}
这下彻底清楚了:为什么 handleClick 永远指向组件实例?因为它在构造函数里被定义的那一刻,就把当时的 this(即实例)存进了闭包。无论你把它丢给 onClick、setTimeout 还是什么别的地方,它内部用的都是那份不变的 _this。
启示:
- 箭头函数的 this 是词法作用域决定的,不是调用方式决定的。
- 需要动态 this 的场景(对象方法、Vue 的 methods)不能用箭头函数。
- 需要固定 this 的场景(回调、定时器、事件处理)用箭头函数最省心。
2. 解构:不是魔法,只是一堆临时变量
你一定写过这样的解构:
javascript
const { name, age } = user;
const [first, ...rest] = arr;
在 Babel 的转译产物里,你会发现解构没有任何黑科技------它只是帮你自动生成了临时变量。
javascript
// const { name, age } = user;
var _user = user;
var name = _user.name;
var age = _user.age;
数组解构更明显,直接用索引访问:
javascript
// const [first, ...rest] = arr;
var _arr = arr;
var first = _arr[0];
var rest = _arr.slice(1);
再来看带默认值的解构:
javascript
const { count = 10 } = obj;
转译后:
javascript
var _obj = obj;
var _obj$count = _obj.count;
var count = _obj$count === undefined ? 10 : _obj$count;
看到了吗?只有当值为 undefined 时,默认值才会生效。这就解释了为什么 null、0、false 都不会触发默认值------Babel 生成的条件就是 === undefined。
如果你之前以为解构默认值等效于 ||,现在从转译结果就能明白两者的区别了。
启示:
- 解构就是语法糖,理解其转译逻辑后,深层嵌套的解构就容易读懂了。
- 默认值只对
undefined有效,需要排除null时请用??。
3. class 和 extends:原型链的马甲
ES6 的 class 写法看起来像 Java/C#,但 Babel 转译后你会发现,它只是原型继承的语法糖。
一个简单的类:
javascript
class Animal {
constructor(name) {
this.name = name;
}
say() {
console.log(this.name);
}
}
Babel 转译后(简化版):
javascript
function Animal(name) {
// 确保是通过 new 调用的
if (!(this instanceof Animal)) {
throw new TypeError("Cannot call a class as a function");
}
this.name = name;
}
// 方法挂到 prototype 上
Object.defineProperty(Animal.prototype, "say", {
value: function say() {
console.log(this.name);
},
enumerable: false, // 方法不可枚举
writable: true,
configurable: true
});
对比一下手工写的构造函数 + 原型方法,是不是一模一样?class 只是多了一个安全检查(防止忘记 new)和自动设置不可枚举。
继承 extends 更精彩:
javascript
class Dog extends Animal {
constructor(name, breed) {
super(name);
this.breed = breed;
}
}
Babel 会生成一套完整的寄生组合继承,并处理 super 调用、静态属性继承、原型链修正等。核心逻辑等价于:
javascript
function Dog(name, breed) {
Animal.call(this, name); // super(name)
this.breed = breed;
}
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
Dog.__proto__ = Animal; // 静态方法继承
启示:
class底层还是原型链,理解转译产物能帮你吃透原型继承。super关键字本质上就是调用父类构造函数(Parent.call(this, ...))和访问Parent.prototype。- 当你需要精确控制实例化(比如写工具库),看 Babel 产物能让你更懂
class的边界行为。
4. async/await:生成器 + Promise 的自动执行器
这是我最震撼的转译发现。看似可以"暂停"的 async/await,底层其实是生成器 + 自动执行器。
javascript
async function fetchUser() {
const user = await fetch('/api/user').then(res => res.json());
console.log(user.name);
return user;
}
Babel(配合 regenerator)会把它转成类似这样的代码:
javascript
function fetchUser() {
return regeneratorRuntime.async(function fetchUser$(_context) {
while (1) {
switch (_context.prev = _context.next) {
case 0:
_context.next = 2;
return regeneratorRuntime.awrap(
fetch('/api/user').then(res => res.json())
);
case 2:
user = _context.sent;
console.log(user.name);
return _context.abrupt("return", user);
case 4:
case "end":
return _context.stop();
}
}
});
}
虽然看起来复杂,但核心逻辑就是状态机:
regeneratorRuntime.async包裹一个生成器函数,自动执行next()await被编译为yield,让出控制权- 每次
yield后,外部等 Promise resolve 再调用next(result)把值传回 switch/case用来记录执行到哪一步(哪个await点)
换句话说,async/await 就是把我们之前手写的那个 runGenerator 函数(让生成器自动等待 Promise)内置到了引擎里,并提供了更简洁的语法。
启示:
await并没有真正阻塞 JS 主线程,它只是暂停了当前 async 函数,让出控制权给其他任务。- 理解
async/await的底层,你就明白了为什么await之后的代码相当于微任务(它们被包裹在生成器的next()调用链里)。 - 如果面试官问你"手写一个 async/await",你心里就有谱了------实现一个生成器自动执行器。
5. for...of:迭代协议的揭示
for...of 遍历数组、Map、Set 都很方便,但它是怎么做到的?
javascript
for (const item of arr) {
console.log(item);
}
Babel 转译后:
javascript
var _iteratorNormalCompletion = true;
var _didIteratorError = false;
var _iteratorError = undefined;
try {
for (
var _iterator = arr[Symbol.iterator](), _step;
!(_iteratorNormalCompletion = (_step = _iterator.next()).done);
_iteratorNormalCompletion = true
) {
var item = _step.value;
console.log(item);
}
} catch (err) {
_didIteratorError = true;
_iteratorError = err;
} finally {
// 处理迭代器未结束的情况
}
简化一下核心:
javascript
var iterator = arr[Symbol.iterator]();
var result = iterator.next();
while (!result.done) {
var item = result.value;
// 执行循环体
result = iterator.next();
}
看到了吗?for...of 只是对 Symbol.iterator 协议的一层包装。任何对象实现了 Symbol.iterator 方法、返回符合 { value, done } 格式的迭代器,就能用 for...of 遍历。
启示:
- 你终于明白为什么
{ a: 1, b: 2 }不能直接用for...of遍历了------因为普通对象没有Symbol.iterator。 - 想让自定义数据结构可遍历?实现
[Symbol.iterator]()就行。 for...of是"黑盒遍历",它不关心数据是什么结构,只关心迭代器协议。这跟用遥控器换频道是一个道理。
6. 模板字面量与标签模板:字符串拼接的真相
模板字面量大家用得很多:
javascript
const msg = `Hello, ${name}. You have ${count} messages.`;
Babel 转译后会变成普通的字符串拼接:
javascript
var msg = "Hello, ".concat(name, ". You have ").concat(count, " messages.");
就这么简单。但如果用了标签模板:
javascript
const result = styled.div`
color: ${props => props.color};
`;
Babel 会把它转成:
javascript
var result = styled.div(
["\n color: ", ";\n"],
function(props) { return props.color; }
);
把模板的静态部分和动态部分拆开,分别传给标签函数。这就是 CSS-in-JS 库(比如 styled-components)的底层原理。
启示:
- 模板字面量就是更优雅的字符串拼接,没有运行时魔法。
- 标签模板提供了"预处理"字符串的能力,是 DSL(领域特定语言)的基础。