本文深入解析 Babel 编译 Class 至 ES5 的核心机制。详细阐述私有字段通过 WeakMap/WeakSet 存储、私有方法通过 WeakSet 拦截调用的实现方式;分析静态与公开成员在构造函数与原型上的挂载逻辑;并拆解子类继承中 prototype 与 __proto__ 的重定向过程,揭示 Babel 如何利用辅助函数实现 ES5 兼容的面向对象编程。
前期准备
- 在项目根目录建立 index.js 与 .babelrc 配置文件
class Parent {
static type = 'Parent';
#salary = 0;
name = '';
static showType() {
console.log(this.type)
}
#seeSalary() {
console.log(this.#salary)
}
speakName() {
this.#seeSalary()
console.log(this.name)
}
}
class Child extends Parent {
constructor(name) {
super();
this.name = name;
}
speakName() {
super.speakName();
}
}
{
"presets": [
"@babel/preset-env"
]
}
npm install @babel/core @babel/cli @babel/preset-env --save-dev
npx babel index.js -o dist.js
编译生成的私有变量及方法存储机制解析
var _salary = new WeakMap();
var _seeSalary = new WeakSet();
var Parent = function () {
function Parent() {
_classPrivateMethodInitSpec(this, _seeSalary);
_classPrivateFieldInitSpec(this, _salary, {
writable: true,
value: 0
});
}
}
function _seeSalary2() {
console.log(_classPrivateFieldGet(this, _salary));
}
function _classPrivateMethodInitSpec(obj, privateSet) {
privateSet.add(obj);
}
function _classPrivateFieldInitSpec(obj, privateMap, value) {
privateMap.set(obj, value);
}
- 代码逻辑归纳:私有数据依托 WeakMap 存储,以实例为键、数值为值;私有行为依托 WeakSet 存储,以实例为键。因方法执行需依赖函数体,故函数体定义于类外部。
- 疑问点:此处调用的是
seeSalary而非seeSalary2。需明确私有行为仅限类内部访问,外部无法直接调用。因此在类内部调用seeSalary时,系统会拦截该操作并转而执行seeSalary2。 - 另一疑问:Babel 转换至 ES5 后为何仍出现
weakMap与weakSet?Babel 架构涵盖编译与 polyfill 两大模块。
编译后静态或公开变量及方法的存储机制解析
var Parent = function() {
function Parent() {
...
_defineProperty(this, "name", '');
}
// 第一个参数是public方法,第二个参数是static方法
_createClass(Parent, [{
key: "speakName",
value: function speakName() {
_classPrivateMethodGet(this, _seeSalary, _seeSalary2).call(this);
console.log(this.name);
}
}], [{
key: "showType",
value: function showType() {
console.log(this.type);
}
}]);
return Parent;
}
_defineProperty(Parent, "type", 'Parent');
function _createClass(Constructor, protoProps, staticProps) {
if (protoProps) _defineProperties(Constructor.prototype, protoProps);
if (staticProps) _defineProperties(Constructor, staticProps);
Object.defineProperty(Constructor, "prototype", { writable: false });
return Constructor;
}
function _classPrivateMethodGet(receiver, privateSet, fn) {
if (!privateSet.has(receiver)) {
throw new TypeError("attempted to get private field on non-instance");
}
return fn;
}
- 代码逻辑归纳:公开属性挂载于实例自身;公开方法挂载于构造器的
prototype上;静态属性与静态方法均挂载于类自身。
继承机制的实现原理探讨
var Child = function(_Parent2) {
_inherits(Child, _Parent2);
function Child(name) {
var _this;
_this = _callSuper(this, Child);
_this.name = name;
return _this;
}
_createClass(Child, [{
key: "speakName",
value: function speakName() {
_get(_getPrototypeOf(Child.prototype), "speakName", this).call(this);
}
}]);
return Child;
}(Parent);
function _inherits(subClass, superClass) {
if (typeof superClass !== "function" && superClass !== null) {
throw new TypeError("Super expression must either be null or a function");
}
subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } });
Object.defineProperty(subClass, "prototype", { writable: false });
if (superClass) _setPrototypeOf(subClass, superClass);
}
function _setPrototypeOf(o, p) {
_setPrototypeOf = Object.setPrototypeOf ?
Object.setPrototypeOf.bind() :
function _setPrototypeOf(o, p) { o.__proto__ = p; return o; };
return _setPrototypeOf(o, p);
}
function _callSuper(t, o, e) {
return o =
_getPrototypeOf(o), _possibleConstructorReturn(t, _isNativeReflectConstruct() ?
Reflect.construct(o, e || [], _getPrototypeOf(t).constructor) :
o.apply(t, e));
}
- 代码逻辑归纳:继承通过修改子类
prototype指向新对象实现,该新对象prototype指向父类prototype,且新对象constructor维持子类constructor不变并允许外部修改。随后禁止子类prototype被赋值为其他对象。最终使子类__proto__指向父类。 - 疑问点:为何创建新对象而非令子类
prototype直接指向父类prototype?旨在避免此前向子类prototype添加公开方法时影响父类。 - 疑问点:为何需
__proto__指向父类?为确保静态属性与方法可被子类访问父类成员,前文已述静态成员挂载于类自身。
延伸知识:原型链机制
- 所有对象(含数组、函数等)均具备
__proto__属性,借由该属性指向其他对象以构建原型链 - 函数除具备
__proto__外还拥有prototype,但原型链查找不经过prototype,除非使用 new 实例化类,因 new 关键字将类prototype赋给实例__proto__。
实例演示
- 案例一
function P() {}
P.prototype.x = 'x'
function C() {}
C.prototype = P.prototype
console.log(C.x) // undefined
- 案例二
function P() {}
P.prototype.x = 'x'
P.x = 'xxx'
function C() {}
C.__proto__ = P;
console.log(C.x) // xxx
上述内容详尽解析了 Babel 将 class 语法糖转换为 ES5 语法的流程,欲获取更多 babel class 转 ES5 资讯,敬请留意本站其他相关博文!