这是一篇番外篇,其实他的主线是由一篇ES6-Class科普文章引发的“惨案”。虽然是番外篇,但是有些剧情还是不容错过的。
现在我们来进入番外篇的主线:ES6的Class是ES5构造函数的语法糖。
那还是让我们简单的回归一下ES5的类是如何构建的:
//构造函数
var Circle = function(name){
this.name = name;
Circle.circlesMade++;
}
//定义一个类属性
Object.defineProperty(Circle, "circlesMade", {
get: function() {
return !this._count ? 0 : this._count;
},
set: function(val) {
this._count = val;
}
});
//定义构造函数的prototype
Circle.prototype = {
constructor:Circle,
toString:function(){
return this.name+'南蓁';
}
}
虽然是封装(OOP编程三大特点之一:封装性)了用于代表一系列数据和操作的Circle类。但是这段代码有些许的冗余。也就是说,实例相关的属性和方法,需要一坨代码,prototype相关的也需要一坨,定义在类上的又双叒一坨。但是写一个类,需要照顾很多(n>3)的情绪。做一个ES5的类好蓝啊。
所以,他的彩霞仙子来了。用最优雅的方式来装X。
Talk is cheap ,show you the code.
class Circle {
construcotr(name){
this.name = name;
Circle.circlesMade++
}
static get circlesMade() {
return !this._count ? 0 : this._count;
};
static set circlesMade(val) {
this._count = val;
};
toString(){
return this.name+'南蓁';
}
}
通过对Prototype糖化处理,代码看起来简洁很多。
但是具体如何实现的呢。我们来进行脱糖处理。
一口吃不成胖子,我们对上面的代码,进行由浅入深的分析一下。但是在分析之前,需要对JS的Class有一个基本的认识。其实JS中的Class和其他传统OOP语言(C++,JAVA)是不一样的。它是基于Prototype的类的构造和继承的实现的。
它的公式如下:
JsClass = Constructor Function + Prototype
其中Constructor Function用于定义实例中行为(属性+方法)Prototype用于定义实例共有的行为
所以我们来构建半个类,只拥有Constructor Function。
Note:这里需要解释半个类,由于为了更好的解释如何进行糖化的,就按ES5的结构类比介绍。但是需要注意的就是,在ES6class中定义的方法都是挂载在prototype上的。(不包含static方法)
"半个"类
糖化代码
class Test {
constructor(x,y){
this.x =x;
this.y = y;
}
}
脱糖代码
来直接看看脱糖之后的效果。
"use strict";
function _instanceof(left, right) {
if (
right != null &&
typeof Symbol !== "undefined" &&
right[Symbol.hasInstance]
) {
return !!right[Symbol.hasInstance](left);
} else {
return left instanceof right;
}
}
function _classCallCheck(instance, Constructor) {
if (!_instanceof(instance, Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
}
var Test = function Test(x, y) {
_classCallCheck(this, Test);
this.x = x;
this.y = y;
};
会发现这里的仅仅比常规的Constructor Function多了一步_classCallCheck(this, Test); 其实就是多了这一步,就制约了ES6的Class不能像ES5的构造函数一样,进行直接调用。(虽然,ES6的Class的typeof Class == 'funtion' //ture)
具体原因可以参照主线剧情
然后我们继续完善一个完整的类: 但是在此之前,我们需要明确一点就是
class中所有的方法,都是挂载在
prototype
没有static属性的"完整"类
糖化代码
class Test {
constructor(x,y){
this.x =x;
this.y = y;
}
toString(){
return '北宸'
}
}
脱糖代码
脱糖处理之后的对应代码如下:
为了针对重点解释,我们就将_classCallCheck相关代码简化,只留下重点代码:
"use strict";
function _instanceof(left, right) { }
function _classCallCheck(instance, Constructor) {}
function _defineProperties(target, props) {
for (var i = 0; i < props.length; i++) {
var descriptor = props[i];
descriptor.enumerable = descriptor.enumerable || false;
descriptor.configurable = true;
if ("value" in descriptor) descriptor.writable = true;
Object.defineProperty(target, descriptor.key, descriptor);
}
}
function _createClass(Constructor, protoProps, staticProps) {
if (protoProps) _defineProperties(Constructor.prototype, protoProps);
if (staticProps) _defineProperties(Constructor, staticProps);
return Constructor;
}
var Test =
/*#__PURE__*/
(function() {
function Test(x, y) {
_classCallCheck(this, Test);
this.x = x;
this.y = y;
}
_createClass(Test, [
{
key: "toString",
value: function toString() {
return "北宸";
}
}
]);
return Test;
})();
让我们分析一波:
首先映入眼帘的是一个IIFE(立即执行函数/自执行匿名函数):在定义时就会执行的函数。
那这个IIFE函数被执行之后,发生了啥。咦。有一段代码很熟悉。
function Test(x, y) {
_classCallCheck(this, Test);
this.x = x;
this.y = y;
}
这不就是定义了一个构造函数吗,并对这个函数进行_classCallCheck检查。
其实这段代码,最神奇的地方在于_createClass这个函数的调用。
先把谜底揭晓一下,这个方法就是在为目标构造prototype的过程。
然后我们来参考一下实现思路:
_createClass(Test, [
{
key: "toString",
value: function toString() {
return "北宸";
}
}
]);
||
||
||
\/
function _createClass(Constructor, protoProps, staticProps) {
if (protoProps) _defineProperties(Constructor.prototype, protoProps);
if (staticProps) _defineProperties(Constructor, staticProps);
return Constructor;
}
||
||
||
\/
function _defineProperties(target, props) {
for (var i = 0; i < props.length; i++) {
var descriptor = props[i];
descriptor.enumerable = descriptor.enumerable || false;
descriptor.configurable = true;
if ("value" in descriptor) descriptor.writable = true;
Object.defineProperty(target, descriptor.key, descriptor);
}
}
其实这段代码中,_createClass(Constructor, protoProps, staticProps)是核心。在函数内部,依次对protoProps,staticProps进行_defineProperties处理。分别目标类进行prototype和静态属性进行设置
Note:
- 前面说过,ES6中定义的非
static方法都是挂载在prototype上的。我们在class中定义了一个constructor(),但是这个方法被加载在protoProps数组里面了。这是因为在ES5Function的实例中的prototype,都有一个默认属性constructor指向构造函数。 - 在ES6定义的非
static方法都是enumerable=false也就是说,es6中定义的方法,是不会被for(let props in obj)所察觉的,而ES5中prototype是可以的。
拥有static属性的"完整"类
直接撸一个实例:(代码结构还是沿用前面的例子)
ES6糖化代码
class Test {
//新增static属性
static _count=1;
constructor(x,y){
this.x =x;
this.y = y;
}
toString(){
return '北宸'
}
//新增static方法
static getAllName(){
return '北宸南蓁'
}
}
note:针对现在ES6的标准,是无法对class新增static属性的。只有在babel的stage-0/1/2/3的协助上才可以。
脱糖代码
我们也会将一些上面已经解释过的代码,简化:
"use strict";
function _instanceof(left, right) {}
function _classCallCheck(instance, Constructor) {}
function _defineProperties(target, props) {}
function _createClass(Constructor, protoProps, staticProps) {}
function _defineProperty(obj, key, value) {
if (key in obj) {
Object.defineProperty(obj, key, {
value: value,
enumerable: true,
configurable: true,
writable: true
});
} else {
obj[key] = value;
}
return obj;
}
var Test =
/*#__PURE__*/
(function() {
//新增static属性
function Test(x, y) {
_classCallCheck(this, Test);
this.x = x;
this.y = y;
}
_createClass(
Test,
[
{
key: "toString",
value: function toString() {
return "北宸";
} //新增static方法
}
],
[
{
key: "getAllName",
value: function getAllName() {
return "北宸南蓁";
}
}
]
);
return Test;
})();
_defineProperty(Test, "_count", 1);
这里有几个地方需要提醒的是:
- 在IIFE中的
_createClass中多了第三个参数staticProps,也就是说,static方法是和prototype的信息存贮是分开的。 - 在
_createClass中针对staticProps信息也是经过_defineProperties处理的。也就是说,挂载在Test上的方法也是enumerable=false。不能被for(let propsName in Class)被捕获。 - 静态属性是不在IIFE的作用域中的。也就说,这个属性是相对于
prototype中的方法和属性是全局的。这也就证实了,主线情节中,Circle能根据挂载在类上的静态属性(circlesMade)计算被实例化了多少次 - 同时挂载在类上的
static是enumerable=true的。 - 与 ES5 一样,类的所有实例共享一个原型对象
实例属性的新写法
这个特性和在class中直接static classPropsName ='value'一样,是需要babel的stage-0及以上才可以实现。
代码糖化
class Test {
count=count;
name =name;
}
脱糖处理
"use strict";
function _instanceof(left, right) {}
function _classCallCheck(instance, Constructor) {}
function _defineProperty(obj, key, value) {
if (key in obj) {
Object.defineProperty(obj, key, {
value: value,
enumerable: true,
configurable: true,
writable: true
});
} else {
obj[key] = value;
}
return obj;
}
var Test = function Test() {
_classCallCheck(this, Test);
_defineProperty(this, "count", count);
_defineProperty(this, "name", name);
};
其实这段代码就是在每次进行一个实例属性的定义的时候,在脱糖之后,在构造函数中调用_defineProperty(this, key, value);进行值的绑定。
由于是定义的是实例属性,所以如上代码中count/name都是在实例化的时候传入到类中的。new Test(1,'北宸')。所以在_defineProperty中判断if(key in obj)的时候,是true。所以,就会进行Object.defineProperty的处理。同时在实例化的时候,this==实例对象。最终的结果就是在实例对象上新增了指定的属性。