对于javascript来讲,类是一种可选(而不是必须)的设计模式,并且在JavaScript这样的[[Prototype]] 语言中实现类是很蹩脚的。javascript
这种蹩脚的感受不仅是来源于语法,虽然语法是很重要的缘由。js里面有许多语法的缺点:繁琐杂乱的.prototype 引用、试图调用原型链上层同名函数时的显式伪多态以及不可靠、不美观并且容易被误解成“构造函数”的.constructor。css
除此以外,类设计其实还存在更进一步的问题。传统面向类的语言中父类和子类、子类和实例之间实际上是复制操做,可是在[[Prototype]] 中并无复制。java
对象关联代码和行为委托使用了[[Prototype]] 而不是将它藏起来,对比其简洁性能够看出,类并不适用于JavaScript。设计模式
classapp
不过并不须要再纠结于这个问题;能够看看ES6 的class机制。这里会介绍它的工做原理并分析class是否改进了以前提到的那些缺点。框架
下面是一个例子:dom
class Widget { constructor(width,height) { this.width = width || 50; this.height = height || 50; this.$elem = null; } render($where){ if (this.$elem) { this.$elem.css( { width: this.width + "px", height: this.height + "px" } ).appendTo( $where ); } } } class Button extends Widget { constructor(width,height,label) { super( width, height ); this.label = label || "Default"; this.$elem = $( "<button>" ).text( this.label ); } render($where) { super( $where ); this.$elem.click( this.onClick.bind( this ) ); } onClick(evt) { console.log( "Button '" + this.label + "' clicked!" ); } }
能够看出,语法较prototype优雅了许多。那还能解决什么问题呢?函数
1. (基本上)再也不引用杂乱的.prototype了。
2. Button 声明时直接“ 继承” 了Widget, 再也不须要经过Object.create(..) 来替换.prototype 对象,也不须要设置.__proto__ 或者Object.setPrototypeOf(..)。
3. 能够经过super(..) 来实现相对多态,这样任何方法均可以引用原型链上层的同名方法。这能够解决一个问题:构造函数不属于类,因此没法互相引用——super() 能够完美解决构造函数的问题。
4. class 字面语法不能声明属性(只能声明方法)。看起来这是一种限制,可是它会排除掉许多很差的状况,若是没有这种限制的话,原型链末端的“实例”可能会意外地获取其余地方的属性(这些属性隐式被全部“实例”所“共享”)。因此,class 语法实际上能够帮助你避免犯错。
5. 能够经过extends 很天然地扩展对象(子)类型,甚至是内置的对象(子)类型,好比Array 或RegExp。没有class ..extends 语法时,想实现这一点是很是困难的,基本上只有框架的做者才能搞清楚这一点。可是如今能够垂手可得地作到!性能
class 语法确实解决了典型原型风格代码中许多显而易见的语法问题。this
class陷阱
然而,class 语法并无解决全部的问题,在JavaScript 中使用“类”设计模式仍然存在许多深层问题。
首先,你可能会认为ES6 的class 语法是向JavaScript 中引入了一种新的“类”机制,其实不是这样。class 基本上只是现有[[Prototype]]机制的一种语法糖。
也就是说,class 并不会像传统面向类的语言同样在声明时静态复制全部行为。若是你(有意或无心)修改或者替换了父“类”中的一个方法,那子“类”和全部实例都会受到影响,由于它们在定义时并无进行复制,只是使用基于[[Prototype]] 的实时委托:
class C { constructor() { this.num = Math.random(); } rand() { console.log( "Random: " + this.num ); } } var c1 = new C(); c1.rand(); // "Random: 0.4324299..." C.prototype.rand = function() { console.log( "Random: " + Math.round( this.num * 1000 )); }; var c2 = new C(); c2.rand(); // "Random: 867" c1.rand(); // "Random: 432" ——噢!
若是已经明白委托的原理,并不会指望获得“类”的副本的话,那这种行为才看起来比较合理。因此会问:为何要使用本质上不是类的class语法呢?
ES6 中的class语法不是会让传统类和委托对象之间的区别更加难以发现和理解吗?
class 语法没法定义类成员属性(只能定义方法),若是为了跟踪实例之间共享状态必需要这么作,那你只能使用丑陋的.prototype 语法,像这样:
class C { constructor() { // 确保修改的是共享状态而不是在实例上建立一个屏蔽属性! C.prototype.count++; // this.count 能够经过委托实现咱们想要的功能 console.log( "Hello: " + this.count ); } } // 直接向prototype 对象上添加一个共享状态 C.prototype.count = 0; var c1 = new C(); // Hello: 1 var c2 = new C(); // Hello: 2 c1.count === 2; // true c1.count === c2.count; // true
这种方法最大的问题是, 它违背了class 语法的本意, 在实现中暴露了.prototype。
若是使用this.count++ 的话,会发如今对象c1 和c2 上都建立了.count 属性,而不是更新共享状态。class 没有办法解决这个问题,而且干脆就不提供相应的语法支持,因此根本就不该该这样作。
此外,class 语法仍然面临意外屏蔽的问题:
class C { constructor(id) { // 噢,郁闷,咱们的id 属性屏蔽了id() 方法 this.id = id; } id() { console.log( "Id: " + id ); } } var c1 = new C( "c1" ); c1.id(); // TypeError -- c1.id 如今是字符串"c1"
除此以外,super也存在一些细微的问题。你可能认为super的绑定方法和this 相似,也就是说,不管目前的方法在原型链中处于什么位置,super 总会绑定到链中的上一层。
然而,出于性能考虑(this 绑定已是很大的开销了),super 并非动态绑定的,它会在声明时“静态”绑定。没什么大不了的,是吧?
可能不是这样。若是你和大多数JavaScript 开发者同样,会用许多不一样的方法把函数应用在不一样的(使用class 定义的)对象上,那你可能不知道,每次执行这些操做时都必须从新绑定super。
此外,根据应用方式的不一样,super 可能不会绑定到合适的对象(至少和你想的不同),因此你可能须要用toMethod(..) 来手动绑定super(相似用bind(..) 来绑定this)。
你已经习惯了把方法应用到不一样的对象上,从而能够自动利用this 的隐式绑定规则。可是这对于super 来讲是行不通的。
思考下面代码中super 的行为(D 和E 上):
class P { foo() { console.log( "P.foo" ); } } class C extends P { foo() { super(); } } var c1 = new C(); c1.foo(); // "P.foo" var D = { foo: function() { console.log( "D.foo" ); } }; var E = { foo: C.prototype.foo }; // 把E 委托到D Object.setPrototypeOf( E, D ); E.foo(); // "P.foo"
若是你认为super 会动态绑定(很是合理!),那你可能指望super() 会自动识别出E 委托了D,因此E.foo() 中的super() 应该调用D.foo()。
但事实并非这样。出于性能考虑,super 并不像this 同样是晚绑定(late bound, 或者说动态绑定)的,它在[[HomeObject]].[[Prototype]] 上,[[HomeObject]] 会在建立时静态绑定。
在本例中,super() 会调用P.foo(),由于方法的[[HomeObject]] 仍然是C,C.[[Prototype]]是P。
确实能够手动修改super 绑定,使用toMethod(..) 绑定或从新绑定方法的[[HomeObject]](就像设置对象的[[Prototype]] 同样!)就能够解决本例的问题:
var D = { foo: function() { console.log( "D.foo" ); } }; // 把E 委托到 D var E = Object.create( D ); // 手动把foo 的[[HomeObject]] 绑定到E,E.[[Prototype]] 是D, 因此 super() 是D.foo() E.foo = C.prototype.foo.toMethod( E, "foo" ); E.foo(); // "D.foo"
toMethod(..) 会复制方法并把homeObject 看成第一个参数(也就是咱们传入的E),第二个参数(可选)是新方法的名称(默认是原方法名)。
除此以外,开发者还有可能会遇到其余问题,这有待观察。不管如何,对于引擎自动绑定的super 来讲,你必须时刻警戒是否须要进行手动绑定。唉!
静态大于动态吗
经过上面的这些特性能够看出,ES6 的class 最大的问题在于,(像传统的类同样)它的语法有时会让你认为,定义了一个class 后,它就变成了一个(将来会被实例化的)东西的静态定义。你会完全忽略C 是一个对象,是一个具体的能够直接交互的东西。
在传统面向类的语言中,类定义以后就不会进行修改,因此类的设计模式就不支持修改。
可是JavaScript 最强大的特性之一就是它的动态性,任何对象的定义均可以修改(除非你把它设置成不可变)。
class 彷佛不同意这样作,因此强制让你使用丑陋的.prototype 语法以及super 问题,等等。并且对于这种动态产生的问题,class 基本上都没有提供解决方案。
换句话说,class 彷佛想告诉你:“动态太难实现了,因此这可能不是个好主意。这里有一种看起来像静态的语法,因此编写静态代码吧。”
对于JavaScript 来讲这是多么悲伤的评论啊:动态太难实现了,咱们伪装成静态吧。(可是实际上并非!)
总地来讲,ES6 的class 想假装成一种很好的语法问题的解决方案,可是实际上却让问题更难解决并且让JavaScript 更加难以理解。
总结
class 很好地假装成JavaScript 中类和继承设计模式的解决方案,可是它实际上起到了副作用:它隐藏了许多问题而且带来了更多更细小可是危险的问题。class 加深了过去20 年中对于JavaScript 中“类”的误解,在某些方面,它产生的问题比解决的多,并且让原本优雅简洁的[[Prototype]] 机制变得很是别扭。结论:若是ES6 的class 让[[Prototype]] 变得更加难用并且隐藏了JavaScript 对象最重要的机制——对象之间的实时委托关联,咱们难道不该该认为class 产生的问题比解决的多吗?难道不该该抵制这种设计模式吗?这些问题没法获得回答,可是但愿这里能从史无前例的深度分析这些问题,而且可以提供回答问题所需的全部信息。