这是一篇番外篇,其实他的主线是由一篇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:
- 1. 前面说过,ES6中定义的非
static
方法都是挂载在prototype
上的。我们在class
中定义了一个constructor()
,但是这个方法被加载在protoProps
数组里面了。这是因为在ES5Function
的实例中的prototype
,都有一个默认属性constructor
指向构造函数。 - 2. 在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); 复制代码
这里有几个地方需要提醒的是:
- 1. 在IIFE中的
_createClass
中多了第三个参数staticProps
,也就是说,static
方法是和prototype
的信息存贮是分开的。 - 2. 在
_createClass
中针对staticProps
信息也是经过_defineProperties
处理的。也就是说,挂载在Test
上的方法也是enumerable=false
。不能被for(let propsName in Class)
被捕获。 - 3. 静态属性是不在IIFE的作用域中的。也就说,这个属性是相对于
prototype
中的方法和属性是全局的。这也就证实了,主线情节中,Circle能根据挂载在类上的静态属性(circlesMade)计算被实例化了多少次 - 4. 同时挂载在类上的
static
是enumerable=true
的。 - 5. 与 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==实例对象
。最终的结果就是在实例对象
上新增了指定的属性。