语法糖褪去之后——Babel 转译产物中的 JavaScript 本貌

我们天天写的 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(即实例)存进了闭包。无论你把它丢给 onClicksetTimeout 还是什么别的地方,它内部用的都是那份不变的 _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 时,默认值才会生效。这就解释了为什么 null0false 都不会触发默认值------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(领域特定语言)的基础。
相关推荐
JustHappy1 小时前
「软件设计思想杂谈🤔」“切图仔”也能懂编译原理?框架源码也许没那么难。聊聊 Vue 的编译(上)
前端·javascript·vue.js
禅思院2 小时前
路由性能高可用架构实战方案
前端·架构·前端框架
IT_陈寒2 小时前
React状态更新总是不及时?你可能漏了这步批处理机制
前端·人工智能·后端
恋猫de小郭2 小时前
AI Agent 开发究竟是啥?如何用 AI 开发 Agent ?深入浅出给你一套概念
android·前端·ai编程
前端双越老师2 小时前
我开发 AI Agent 项目踩过的 5个坑
前端·agent·全栈
晓得迷路了2 小时前
栗子前端技术周刊第 134 期 - React Router v8、TypeScript 7 RC、React Native 0.86...
前端·javascript·react.js
Carson带你学Android2 小时前
Android 17 正式发布:AI 终于成了系统能力
android·前端·ai编程
Mike_jia3 小时前
ZbxTable:Zabbix开源报表神器,从运维数据到决策洞察的最后一公里
前端
LinXunFeng12 小时前
Obsidian - 使用 Share Note 分享笔记并自部署
前端·笔记·github