从理论上讲,JavaScript并没有类。在实践中,下面的代码片段被广泛认为是JavaScript“类”的一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function Account () {
  this._currentBalance = 0;
}
 
Account.prototype.balance = function () {
  return this._currentBalance;
}
 
Account.prototype.deposit = function (howMuch) {
  this._currentBalance = this._currentBalance + howMuch;
  return this;
}
 
// ...
 
var account = new Account();

  这个模式可以被拓展以提供子类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function ChequingAccount () {
  Account.call(this);
}
 
ChequingAccount.prototype = Object.create(Account.prototype);
 
ChequingAccount.prototype.sufficientFunds = function (cheque) {
  return this._currentBalance >= cheque.amount();
}
 
ChequingAccount.prototype.process = function (cheque) {
  this._currentBalance = this._currentBalance - cheque.amount();
  return this;
}

  这些类和子类拥有类的大部分特性,就像Smalltalk语言中类的特性一样:

  • 类负责创建对象且用参数来初始化它们(比如当前余额)。
  • 类管理和拥有函数(方法),对象委托函数(方法)来处理它们的类(还有超级类)。
  • 函数(方法)直接操作对象的属性。

  这种模式在JavaScript文化中变得根深蒂固,ECMAScript-6——即将对JavaScript进行大修改的标准,提供了一些“糖衣语法”,因此在我们写类和子类的时候不用手工写完全部的模式内容。这对语义并没有太大的更改, 一切还是如我们看到的一样在后台运行得很好。

  当然,Smalltalk是四十多年前发明的,在这四十多年里,我们学会了关于哪些能或不能在面向对象程序设计中使用的很多问题。不幸的是,这种模式为做不了的事高兴,而却掩盖或忽略能做的事情。

  更不幸的是,即将到来的糖衣语法并没有解决关于类的任何问题,只解决了这些问题:“我希望能少敲点代码”这样的问题,或者对于新程序员来说“我不理解这些运动的部件实际上是如何工作的,所以我可以写错代码了,有没有更简单的方法来写这些代码呢?”。

  层次结构的语义问题

  在语义层面,类是本体的构建单元。下图的内容通常有有效的:

  基于类的面向对象编程的背后思想是将我们的对象知识分类(注意这个词)成树。在顶层的是有关所有对象的最一般知识,顺着树下来,我们得到越来越多的关于对象特定的类的特定知识,比如代表VisaDebit账号的对象。

  仅仅在编程上而已,真实的世界并不是那样。确实不是那样的。在形态学中,比如我们有,企鹅像鸟类那样游泳,蝙蝠像哺乳动物那样飞,像鸭嘴兽那样的单孔目动物是卵生的哺乳动物。

  事实证明我们有意义的领域(比如形态学或银行业务)的行为并没有能分类成很好的树,它形成一个有向非循环图。如果我们站在其中,那么它就是一片丛林。

  此外,在树形本体顶端构建软件的想法将要破灭,即使我们的知识能很整齐的构成一棵树。本体论不用来构建真实的世界,他们通过观察来描绘这个世界。随着认知不断的增长,我们也在不断的更新自己的本体论,有时能移动周围的一切。

  在软件中,这具有难以置信的破坏力:移动周围所有的东西会破坏所有的东西。在真实世界中,如果我们重新排列本体,卑微的鸭嘴兽并不介意,因为我们并没有用本体论来构建澳大利亚,只是用来描述我们的发现而已。

  通过观察像银行账号这样的事物来构建本体论是合理的。这种本体对于需求、用例、测试等来说是有用的。但这并不意味着它对实现银行账户代码书写有帮助。

  类层次结构是错误的语义模型,四十年的经验智慧让他们有更好的办法构建程序。

  封装

  这些都是语义问题。让我们来谈谈工程方面的问题,让我们来处理类就好像我们并不关心它们是否代表真实世界中的一些知识,让我们相信类仅仅只是让我们程序能够正常运行的一个工具而已。那么这些还是问题吗?

  类层次结构是一个问题,即使我们都想要做的是用它们来实现一些行为。程序有三个重要的规则:

  1.程序必须易于编写

  2.程序必须易于理解

  3.程序必须易于修改

  类要权衡所有这3个重要的规则,但类层次结构对遵从理解和改变程序是有害的,因为这种方式导致了封装问题。

  封装是面向对象编程一个核心的原则。(其它的编程风格,如函数式编程,也注重封装,即使以不同的方式实现)。在面向对象编程中,封装是由对象的私有状态和方法的公共接口来实现的。

  JavaScript并不强制要求私有状态,但能很容易写出封装很好的程序:只需避免一个对象直接操作另一个对象的属性。Smalltalk发明了四十多年后,这是一个很好理解的原则。

  显然,代码间将会有依赖性。A将依赖B,B将依赖C,且这些依赖是具有传递性的,所有A依赖B,那么A同时也依赖于C。封装并没有消除依赖关系,但确实还是限制了依赖的范围:如果我们改变B和/或C,假如我们没有改变或移动A调用的外部可视的方法,那么A就不会被破坏。

  到目前为止,一切都很好。或至少如果A、B、C是对象和/或方法。例如:

