继承与组合是面向对象的代码复用方式。了解各自的特点,可以让我们写出更简洁的代码,设计出更好的代码架构。这是serhiirubets翻译的文章《HowshouldIuseinheritanceandcomposition》这是一个常见的问题,不仅与JavaScript相关,而且在本文中我们只讨论与JavaScript相关的内容和示例。如果你不知道什么是组合或继承,我强烈建议你去看看,因为这篇文章主要是关于如何使用和如何选择它们,但只是为了确保我们在同一个频道,让我们先理解组合和继承。继承是面向对象编程的核心概念之一,它可以帮助我们避免代码重复。主要思想是我们可以创建一个包含可以被子类重用的逻辑的基类。我们来看一个例子:classElement{remove(){}setStyles(){}}classFormextendsElement{}classButtonextendsElement{}我们创建了一个基类“Element”,子类将继承公共元素在元素逻辑中。继承中存在is-a关系:Form是一个Element,Button也是一个Element。组合:与继承不同,组合使用has-a关系将不同的关系集合在一起。类汽车{构造函数(引擎,传输){this.engine=engine;this.transmission=传输;}}classEngine{constructor(type){this.type=type;}}classTransmission{构造函数(类型){这个。类型=类型;}}constpetrolEngine=newEngine('petrol');constautomaticTransmission=newEngine('automatic');constpassengerCar=newCar(petrolEngine,automaticTransmission);我们使用Engine和Transmission创建了Car,我们不能说Engine是Car,但我们可以说Car包含Engine。希望上面的例子可以帮助你理解什么是继承,什么是组合。我们再来看两个不同的例子,比较一下使用类方法实现继承和函数方法实现组合的区别。假设我们正在使用文件系统并希望实现读取、写入和删除功能。我们可以创建一个类:classFileService{constructor(filename){this.filename=filename;}read(){}write(){}remove(){}}目前做了我们想要的,我们可能要添加权限控制,有些用户只有读权限,有些用户可能有写权限。我们应该做什么?一种解决方案是我们可以将方法分为不同的类:classFileService{constructor(filename){this.filename=filename;}}classFileReaderextendsFileService{read(){}}classFileWriterextendsFileService{write(){}}classFileRemoverextendsFileService{remove(){}}现在每个用户都可以使用他们需要的权限,但是仍然有一个问题,如果我们需要同时给一些人分配读写权限怎么办?同时分配读取和删除权限怎么样?根据当前的实施,我们无法做到这一点。我们该如何解决呢?第一个想到的方案可能是:创建一个读写类,创建一个读写类。classFileReaderAndWriterextendsFileService{read(){}write(){}}classFileReaderAndRemoverextendsFileService{read(){}remove(){}}按照这种方法,我们可能还需要以下类:FileReader、FileWriter、FileRemove、FileReaderAndWriter,FileReaderAndRemover。这不是一个很好的实现方式:一,我们可能不仅有3个,还有10、20个方法,需要它们之间有很多组合。二是我们类中存在重复逻辑,FileReader类包含read方法,FileReaderAndWriter也包含相同的代码。这不是一个好的解决方案,还有其他方法可以实现吗?多重继承?JavaScript中没有这个特性,也不是一个好的解决方案:A类继承B类,B类可能继承其他类……这样的设计会很混乱,不是一个好的代码结构。如何解决?一种合理的方法是使用组合:我们将方法拆分为单独的函数工厂。functioncreateReader(){return{read(){console.log(this.filename)}}}functioncreateWriter(){return{write(){console.log(this.filename)}}}在上面的例子中,我们有两个函数可以创建可读可写的对象。现在我们可以在任何地方使用它们,或者组合它们:classFileService{constructor(filename){this.filename=filename;}}functioncreateReadFileService(filename){constfile=newFileService(filename);return{...file,...createReader()}}functioncreateWriteFileService(filename){constfile=newFileService(文件名);return{...file,...createWriter(),}}在上面的例子中,我们创建了读写服务,如果我们想组合不同的权限:读、写和删除,我们很容易做到:functioncreateReadAndWriteFileService(文件名){constfile=newFileService(文件名);}return{...file,...createReader(),...createWriter()}constfileForReadAndWriter=createReadAndWriteFileService('test');fileForReadAndWriter.read();fileForReadAndWriter.write();如果我们有5,10,20这样,我们就可以按照我们想要的方式组合,不会出现重复代码的问题,也不会混淆代码架构。让我们看另一个使用函数的例子。假设我们有很多员工,包括出租车司机、健身教练和司机:){return{name,canSport:true}}貌似没有问题,但是假设有些员工白天做健身教练,晚上出租出去,我们应该怎么调整呢?functioncreateDriverAndSportCoach(name){return{name,canSport:true,canDriver:true}}可以实现,但是像第一个例子,如果我们有多种类型混合,会产生很多重复的代码。我们可以通过组合重构:return{canSport:true}}现在我们可以根据需要组合所有的工作类型,没有代码重复,更容易理解:constdriver={...createEmployee('Alex',20),...createDriver()}constmanager={...createEmployee('Max',25),...createManager()}constsportCoach={...createEmployee('Bob',23),...createSportCoach()}constsportCoachAndDriver={...createEmployee('Robert',27),...createDriver(),...createSportCoach()}希望你现在明白了继承和组合的区别,一般来说继承可以用is-a关系,组合可以是用于has-a。但在实践中,继承有时并不是一个好的解决方案:就像例子中,司机是雇员(is-a关系),经理也是雇员。如果我们需要混合不同的部分,组合确实比继承好。合适的。最后,我想强调一下,继承和组合都很容易实现,但是要正确使用。有些场景组合可能更合适,反之亦然。当然,我们可以将继承和组合结合起来,比如说我们有一个is-a关系,但是想要添加不同的值或方法:我们可以创建一些基类,为实例提供所有通用功能,然后使用组合来添加其他特定功能。欢迎来到“混沌前端”公众号
