对象在JavaScript语言中使用十分广泛,学会如何有效地运用对象,有助于工作效率的提升。而不良的面向对象设计,可能会导致代码工程的失败,更严重的话还会引发整个公司悲剧。
不同于其它大部分语言,JavaScript是基于原型的对象系统,而不是基于类。遗憾的是,大多数JavaScript开发者对其对象系统理解不到位,或者难以良好地应用,总想按照类的方式使用,其结果将导致代码里的对象使用混乱不堪。所以JavaScript开发者最好对原型和类都能有所了解。
类继承和原型继承有何区别?
这个问题比较复杂,大家有可能会在评论区各抒己见、莫衷一是。因此,列位看官需要打起十二分的精神学习个中差异,并将所学良好地运用到实践当中去。
类继承:可以把类比作一张蓝图,它描绘了被创建对象的属性及特征。。
众所周知,使用new关键字调用构造函数可以创建类的实例。在ES6中,不用class关键字也可以实现类继承。像Java语言中类的概念,从技术上来说在JavaScript中并不存在。不过JavaScript借鉴了构造函数的思想。ES6中的class关键字,相当于是建立在构造函数之上的一种封装,其本质依旧是函数。
class Foo {}typeof Foo // 'function'
虽然JavaScript中的类继承的实现建立在原型继承之上,但是并不意味二者具有相同的功能:
JavaScript的类继承使用原型链来连接子类和父类的[[Prototype]],从而形成代理模式。通常情况下,super()_构造函数也会被调用。这种机制,形成了单一继承结构,以及面向对象设计中最紧密的耦合行为。
“类之间的继承关系,导致了子类间的相互关联,从而形成了基于层级的分类。”
原型继承:原型是工作对象的实例。对象直接从其他对象继承属性。
原型继承模式下,对象实例可以由多个对象源所组成。这样就使得继承变得更加灵活且[[Prototype]]代理层级较浅。换言之,对于基于原型继承的面向对象设计,不会产生层级分类这样的副作用这是区别于类继承的关键所在。
对象实例通常由工厂函数或者Object.create()来创建,也可以直接使用Object字面定义。
“原型是工作对象的实例。对象直接从其他对象继承属性。”
为什么搞清楚类继承和原型继承很重要?
继承,本质上讲是一种代码重用机制各种对象可以借此来共享代码。如果代码共享的方式选择不当,将会引发很多问题,如:
使用类继承,会产生父-子对象分类的副作用
这种类继承的层次划分体系,对于新用例将不可避免地出现问题。而且基类的过度派生,也会导致脆弱基类问题,其错误将难以修复。事实上,类继承会引发面向对象程序设计领域的诸多问题:
紧耦合问题(在面向对象设计中,类继承是耦合最严重的一种设计),紧耦合还会引发另一个问题:
脆弱基类问题
层级僵化问题(新用例的出现,最终会使所有涉及到的继承层次上都出现问题)
必然重复性问题(因为层级僵化,为了适应新用例,往往只能复制,而不能修改已有代码)
大猩猩-香蕉问题(你想要的是一个香蕉,但是最终到的却是一个拿着香蕉的大猩猩,还有整个丛林)
对于这些问题我曾做过深入探讨:“类继承已是明日黄花探究基于原型的面向对象编程思想”
“优先选择对象组合而不是类继承。” ~先驱四人,《设计模式:可复用面向对象软件之道》
里面很好地总结了:
是否所有的继承方式都有问题?
人们说“优先选择对象组合而不是继承”的时候,其实是要表达“优先选择对象组合而不是类继承”(引用自《设计模式》的原文)。该思想在面向对象设计领域属于普遍共识,因为类继承方式的先天缺陷,会导致很多问题。人们在谈到继承的时候,总是习惯性地省略类这个字,给人的感觉像是在针对所有的继承方式,而事实上并非如此。
因为大部分的继承方式还是很棒的。
三种不同的原型继承方式
在深入探讨其他继承类型之前,还需要先仔细分析下我所说的类继承。
你可以在Codepen上找到并测试下这段示例程序。
BassAmp继承自GuitarAmp,ChannelStrip继承自BassAmp和GuitarAmp。从这个例子我们可以看到面向对象设计发生问题的过程。ChannelStrip实际上并不是GuitarAmp的一种,而且它根本不需要一个cabinet的属性。一个比较好的解决办法是创建一个新的基类,供amps和strip来继承,但是这种方法依然有所局限。
到最后,采用新建基类的策略也会失效。
更好的办法就是通过类组合的方式,来继承那些真正需要的属性:
修改后的代码。
认真看这段代码,你就会发现:通过对象组合,我们可以确切地保证对象可以按需继承。这一点是类继承模式不可能做到的。因为使用类继承的时候,子类会把需要的和不需要的属性统统继承过来。
这时候你可能会问:“唔,是那么回事。可是这里头怎么没提到原型啊?”
客官莫急,且听我一步步道来~首先你要知道,基于原型的面向对象设计方法总共有三种。
拼接继承:是直接从一个对象拷贝属性到另一个对象的模式。被拷贝的原型通常被称为mixins。ES6为这个模式提供了一个方便的工具Object.assign()。在ES6之前,一般使用Underscore/Lodash提供的.extend(),或者jQuery中的$.extend(),来实现。上面那个对象组合的例子,采用的就是拼接继承的方式。
原型代理:JavaScript中,一个对象可能包含一个指向原型的引用,该原型被称为代理。如果某个属性不存在于当前对象中,就会查找其代理原型。代理原型本身也会有自己的代理原型。这样就形成了一条原型链,沿着代理链向上查找,直到找到该属性,或者找到根代理Object.prototype为止。原型就是这样,通过使用new关键字来创建实例以及Constructor.prototype前后勾连成一条继承链。当然,也可以使用Object.create()来达到同样的目的,或者把它和拼接继承混用,从而可以把多个原型精简为单一代理,也可以做到在对象实例创建后继续扩展。
函数继承:在JavaScript中,任何函数都可以用来创建对象。如果一个函数既不是构造函数,也不是class,它就被称为工厂函数。函数继承的工作原理是:由工厂函数创建对象,并向该对象直接添加属性,借此来扩展对象(使用拼接继承)。函数继承的概念最先由道格拉斯克罗克福德提出,不过这种继承方式在JavaScript中却早已有之。
这时候你会发现,拼接继承是JavaScript能够实现对象组合的秘诀,也使得原型代理和函数继承更加丰富多彩。
多数人谈起JavaScript面向对象设计时,首先想到的都是原型代理。不过你看,可不仅仅只有原型代理。要取代类继承,原型代理还是得靠边站,对象组合才是主角。
*为什么说对象组合能够避免脆弱基类问题
要搞清楚这个问题,首先要知道脆弱基类是如何形成的:
假设有基类A;
类B继承自基类A;
类C继承自B;
类D也继承自B;
在C中调用super方法,该方法将执行类B中的代码。同样,B也调用super方法,该方法会执行A中的代码。
C和D需要从A、B中继承一些无关联的特性。此时,D作为一个新用例,需要从A的初始化代码继承一些特性,这些特性与C的略有不同。为了应对以上需求,菜鸟开发人员会去调整A的初始化代码。于是乎,尽管D可以正常工作,但是C原本的特性被破坏了。
上面这个例子中,A和B为C和D提供各种特性。可是,C和D不需要来自A和B的所有特性,它们只是需要继承某些属性。但是,通过继承和调用super方法,你无法选择性地继承,只能全部继承:
“面向对象语言的问题在于,子类会携带有父类所隐含的环境信息。你想要的是一个香蕉,但是最终到的却是一个拿着香蕉的大猩猩,以及整个丛林”乔阿姆斯特朗《编程人生》
如果是使用对象组合的方式设想有如下几个特性:
feat1, feat2, feat3, feat4
C需要特性feat1和feat3,而D需要特性feat1,feat2,feat4:
const C = compose(feat1, feat3);const D = compose(feat1, feat2, feat4);
声明:文章版权归原作者所有 部分文章转自互联网 如有侵权请联系
[邮箱地址] 删除
|