1
2
3
4
5
6
7
function depositAndReturnBalance(account, amount) {
  return account.deposit(amount).balance();
}
 
var account = new Account();
depositAndReturnBalance(account, 100)
  //=> 100

  很明显depositAndReturnBalance通过一个对象的传递实现了.deposit 和.balance方法。但这不依赖于这些方法是如何实现的:我们可以这样来写Account,也能得到相同功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function Account () {
  this._transactionHistory = [];
}
 
Account.prototype.balance = function () {
  return this._transactionHistory.reduce(function (acc, transaction) {
    return acc + transaction;
  }, 0);
}
 
Account.prototype.deposit = function (howMuch) {
  this._transactionHistory.unshift(howMuch)
  return this;
}
 
function depositAndReturnBalance(account, amount) {
  return account.deposit(amount).balance();
}
 
var account = new Account();
depositAndReturnBalance(account, 100)
  //=> 100

  .deposit 和.balance完全不同的实现方法,但depositAndReturnBalance并没有依赖于这些实现方法。

  所以,类给我们提供了封装“账户余额”实现的一种方法。太棒了!这有什么问题吗?

  父类没有被封装

  我们说过当实体只包含对象和/或方法时,封装能在JavaScript中实现。但是类呢?

  事实证明,类之间的层级关系是没有封装的。这是因为类之间没有通过明确定义的方法接口来进行关联,而是各自“隐藏”其内部状态。

  这是ChequingAccount子类实现.process函数的一种方法:

1
2
3
4
ChequingAccount.prototype.process = function (cheque) {
  this._currentBalance = this._currentBalance - cheque.amount();
  return this;
}

  如果我们用交易记录代替当前余额来重写Account类,这会破坏ChequingAccount的代码。在JavaScript(和同一家庭的其它语言)中类和子类共享对象私有属性的访问权。如果没有细致检查每一个子类和每一处调用子类的代码,那么改变Account的实现细节是不太可能的,因为改变私有属性将破坏它们。

  当然,我们知道代码间是存在关联的,所有子类依赖父类这并不让我们感到惊讶。但不同的是这种关联是不受方法和接口范围影响的。我们没有封装。

  这个问题并不是一个新的问题。这很好理解,它甚至有一个名字:叫做脆弱的基类问题。改变靠近继承树顶端的类会产生深远的影响,且这种影响是呈数量级排列的,还是因为没有封装。

  类继承会让程序变得难以修改且脆弱。

  展望未来

  JavaScript是在1995年首次露面的,约Smalltalk首次发布后的15年。从那以后的20年里,我们学习了很多关于JavaScript好的坏的东西,同时我们也学到了面向对象编程的很多好方法和坏主意。

  很明显,我们应该回顾和借鉴之前发生的事情。好的理念,比如封装,函数属于第一类对象,委托,特性和构成应该被包含进来且需要提升。新的理念,比如promises模式,应该得以发展。

  人家经常说“JavaScript不是Ruby”,因为它是基于原型的,不是基于类的。这确实是真的,但如果我们重造,那么优势将会丢失,如果将40年前创造的并一直延用的理念弃用,这很不好。

  所以当有人让你陈述如何写一个类层次结构的话,请告诉它们:别那么做!

  (在 hacker news/r/javascript, 和/r/programming上参与讨论)

