对于javascript来说,类是一种可选(而不是必须)的设计模式,而且在JavaScript这样的[[Prototype]] 语言中实现类是很蹩脚的。

这种蹩脚的感觉不只是来源于语法,虽然语法是很重要的原因。js里面有许多语法的缺点:繁琐杂乱的.prototype 引用、试图调用原型链上层同名函数时的显式伪多态以及不可靠、不美观而且容易被误解成“构造函数”的.constructor。

除此之外,类设计其实还存在更进一步的问题。传统面向类的语言中父类和子类、子类和实例之间其实是复制操作,但是在[[Prototype]] 中并没有复制。

对象关联代码和行为委托使用了[[Prototype]] 而不是将它藏起来,对比其简洁性可以看出,类并不适用于JavaScript。

class

不过并不需要再纠结于这个问题;可以看看ES6 的class机制。这里会介绍它的工作原理并分析class是否改进了之前提到的那些缺点。

下面是一个例子:

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 语法确实解决了典型原型风格代码中许多显而易见的语法问题。

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 产生的问题比解决的多吗?难道不应该抵制这种设计模式吗?
这些问题无法得到回答,但是希望这里能从前所未有的深度分析这些问题,并且能够提供回答问题所需的所有信息。

ES6中的Class的更多相关文章

  1. ES6中的模板字符串和新XSS Payload

    ES6中的模板字符串和新XSS Payload 众所周知,在XSS的实战对抗中,由于防守方经常会采用各种各样严格的过滤手段来过滤输入,所以我们使用的XSS Payload也会根据实际情况作出各种各样的 ...

  2. ES5和ES6中的继承 图解

    Javascript中的继承一直是个比较麻烦的问题,prototype.constructor.__proto__在构造函数,实例和原型之间有的 复杂的关系,不仔细捋下很难记得牢固.ES6中又新增了c ...

  3. ES6中块作用域之于for语句是怎样的?

    在ES6中新加了快作用域的概念(C语言就有,作为类c语言的js,当然应该加上),算是很好理解. { let i; } console.log(i);// i is not defined 在代码块当中 ...

  4. ES6中的var let const应如何选择

    javascript世界里面的每个人都在说有关ECMAScript 6 (ES6,也称作ES 2015)的话题,对象的巨大变化 ( 类 , super() , 等), 函数 (默认参数等), 以及模块 ...

  5. Nodejs与ES6系列4:ES6中的类

    ES6中的类 4.1.class基本语法 在之前的javascript语法中是不存在class这样的概念,如果要通过构造函数生成一个新对象代码 function Shape(width,height) ...

  6. ES6中Arguments和Parameters用法解析

    原文链接 译文 ECMAScript 6 (也称 ECMAScript 2015) 是ECMAScript 标准的最新版本,显著地完善了JS中参数的处理方式.除了其它新特性外,我们还可以使用rest参 ...

  7. ES5和ES6中对于继承的实现方法

    在ES5继承的实现非常有趣的,由于没有传统面向对象类的概念,Javascript利用原型链的特性来实现继承,这其中有很多的属性指向和需要注意的地方. 原型链的特点和实现已经在之前的一篇整理说过了,就是 ...

  8. ES6中的高阶函数:如同 a => b => c 一样简单

    作者:Sequoia McDowell 2016年01月16日 ES6来啦!随着越来越多的代码库和思潮引领者开始在他们的代码中使用ES6,以往被认为是"仅需了解"的ES6特性变成了 ...

  9. 深入理解 JavaScript 异步系列(3)—— ES6 中的 Promise

    第一部分,Promise 加入 ES6 标准 原文地址 http://www.cnblogs.com/wangfupeng1988/p/6515855.html 未经作者允许不得转载! 从 jquer ...

随机推荐

  1. pig hive 区别

    Pig是一种编程语言,它简化了Hadoop常见的工作任务.Pig可加载数据.表达转换数据以及存储最终结果.Pig内置的操作使得半结构化数据变得有意义(如日志文件).同时Pig可扩展使用Java中添加的 ...

  2. iOS开发:XCTest单元测试(附上一个单例的测试代码)

    测试驱动开发并不是一个很新鲜的概念了.在我最开始学习程序编写时,最喜欢干的事情就是编写一段代码,然后运行观察结果是否正确.我所学习第一门语言是c语言,用的最多的是在算法设计上,那时候最常做的事情就是编 ...

  3. Newtonsoft.Json(C#处理json)

    转载自:http://blog.csdn.net/wswqiang/article/details/6783161 using Newtonsoft.Json; using System.Text; ...

  4. 315Mhz、433Mhz无线遥控信号的解码分析和模拟

    摘要 前段时间学习无线电的同时了解到arduino是作为技能尚未成熟技术宅的我继树莓派又一个不错的选择.于是花了200元购得3块arduino开发板(2*nano&1*uno)和其他传感器等, ...

  5. 如何垂直居中一个<img>?

    <!doctype html><html> <head> <meta charset="UTF-8"> <meta name= ...

  6. Kafka实战系列--Kafka API使用体验

    前言: kafka是linkedin开源的消息队列, 淘宝的metaq就是基于kafka而研发. 而消息队列作为一个分布式组件, 在服务解耦/异步化, 扮演非常重要的角色. 本系列主要研究kafka的 ...

  7. Linux 安装 node

    在 Linux 上安装 node,使用 Linux 编译后的版本最佳. 1.进入 node 官网,找到 Linux 版本下载,这里我们右键复制下载地址即可. 2.在 Linux 上,使用命令 curl ...

  8. Streaming replication slots in PostgreSQL 9.4

    Streaming replication slots are a pending feature in PostgreSQL 9.4, as part of the logical changese ...

  9. Cookie初识篇

    一.什么是Cookie Cookie,有时也用其复数形式Cookies,指某些网站为了辨别用户身份.进行session跟踪而储存在用户本地终端上的数据(通常经过加密).定义于RFC2109和2965都 ...

  10. Docker registry V2

    部署私有Docker Registry 搭建 Insecure Registry 修改Registry server上的Docker daemon的配置,为DOCKER_OPTS增加–insecure ...