// Shape 类
functionShape(id,,){
this.id =;
this.setLocation(x,);
}
// 设置坐标的原型方法
Shape.prototype.setLocation =function(x,){
this.x =;
this.y =;
};
classShape{
constructor(id,,){// 构造函数语法糖
this.id =;
this.setLocation(x,);
}

setLocation(x,){// 原型方法
this.x =;
this.y =;
}
}

一、揭秘

好像没那么简单,为了摸清实际转译流程,我们先将上述类定义代码简化为一个只有14字节的空类:

classShape{}

首先,当访问器走到类声明阶段,需要补充严格模式:

"use strict";
classShape{}

而进入变量声明与标识符阶段时则需补充 let 关键字并转为 var:

"use strict";
varShape=classShape{};

到这个时候代码的变化都不太大。接下来是进入函数表达式阶段,多出来几行函数:

"use strict";
function(instance,Constructor){if(!(instance instanceofConstructor)){thrownewTypeError("Cannot call a class as a function");}}
varShape=functionShape(){
_classCallCheck(this,Shape);
};

该阶段不仅替换了 class,还在类中调用了叫做 _classCallCheck 的方法。这是什么呢?

这个函数的作用在于确保构造方法永远不会作为函数被调用,它会评估函数的上下文是否为 Shape 对象的实例,以此确定是否需要抛出异常。接下来,则轮到 babel-plugin-minify-simplify 上场,这个插件做的事情在于通过简化语句为表达式、并使表达式尽可能统一来精简代码。运行后的输出是这样的:

"use strict";
function(instance,Constructor){if(!(instance instanceofConstructor))thrownewTypeError("Cannot call a class as a function");}
varShape=functionShape(){
_classCallCheck(this,Shape);
};

可以看到 if 语句中由于只有一行代码,于是花括号被去掉。接下来上场的便是内置的 Block Hoist,该插件通过遍历参数排序然后替换,Babel 输出结果为:

"use strict";
function(a,){if(!(a instanceof))thrownewTypeError("Cannot call a class as a function");}
varShape=function(){
_classCallCheck(this,);
};

最后一步,minify 一下,代码体积由最初的14字节增为338字节:

"use strict";function(a,b){if(!(a instanceof))thrownewTypeError("Cannot call a class as a function")}varShape=function(){_classCallCheck(this,a)};


二、再说一些

这是一个什么都没干的类声明,但现实中任何类都会有自己的方法,而此时 Babel 必定会引入更多的插件来帮助它完成代码的转译工作。直接在刚刚的空类中定义一个方法吧。

classShape{
render(){
console.log("Hi");
}
}

我们用 Babel 转译一下,会发现代码中包含如下这段:

var=function(){function(a,){for(var,=0;<.length;++)=[d],.enumerable =.enumerable ||!1,.configurable =!0,"value"in&&(c.writable =!0),Object.defineProperty(a,.key,);}returnfunction(b,,){return&&(b.prototype,),&&(b,),;};}();


类似前面我们遇到的 _classCallCheck,这里又多出一个 _createClass,这是做什么的呢?我们稍微把代码状态往前挪一挪,来到 babel-plugin-minify-builtins 处理阶段(该插件的作用在于缩减内置对象代码体积,但我们主要关注点在于这个阶段的 _createClass 函数是基本可读的),此时 _classCallCheck 长成这样:

var=function(){
function(target,){
for(var=0;<.length;++){
var=[i];
descriptor.enumerable =.enumerable ||false;
descriptor.configurable =true;
if("value"in).writable =true;
Object.defineProperty(target,.key,);
}
}
returnfunction(Constructor,,){
if(protoProps)(Constructor.prototype,);
if(staticProps)(Constructor,);
returnConstructor;
};
}();

可以看出 _createClass 用于处理创建对象属性,函数支持传入构造函数与需定义的键值对属性数组。函数判断传入的参数(普通方法/静态方法)是否为空对应到不同的处理流程上。而 defineProperties 方法做的事情便是遍历传入的属性数组,然后分别调用 Object.defineProperty 以更新构造函数。而在 Shape 中,由于我们定义的不是静态方法,我们便这样调用:

 

_createClass(Shape,[{
key:"render",
value:function(){
console.log("Hi");
}
}]);

T.J. Crowder 在 How does Babel.js create compile a class declaration into ES2015? 中谈到 Babel 是如何将 class 转化为 ES5 兼容代码时谈到了几点,大意为:

  • constructor 会成为构造方法数;
  • 所有非构造方法、非静态方法会成为原型方法;
  • 静态方法会被赋值到构造函数的属性上,其他属性保持不变;
  • 派生构造函数上的原型属性是通过 Object.create(Base.prototype) 构造的对象,而不是 new Base();
  • constructor 调用构造器基类是第一步操作;
  • ES5 中对应 super 方法的写法是 Base.prototype.baseMethod.call(this);,这种操作不仅繁琐而且容易出错;

这些概述大致总结了类定义在两个 ES 版本中的一些差异,其他很多方面比如 extends ——继承关键字,它的使用则会使 Babel 在转译结果加上 _inherits与 _possibleConstructorReturn 两个函数。篇幅所限,此处不再展开详述。

三、最后

语法糖 class 给我们带来了很多写法上的便利,但可能会使我们在代码体积上的优化努力“付诸东流”。

另一方面,如果你是一名 React 应用开发者,你是否已经在想将代码中的所有 class 写法换为 function 呢?那样做的话,代码体积无疑会减少很多,但你一定也知道 PureComponent 相比 Component 的好处。所以虽然 function 给你的代码体积减负了,但他在哪里又给你无形增加负担了呢?

因此,真的不推荐开发者用 class 这种写法么,你觉得呢?