在JavaScript里写类层次结构?别那么做!的更多相关文章

  1. JavaScript的写类方式(6)

    时间到了2015年6月18日,ES6正式发布了,到了ES6,前面的各种模拟类写法都可以丢掉了,它带来了关键字 class,extends,super. ES6的写类方式 // 定义类 Person c ...

  2. JavaScript里的类和继承

    JavaScript与大部分客户端语言有几点明显的不同: JS是 动态解释性语言,没有编译过程,它在程序运行过程中被逐行解释执行JS是 弱类型语言,它的变量没有严格类型限制JS是面向对象语言,但 没有 ...

  3. JavaScript里的类和继承(转)

    转自: http://www.h5cn.com/js/jishu/2016/0121/17634.html js与大部分客户端语言有几点明显的不同: JS是 动态解释性语言,没有编译过程,它在程序运行 ...

  4. JavaScript 面向对象编程(三)如何写类和子类

    在JavaScript面向对象编程(一)原型与继承和JavaScript面向对象编程(二)构造函数和类中,我们分别讨论了JavaScript中面向对象的原型和类的概念.基于这两点理论,本篇文章用一个简 ...

  5. javascript 和 CoffeeScript 里的类

    javascript不是面向对象的语言,它用函数来模拟类和继承. javascript里,提供一个类并不难: var Person,l4, z3; Person = function(name) { ...

  6. 千万别在Java类的static块里写会抛异常的代码!

    public class Demo{ static{ // 模拟会抛异常的代码 throw new RuntimeException(); } } 如果你在Java类的static块里写这样会抛异常的 ...

  7. JavaScript权威指南--类和模块

    知识要点 每个javascript对象都是一个属性集合,相互之间没有任何联系.在javascript中也可以定义对象的类,让每个对象都共享某些属性,这种“共享”的特性是非常有用的.类的成员或实例都包含 ...

  8. 前端要革命?看我在js里写SQL

    在日新月异的前端领域中,前端工程师能做的事情越来越多,自从nodejs出现后,前端越来越有革了传统后端命的趋势,本文就再补一刀,详细解读如何在js代码中执行标准的SQL语句 为什么要在js里写SQL? ...

  9. javascript继承(一)—类的属性研究

    本篇文章主要针对javascript的属性进行分析,由于javascript是一种基于对象的语言,本身没有类的概念,所以对于javascript的类的定义有很多名字,例于原型对象,构造函数等,它们都是 ...

随机推荐

  1. selenium + python 怎样才能滚到页面的底部?

    可以用 execute_script方法来处理这个. 调用原生javascript的API,这样你想滚到哪里就能滚到哪里. 下面的代码演示了如何滚到页面的最下面:   driver.execute_s ...

  2. DBA_实践指南系列2_Oracle Erp R12系统安装配置设定Setup(案例)

    2013-12-02 Created By BaoXinjian

  3. [Android&Java]浅谈设计模式-代码篇:观察者模式Observer

    观察者,就如同一个人,对非常多东西都感兴趣,就好像音乐.电子产品.Game.股票等,这些东西的变化都能引起爱好者们的注意并时刻关注他们.在代码中.我们也有这种一种方式来设计一些好玩的思想来.今天就写个 ...

  4. Ubuntu下设置环境变量

    Ubuntu下设置环境变量有三种方法,一种用于当前终端,一种用于当前用户,一种用于所有用户:   一:用于当前终端: 在当前终端中输入:export PATH=$PATH:<你的要加入的路径&g ...

  5. 办公技巧:局域网内设置固定ip

    第一步:查看自己现在的网络配置 打开命令行,输入:ipconfig /all 第二步:打开控制面板 - 网络配置 根据CMD命令的ipconfig信息对号入座填入即可. 然后,重启一下WIFI即可. ...

  6. mysql-5.7中的innodb_buffer_pool_prefetching(read-ahead)详解

    一.innodb的read-ahead是什么: 所谓的read-ahead就是innodb根据你现在访问的数据,推测出你接下来可能要访问的数据,并把它们(可能要访问的数据)读入 内存. 二.read- ...

  7. How to set JAVA environment variables in Linux or CentOS

    How to set JAVA environment variables JAVA_HOME and PATH in Linux After installing new java (jdk or ...

  8. CCMotionStreak(一)

    void MotionStreakTest1::onEnter() { MotionStreakTest::onEnter(); CCSize s = CCDirector::sharedDirect ...

  9. Spark的基本说明

    1.关于Application 用户程序,一个Application由一个在Driver运行的功能代码和多个Executor上运行的代码组成(工作在不同的节点上). 又分成多个Job,每个Job由多个 ...

  10. Leetcode: LRU Cache 解题报告

    LRU Cache  Design and implement a data structure for Least Recently Used (LRU) cache. It should supp ...