阅读原文
在我们进入这个主题之前,请允许我先介绍一下我自己。我想在这结束之前,你可能会想知道我是谁。
我是Eric Elliott,《Programming JavaScript Applications》(O’Reilly)的作者,正篇长度的纪录片电影《编程素养》的制作主办人,以及在线JavaScript课程《Learn JavaScript With Eric Elliott》系列的创造者。
我曾为多家公司的软件体验做出过贡献,包括Adobe Systems, Zumba Fitness, The Wall Street Journal, ESPN, BBC,还有顶级唱片艺术家Usher, Frank Ocean, Metallica等还有很多。
曾几何时
我被困在黑暗之中,我很迷茫,拖拉着脚步,撞到东西,摔东西,总是把我碰到的东西都弄的一团糟。
在90年代,我用C++,Delphi和Java编程,为一套后来叫做Maya的软件编写3D插件(Maya被许多主要的电影制作工作室用来制作暑期大片)。
然后事情发生了:互联网突然崛起。每个人都开始建造网站,并且在写作和编辑一些在线杂志后,一个朋友让我相信,未来的网络将会是SaaS(在这个词被创造出来之前)产品。我当时并不知道,但是那微妙的方向变化,从根本上改变了我对编程的思考方式。因为如果你想创造一个好的SaaS产品,你不得不学习JavaScript。
一旦我学会了,我绝不会回头。突然,所有事情都变得简单了。我开发的软件更具可塑性,代码不需要被重写就可以存活的更久。起初,我认为JavaScript主要是UI脚本胶水,但是当我学习了cookies和AJAX,事情再一次改变了。
我上瘾了,我已经回不去了,JavaScript给了我其它语言缺乏的东西:
Freedom!
JavaScript是有史以来最重要的编程语言之一,不是简单的因为它的流行,而是因为它普及了编程演变过程中极其重要的两大范例。
- 原型继承(没有类的对象,原型代理,又被称为OLOO-Objects Linking to Other Objects);
- 函数式编程(使用lambda表达式和闭包)。
我喜欢把这两个编程范例统称为JavaScript两大支柱,并且我毫不感到羞愧的承认我被它们宠坏了,我不想使用没有它们的语言编程。
JavaScript将被作为一个有史以来最有影响力的语言之一。许多其他的语言已经复制了原型继承或者函数式编程或者都有,这两大支柱改变了我们编写应用的方式,即使使用其他语言。
Brendan Eich(JavaScript之父)并没有发明这两大支柱中的任何一个,但是JavaScript大量展示了它们的编程实践。两大支柱同等重要,但我担心的是大量的JavaScript程序员完全缺少其中一个或两个,因为如果你不费心好好学习,JavaScript很容易让你编写糟糕的代码。
这实际上是一个特点,因为它让拿起JavaScript并开始用它做些有用的东西变得真的很容易,但是作为一个JavaScript程序员,那样的开发阶段不应该超过一年。
如果你还没有,现在是时候升级了。
如果你现在还在创建构造函数并且从它们继承,你还没有学会JavaScript。你是否从1995年就开始使用并不重要,你确实还未能利用JavaScript最强大的能力。
你正在使用的是虚假的JavaScript,这只是把这门语言打扮的更像Java。
你正在使用一个令人惊异的,改变游戏规则的,开创性的编程语言但是你完全失去了让它变得如此酷和有趣的东西。
我们正在构造混乱
“那些不知道自己在黑暗中行走的人,永远也不会寻求光明。”~李小龙
构造函数违反了打开/关闭原则。因为它们将所有的调用者耦合进对象如何实例化这样的细节中。制作一个html5游戏?想从新的对象实例改变去使用对象池以便你可以重新利用对象和阻止垃圾回收器破坏你的帧速率?这太糟了,你会打破所有的调用者,或者你最终会得到一个行动不便的工厂函数。
如果你从构造函数中返回一个任意的对象,它会破坏你的原型链,在构造函数中,this
关键字不会被绑定到新对象的实例。它比一个真正的工厂函数缺少灵活性因为你在工厂中并不能使用this
;它只是被扔掉。
没有运行在严格模式下的构造函数也会变得非常危险。如果一个调用者忘记了new
而且你没有使用严格模式或者ES6 class
[叹气],你给this
赋值的任何东西都会污染全局命名空间,那是可怕的。
在严格模式出现之前,这个语言故障在我工作过的两家不同的初创公司都引起过难以发现的bugs,就在关键的成长时期,当我们没有很多额外的时间去追逐寻找错误的时候。
在JavaScript中,工厂函数就是简单的构造函数减去new
的需要,全局污染的危险以及不方便的限制。(包括那个恼人的首字母大写惯例。) JavaScript不需要构造函数因为任何方法都可以返回一个新的对象。有动态对象扩展,对象字面量以及Object.create()
,我们有我们需要的一切,没有混乱。this
的行为就和在其他函数中一样。Hurray!
欢迎来到第七层地狱
“我不那么痛苦,因为它是明智的。”~T.H. White
每个人都听说过温水煮青蛙的故事:如果你把青蛙放入沸水中,它会立即跳出来。如果你把青蛙放在冷水中并逐渐提升温度,它会被煮死在热水中,因为它并没有意识到危险。在这个故事里,我们就是青蛙。
如果构造函数行为是煎锅的话,经典继承不是火;它是来自但丁的第七层地狱的火!
猩猩/香蕉问题
“使用面向对象语言的问题是,他们已经得到了所有这一隐含的环境,他们都随身携带。你想要一个香蕉但是你得到的是一只大猩猩拿着香蕉和整个丛林。”~ Joe Armstrong
经典继承通常只能让你从单一的祖先继承,强制你进入麻烦的分类系统。我说很麻烦是因为如果不失败,我在大型应用中所见过的所有的面向对象设计分类最终都是错误的。
假设你从两个类开始:工具和武器。你已经完蛋了——你不能制作游戏《线索》。
紧耦合问题
子类与父类之间的耦合关系是面向对象设计中最紧的形式,这与可重用,模块化的代码是相对的。
对某个类的小的改变会造成起伏的副作用,这会破坏那些原本应该完全无关的东西。
必要的重复问题
对分类问题的一次明显的解决办法是及时返回,通过改变什么继承了什么构建只有微小差别的新类。但是因为偶和过紧从而难以正确的提取与重构。你最终复制了代码而不是重用它。你违反了DRY原则(Don’t Repeat Yourself)。
结果是,你继续增加你的只有少量不同的类的丛林,当你继续添加继承的层次,你会有越来越多的关节炎和骨质疏松。当你发现一个bug,你不能在一个地方修改它,你得到处修改它。
“哎呀,少改一个。”~每一个经典面向对象程序员
这在面向对象设计圈中被称为必要的重复问题。
ES6的类型并没有解决上述任何一个问题,甚至让它们更糟,因为这些坏的主意会被官方文档正式的确定,还会被写到上千本书和博客中。
class
关键字很有可能会成为JavaScript中最有害的功能。我非常尊重那些曾经参与标准化工作的聪明和勤奋的人,但即使是聪明的人也会偶尔做错事。比如说在浏览器控制台中输入.1+.2
。总的来说,我仍然认为Brendan Eich极大地促进了web,编程语言还有计算机科学。
P.S. 不要使用super
,除非你喜欢通过调试器进入多个层次的继承抽象。
后果
当你的应用程序不断地成长,这些问题有一个倍增的效果,并且最终,唯一的解决办法是从头开始重写应用程序或完全放弃它——有时企业只需要减少它的损失。
我已经见过这个过程不停的重演,一个接一个的工作,一个接一个的项目。我们会学到什么吗?
在我曾经工作过的公司,为了要重写而导致软件发布日期跳票了一整年。我相信更新,而不是重写。在我担任顾问的另一家公司,这几乎使整个公司都崩溃了。
这些问题不只是一个品味或风格的问题。这个选择可以使你的产品成功或者失败。
大公司通常会慢步就班的行驶就像没什么有问题,但是初创公司不能像这样让他们的轮子行驶在问题之上,当他们正在有限的轨道里努力寻找自己的产品/市场。
在那些完全避免经典继承的现代代码库中,我都没有发现上述的任何一个问题。
进入光明
“完美不是没有更多东西可以添加,而是没有更多东西可以减少。”~Antoine de Saint-Exupéry
前不久,我在开发一个新的库,用来在我的书《Programming JavaScript Applications》中展示如何使用原型继承,那是我决定采用一个有趣的想法:用一个工厂函数,它可以帮助你产生一些工厂函数,你可以继承或者组合这些工厂函数。我把这些可组合的工厂叫做“stamps”,这个库叫做“Stampit”。库非常小并且非常简单。我在2013年的O’Reilly Fluent Conference上做了一个关于Stampit的演讲,并且写了一篇关于stamps的博客。
现在有一个小的,但是稳定增长的开发者社区,他们的编码风格已经被stamps改变。Stampit被多个月活跃用户达数百万的应用使用到生产环境中。
当然,Stampit并不是唯一的选择。Douglas Rockford从不使用new
或者this
,而是选择一种完全函数式的方法来代码重用。
所有他的对象都只是无状态的函数包,或者是像关联数组那样的只有数据没有方法的对象。这种方式工作的很好,除非你正在创建成百上千的对象并且你需要你的应用运行流畅或者近乎实时(例如游戏引擎,实时信号处理器等等)。在那些场景下,对方法的代理调用可以把你从大量的手动内存管理中解救出来。
其他好的可选方案包括更好的使用JavaSCript的模块来代替继承(我推荐npm和ES6模块配合Browserify或者WebPack),或者简单的从源对象复制属性到新对象来克隆一个(Object.assign()
, $.extend()
, _.extend()
等等)。
复制机制是原型继承的另一种形式。克隆属性的源是一种特定的原型类型,被叫做范例原型(exemplar prototypes),并且克隆一个范例原型被称为拼接继承(concatenative inheritance)。
即使你听从Douglas Rockford的建议停止使用this
,你仍然可以用原型的方式做事。拼接继承有可能是因为JavaScript有一个叫做动态对象扩展(dynamic object extension)的功能:在对象被实例化之后可以继续向对象添加属性和方法的功能。
在JavaScript中你永远都不需要类型(classes),并且我从没见到过一个场景class
比替代方案更好。如果你想到任何一个,留下评论,但是到现在我做这个挑战已经很多年了,没有一个人能提出一个好的应用案例,只是一个站不住脚的关于微优化的争论和风格偏好。
当我告诉别人构造函数和经典继承是糟糕的时候,他们就会产生防卫心理。我并没有攻击你,我在试着帮助你。
人们对自己的编程风格有着高度的重视,就像他们的编码方式是他们表达自己的方式一样。胡说八道。
你用代码所创造的东西才是你如何表达你自己的方式。
至于它是如何实现的,根本没有关系,除非它被实现的不好。
件开发中唯一重要的事情就是你的用户喜欢这个软件。
我可以警告你前方有一个悬崖,但有些人不相信有危险,直到他们获得第一手体验。不要犯那样的错误,代价会是巨大的。这是你从错误中吸取的机会,在几十年的时间里,无数的人都犯了错误。所有的书都写过这些问题。
由四人帮所著的具有开创性的《设计模式》是由两个基本原则构建的:
- 面向接口编程,而不是面向实现;
- 优先对象组合而不是类型继承。
因为子类面向父类的视线编程,第二个原则又跟随第一个原则,但是详细解释是有用的。
经典的面向对象设计的开创性工作是反类继承的。
它含有一整部份的对象创建模式,只存在关于构造函数和类继承的局限性的工作中。
谷歌一下“new considered harmful,” “inheritance considered harmful,” and “super is a code smell.” 你可以找到数十篇文章,从博客到受人尊敬的刊物像Dr. Dobb’s Journal,一直到JavaScript被发明之前,都说了同样的事情:new
,脆弱的经典继承分类系统和父子耦合会引起灾难性的后果。
即使是James Gosling,Java的创造者,也承认Java没有正确的实现对象。
想要紧跟JavaScript参考手册?Douglas Crockford把Object.create()
添加到语言中,所以他不会使用new
。
Kyle Simpson(作者 “You Don’t Know JS”)写了令人着迷的有三部份的系列博客,关于“JS Objects: Inherited a Mess.”。
Kyle认为原型继承是反类型的,这更简单并且比类型更好。他甚至创造了术语OLOO(Objects Linked to Other Objects)来阐明原型代理和类型继承的区别。
好代码是简单的
“简单就是减去了明显的,并增加了有意义的。”~John Maeda
当你从JavaScript中除去了构造函数和经典继承,它:
- 变得更简单(容易读写,没有错误的设计分类系统);
- 变得更灵活(切换新的实例,回收对象池或代理?没问题);
- 变得更强大更有表现力(从多个祖先继承?继承私有状态?没问题)。
更好的选择
“如果一个功能有时是危险的,而且有一个更好的选择,那么总是使用更好的选择。”~Douglas Crockford
我并没有试图拿走你有用的工具,我是在警告你你所认为的是一个工具实际上是一个足枪(自杀用的)。在构造函数和类型的情况下,其实还有更好的选项。
程序员使用的另一种常见的观点是,应该由他们决定该如何表现自己,好像把代码风格上升到艺术或者时尚的水平。这种说法是纯粹的感情用事和非理性的:
你的代码并不是你自我表达的产品就像画家啊的画笔并不是他们自我表达的产品。代码是工具。程序是产品。
是的,有些代码本身就是一种艺术,但是如果它不在纸上单独发表,你的代码就不会被归为这种类型的。否则,就你的用户而言,代码只是一个黑盒子,他们所喜爱的是你的程序。
良好的编程风格要求当你有一个选择优雅,简单,灵活,或另一个选择是复杂的,笨拙的,和限制性,你选择前者。我知道对语言功能的开放是很受欢迎的,但是这有正确的方式和错误的方式。
选择正确的方式。
~ Eric Elliott
原文链接有相关视频。
网友评论