书名:JavaScript设计模式
ISBN:978-7-115-39686-0
本书由人民邮电出版社发行数字版。版权所有,侵权必究。
您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。
我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。
如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。
张容铭百度资深高级web前端研发工程师,长期工作在web前端一线上,现研发与维护百度图片搜索业务,曾主导百度新首页项目改版等。 喜欢钢琴,素描,而又常带着他的单反各地旅行。
本书卖点
国内第一本专业的、讲解最全面JavaScript的Web前端设计模式书
阿里巴巴、百度前端高级工程师鼎力推荐
Web前端界第一本以一种讲故事方式讲解每一种模式,让每位读者都能读懂。
书中案例都是实际工作中的真实案例,让读者身临其境。
全面涵盖专门针对JavaScript的36个设计模式,帮助读者尽快提高开发效率
深入剖析面向对象的设计原则及代码重构,帮助读者快速融入团队项目开发中
本书通过职场主人公“小铭”实战历练,介绍了他从菜鸟到高级程序员的蜕变过程,值得每一个程序员借鉴和学习!
各种设计模式的原则和准确定义、应用方法和最佳实践
本书共分6篇40章。首先讨论了几种函数的编写方式,体会JavaScript在编程中的灵活性;然后讲解了面向对象编程的知识,其中讨论了类的创建、数据的封装以及类之间的继承;最后探讨了各种模式的技术,如简单工厂模式,包括工厂方法模式、抽象工厂模式、建造者模式、原型模式、单例模式、外观模式,以及适配器模式。本书还讲解了几种适配器、代理模式、装饰者模式和MVC模式,讨论了如何实现对数据、视图、控制器的分离。在讲解MVP模式时,讨论了如何解决数据与视图之间的耦合,并实现了一个模板生成器;讲解MVVM模式时,讨论了双向绑定对MVC的模式演化。本书几乎包含了关于JavaScript设计模式的全部知识,是进行JavaScript高效编程必备的学习手册。
本书适合JavaScript初学者、前端设计者、JavaScript程序员学习,也可以作为大专院校相关专业师生的学习用书,以及培训学校的教材。
认识张容铭是在2012年年底的时候,那时张容铭来公司实习,大家都觉得这小伙子实力不俗,而且很爱钻研。得知张容铭利用业余时间完成了本书的创作,作为他的朋友,真替他感到高兴!短短几年,进步如此迅速,在前端实战开发方面有着这么多的积累,有时也会让我自愧不如。
在Web应用日益丰富的今天,越来越多的JavaScript被运用在我们的网页中。随着用户体验日益受到重视,前端的可维护性、前端性能对用户体验的影响开始备受关注,因此,如何编写高效的、可维护的代码,成为众多互联网公司争相研究的对象。
本书通过情境对话的方式,详细地介绍了各种设计模式的原则、准确定义、应用方法和最佳实践,全方位比较各种同类模式之间的异同,详细讲解不同模式的使用方法。
“极具趣味,容易理解,但讲解又极为严谨和透彻”是本书的写作风格和最大特点。希望大家在学到知识的同时,能够感受到作者的风趣幽默。
最后,希望本书能够帮助业界同仁打造出更为卓越的Web产品。
阿里巴巴集团 高级前端研发工程师 王鹏飞
在百度工作的时间里,和张容铭共事过一段时光,在相处的过程中就发现张容铭对设计模式的研究和应用有很大的热情,投入精力很多,且在应用上有很好的理解和收获。本书可以说是张容铭多年来积累的技术和经验的总结,本书涵盖了绝大多数设计模式;本书写作上很有特色,采用新人与导师对话的方式,风趣幽默、通俗易懂,让读者易学、易用、易理解,非常适合JavaScript初学者和前段开发工程师学习。
百度 高级前端研发工程师 杨坤
一年前如果有人问我是否会写一本书的话,我会直接而坦诚地说我不会。不过随着团队开发中,对同事编写的代码的阅读中我发现,有的人写的代码难懂且臃肿,很难继续编写下去;有的人写的代码简明而灵活,即使再多的需求也很容易实现。我一直在思考,为何为同一需求编写的代码会有这么大的差别?
随着团队项目的开发,我发现,当对类似的需求以类似的模板去解决时,开发成本会减少许多,而且他人也会轻松介入项目的开发。这样,按照同样的流程去解决问题,开发效率得以提高。而将这些解决问题的模板提炼出来,会发现复杂的问题也会简单许多,书写的代码思路清晰且结构简明,这些模板是一种解决问题的方式,或者说是一种模式。
复杂的问题可以分解成一个个小的模块,然后像拼图一样将这些通过模式解决的模块拼凑成一个完整的需求。同时可将余下的精力用去研究其他烦琐问题的解决模式。积攒的模式越多,在工作中以这些模式来解决问题,工作效率就越高。
于是我将这些模式总结出来,编写成一本JavaScript设计模式书。考虑到对技术的探讨有时是很枯燥的,为了降低读者的学习难度,不至于在阅读中出现倦意,我把工作实战中的角色引入书中,通过他们工作中的情境故事来表达每种模式的内涵与应用,也借此希望更多的人读懂,并领悟更多的设计模式,以便应用到自己的项目中。
本书不是一本入门级别的书,本书适用于希望将自身JavaScript经验技巧提升一个层次的读者,所以,本书对JavaScript基础知识点,如数据类型、运算符、语句等未进行讨论。本书将面向如下4类读者。
第一类读者有点JavaScript基础,想要更深入地学习JavaScript,并成为一名标准Web开发人员或者前端工程师。想深入了解面向对象编程思想,并且提高自己模块化开发能力,写出可维护、高效率、可拓展的代码的程序员。
第二类读者主要是以前从事Java或者C++等编程语言的程序员,现如今想转行从事前端开发,他们可能对于JavaScript这种语言比较陌生,但是对于面向对象思想以及设计模式了解较多,只是对于将这些思想运用于前端不是十分清楚,因此,通过阅读本书,对于他们实现前端编程开发很有意义。
第三类读者主要是对设计模式感兴趣,并且想更多了解设计模式在JavaScript高效应用的研发人员。通过阅读本书,他们可以体会JavaScript中设计模式的实现,突破以往面向对象语言中的实现,用更具灵活的方式解决问题。
第四类读者主要是那些从事前端开发的专业人员。他们能熟练应用JavaScript开发,但是还希望提升自己,使自己在团队开发中更具有价值。阅读本书后,他们可以更深入地了解面向对象编程,掌握各种设计模式,使自己的编程技术更灵活,他们会懂得在何种情况下使用那种设计模式解决问题效果会更佳。因此,他们可以自由而熟练地运用设计模式重构现有的代码,使其更灵活、高效、可拓展,即使出现复杂的问题也会编写出一目了然、结构清晰的代码。
诚然会有一些不了解JavaScript以及设计模式的读者。他们可能很难看懂书中示例的代码。因此,本书用一种更通俗易懂的方法编写,力求深入浅出,尽可能让更多不同层面的读者理解。
本书突破以往填鸭式著书风格,以生动有趣的故事情节推出一个个精彩的设计模式实践。文中以大学刚毕业的小白同学的编程工作经历为主线,在阅读时可以跟着他的经历来学习这些设计模式的具体应用。对于每种模式我们首先提出该模式的定义,这也就声明了该模式的用途。随后交代应用背景,这往往就是该种设计模式的某种应用场境。随着故事的演进,小白所经历的往往是读者在项目中所经历的,因此,很有可能会遇到小白所遇到的问题,这也正是我们需要学习的地方。最后,通过项目经理、小铭等人的帮助使小白顺利地解决一道道难题,从而使小白从初学者一步步进入了工程师的角色。
本书分为6篇,共40章。第一篇主要讲述JavaScript面向对象编程基础知识,章节之间知识点连贯,因此,建议读者顺序阅读,并且该篇也是后续5篇的基础,因此,一定要按顺序阅读。后5篇则是讲述各个设计模式,因此,读者可以根据自己的兴趣选择性阅读。但5篇各自侧重点不同,第二篇主要讲述创建型模式,第三篇主要讲述结构型模式,第四篇主要讲述行为型模式,第五篇主要讲述技巧型模式,第六篇主要讲述架构型模式。
第一篇包括第1章和第2章。
第1章介绍JavaScript基础知识,讨论了几种函数编写方式,让读者体会JavaScript在编程中的灵活性。
第2章介绍面向对象编程,讨论了类的创建、数据的封装以及类之间的继承。
第二篇包括第3章到第8章。
第3章介绍简单工厂模式,讨论了对象创建的几种方式。
第4章介绍工厂方法模式,讨论了创建多类对象以及一种安全的创建方式。
第5章介绍抽象工厂模式,讨论了抽象类以及如何定义一种类簇。
第6章介绍建造者模式,讨论了如何更灵活地创建一种复杂的对象。
第7章介绍原型模式,讨论了JavaScript的核心继承方式——原型式继承。
第8章介绍单例模式,讨论了单例对象及其实现与用途。
第三篇包括第9章到第15章。
第9章介绍外观模式,讨论了如何通过外观模式简化接口的使用。
第10章介绍适配器模式,讨论了几种用途的适配器。
第11章介绍代理模式,讨论了代理思想对于跨域的解决方案。
第12章介绍装饰者模式,讨论了装饰者模式更友好地对于已有功能的拓展。
第13章介绍桥接模式,讨论了桥接模式解决对象之间的依赖。
第14章介绍组合模式,讨论了组合模式如何优化系统的可拓展性。
第15章介绍享元模式,讨论了享元模式如何优化系统、提高性能。
第四篇包括第16章到第26章。
第16章介绍模板方法模式,讨论了基于模板类的拓展与创建。
第17章介绍观察者模式,讨论了观察者模式解决团队开发中的模块间通信的实践。
第18章介绍状态模式,讨论了状态模式中状态在交互中的保存与执行。
第19章介绍策略模式,讨论了策略模式如何丰富交互算法。
第20章介绍职责链模式,讨论了如何实现一个需求链。
第21章介绍命令模式,讨论了如何定义命令集合及运用。
第22章介绍访问者模式,讨论了借助已有对象解决已有问题。
第23章介绍中介者模式,讨论了中介者如何管理对象之间的通信交互。
第24章介绍备忘录模式,讨论了如何更好地处理数据缓存问题。
第25章介绍迭代器模式,讨论了迭代器的易用性及其对性能的优化。
第26章介绍解释器模式,讨论了通过解释器解决规定的需求。
第五篇包括第27章到第34章。
第27章介绍链模式,讨论了一种高效的方法调用模式。
第28章介绍委托模式,讨论了事件委托对性能的优化。
第29章介绍数据访问对象模式,讨论了数据访问对象模式对数据库操作对象的封装。
第30章介绍节流模式,讨论了如何优化页面中的高频事件以及交互动画。
第31章介绍简单模板模式,讨论了一种新的生成页面视图的方法。
第32章介绍惰性模式,讨论了对方法的加载以及执行的优化。
第33章介绍参与者模式,讨论了一种宽松地为对象绑定方法的方式。
第34章介绍等待者模式,讨论了对于异步执行方法回调函数的处理。
第六篇包括第35章到第40章。
第35章介绍同步模块模式,讨论了如何模块化封装代码。
第36章介绍异步模块模式,讨论了一种更适合的前端模块化开发实践方式。
第37章介绍Widget模式,讨论了当今流行的组件式开发,并实现了一个简单的模板引擎。
第38章介绍MVC模式,讨论了如何实现对数据、视图、控制器的分离。
第39章介绍MVP模式,讨论了如何解决数据与视图之间的耦合,并实现了一个模板生成器。
第40章介绍MVVM模式,讨论了双向绑定对MVC的模式演化。
标 题 |
意 义 |
---|---|
模式定义 |
指明该模式的定义与用途 |
故事背景 |
指明该模式的某种使用场合 |
故事情节 |
通过该模式解决的一类问题 |
下章剧透 |
点明下一章故事内容 |
忆之获 |
回顾本章知识点 |
我问你答 |
给出问题,请读者思考与解答,并深入体会该模式 |
设计模式是工作经验的结晶,如此多的模式是我一个人无法做到的,因此,本书取得的成果是在前人工作经验总结的基础上提出的。能够完成本书需要感谢太多太多的人。
从百度空间,到百度首页,从百度翻译,再到百度图片搜索,期间经历了太多的团队,得到了太多同事的帮助,因此要感谢每一个人。感谢慧总、冬叔、璇姐、辉哥。感谢慧总让我加入百度工作;感谢冬叔对我工作的支持;感谢璇姐对我的关怀与帮助,让我们的团队气氛无比融洽;感谢辉哥为我提供百度的图搜工作机会,你的决策使我更加坚定前端的工作。
百度新首页的重构是我工作中经历的最难忘的一段日子,感谢大家给予的帮助,感谢坤哥、周全、鹏飞、锡月、王晨、亚斌、研婷。
除了自己所在团队同事给予的帮助,还要感谢很多帮助过我的人,他们是王潇、尚实、先烈、王凯、冯振兴等。
感谢我的新团队,王群、尊程、晓晨、乔岳、腾飞、茗名、李毅、佳佳、阳阳、潇潇、琳琳、胜敏、王敏。有你们在,工作和生活变得如此融洽。
感谢我的老师,刘嘉敏老师,是你让我认知了计算机世界。
本书能够出版最该感谢的就是人民邮电出版社,感谢我们这次融洽的合作,尤其要感谢张涛编辑,没有你对我的支持与帮助,本书可能不会这么顺利地出版,你是一名专业的编辑。当然还要感谢本书默默无闻的编辑,你们辛苦的审校才使本书顺利地出版。
最后,在此感谢我的家人,你们培养了我,从一个对计算机一无所知的孩子,到如今的一名工程师,感谢你们,感谢你们对我的付出。你们虽然对我的工作不是很了解,但每天依旧是那么关心、支持着我。希望你们每天健康而开心地生活。
本书读者答疑QQ群为:471118627。
编辑联系邮箱:zhangtao@ptpress.com.cn。
面向对象编程(Object-oriented programming,OOP)是一种程序设计范型。它将对象作为程序的基本单元,将程序和数据封装其中,以提高程序的重用性、灵活性和扩展性。
第1章 灵活的语言——JavaScript
第2章 写的都是看到的——面向对象编程
结束了4年的大学学习生活,小白信心满满地来到应聘的M公司。今天是入职的第一天,项目经理分下来一个验证表单功能的任务,内容不多,仅需要验证用户名、邮箱、密码等。
小白接到需求看了看,感觉很简单,于是便写下几个函数。
function checkName(){
// 验证姓名
}
function checkEmail(){
// 验证邮箱
}
function checkPassword(){
// 验证密码
}
......
于是要把自己的代码提交到团队项目里。
正在此时,一位工作多年的程序员小铭看到小白要提交的代码摇了摇头说:“小白,等一下,先不要提交。”
“怎么了?”
“你创建了很多全局变量呀。”
“变量?我只是写了几个函数而已。”
“函数不是变量么?”小铭反问道。
此时小白不知所措,心想:“难道函数是变量?”脸瞬间沉了下来。
小铭见此情形忙笑着说:“别着急,你看,如果我这么声明几个变量来实现你的功能你看可以么?”
var checkName = function(){
// 验证姓名
}
var checkEmail = function(){
// 验证邮箱
}
var checkPassword = function(){
// 验证密码
}
“一样的,只不过……”
“对,只不过这个在用的时候要提前声明,但是这么看你就会发现你创建了3个函数保存在变量里来实现你的功能,而你写的是将你的变量名放在function后面而已,它也代表了你的变量。所以说你也声明了3个全局变量。”
“这有什么问题呢?”
“从功能上讲当然没问题,但是今天你加入了我们的团队,在团队开发中你所写的代码就不能只考虑自己了,也要考虑不影响到他人,如果别人也定义了同样的方法就会覆盖掉原有的功能了。如果你定义了很多方法,这种相互覆盖的问题是很不容易察觉到的。”
“那我应该如何避免呢?”小白问道。
“你可以将它们放在一个变量里保存,这样就可减少覆盖或被覆盖的风险,当然一旦被覆盖,所有的功能都会失效,这种现象也是很明显的,你自然也会很轻易觉察到。”
“可是我该如何做呢?”小白迫不及待地追问道。
“一猜你就会问。”
“好吧,请你先简单地说一下。”
“对象你知道吧,它有属性和方法,而如果我们要访问它的属性或者方法时,可通过点语法向下遍历查询得到。我们可以创建一个检测对象,然后把我们的方法放在里面。”
var CheckObject = {
checkName : function(){
// 验证姓名
},
checkEmail : function(){
// 验证邮箱
},
checkPassword : function(){
// 验证密码
}
}
“此时我们将所有的函数作为CheckObject 对象的方法,这样我们就只有一个对象,而我们要想使用它们也很简单,比如检测姓名CheckObject.checkName(),只是在我们原来使用的函数式前面多了一个对象名称。”
“哦,这样呀,但是我们既然可以通过点语法来使用方法,我们是不是也可以这么创建呢?”
“当然,不过首先你要声明一个对象,然后给它添加方法,当然在JavaScript中函数也是对象,所以你可以这么做:”
var CheckObject = function(){};
CheckObject.checkName = function(){
// 验证姓名
}
CheckObject.checkEmail = function(){
// 验证邮箱
}
CheckObject.checkPassword = function(){
// 验证密码
}
“使用和前面的方式是一样的,比如CheckObject.checkName(),”小铭接着说,“现在虽然能满足你的需求,但当别人想用你写的对象方法时就有些麻烦了,因为这个对象不能复制一份,或者说这个对象类在用new关键字创建新的对象时,新创建的对象是不能继承这些方法的。”
“但是复制又有什么用呢?”小白不解地问道。
“给你举个例子吧,假如你喜欢设计模式,你买了这本书,然后回去你的小伙伴看见了,感觉很有用,他们也想要怎么办?书就这一本。但如果你买的是一台打印机,那么好吧,即使你的小伙伴再多,你也有能力给他们每个人打印一本。”
“哦,有些明白了,但是我该如何做到呢?”
小铭解释说:“如果你想简单地复制一下,你可以将这些方法放在一个函数对象中。”于是小铭将代码写下。
var CheckObject = function(){
return {
checkName : function(){
// 验证姓名
},
checkEmail : function(){
// 验证邮箱
},
checkPassword : function(){
// 验证密码
}
}
}
小白看了看代码,思考一下说:“哦,你写的看上去是,当每次调用这个函数的时候,把我们之前写的那个对象返回出来,当别人每次调用这个函数时都返回了一个新对象,这样执行过程中明面上是CheckObject 对象,可实际上是返回的新对象。这样每个人在使用时就互不影响了。比如想检测邮箱可以像这样吧。”
var a = CheckObject ();
a.checkEmail ();
“嗯,对”小铭接着说,“虽然通过创建了新对象完成了我们的需求,但是他不是一个真正意义上类的创建方式,并且创建的对象a和对象CheckObject没有任何关系(返回出来的对象本身就与CheckObject对象无关),所以我们还要对其稍加改造一下。”
var CheckObject = function(){
this.checkName = function(){
// 验证姓名
}
this.checkEmail = function(){
// 验证邮箱
}
this.checkPassword = function(){
// 验证密码
}
}
“像上面这样的对象就可以看成类了。”小铭继续说。
“那么我们使用它还像之前那样创建对象的方法创建么?”小白追问道。
“不,既然是一个类,你就要用关键字new来创建了。”
var a = new CheckObject();
a.checkEmail();
“这样你就可以用CheckObject类创建出来的对象了。”
“如果我和我的小伙伴们都对类实例化了(用类创建对象),那么我们每个人都会有一套属于自己的方法吧。”小白不解地问道。
“当然,你看,我们是把所有的方法放在函数内部了,通过this定义的,所以每一次通过new关键字创建新对象的时候,新创建的对象都会对类的this上的属性进行复制。所以这些新创建的对象都会有自己的一套方法,然而有时候这么做造成的消耗是很奢侈的,我们需要处理一下。”
var CheckObject = function(){};
CheckObject.prototype.checkName = function(){
// 验证姓名
}
CheckObject.prototype.checkEmail = function(){
// 验证邮箱
}
CheckObject.prototype.checkPassword = function(){
// 验证密码
}
“这样创建对象实例的时候,创建出来的对象所拥有的方法就都是一个了,因为它们都要依赖prototype原型依次寻找,而找到的方法都是同一个,它们都绑定在CheckObject对象类的原型上,”小铭继续说,“这种方式我们要将prototype写很多遍,所以你也可以这样做。”
var CheckObject = function(){};
CheckObject.prototype = {
checkName : function(){
// 验证姓名
},
checkEmail : function(){
// 验证邮箱
},
checkPassword : function(){
// 验证密码
}
}
“但有一点你要记住,这两种方式不能混着用,否则一旦混用,如在后面为对象的原型对象赋值新对象时,那么它将会覆盖掉之前对prototype对象赋值的方法。”小铭补充说。
“知道了,不过我们要使用这种方式定义的类是不是要像下面这样呢?”小白问道。
var a = new CheckObject();
a.checkName();
a.checkEmail();
a.checkPassword();
“没错,但是你发现没,你调用了3个方法,但是你对对象a书写了3遍。这是可以避免的,那就要在你声明的每一个方法末尾处将当前对象返回,在JavaScript中this指向的就是当前对象,所以你可以将它返回。例如我们开始写的第一个对象还记得么?改动它很简单,像下面这样就可以。”
var CheckObject = {
checkName : function(){
// 验证姓名
return this;
},
checkEmail : function(){
// 验证邮箱
return this;
},
checkPassword : function(){
// 验证密码
return this;
}
}
“此时我们要想使用他就可以这样:”
CheckObject.checkName().checkEmail().checkPassword();
“当然同样的方式还可以放到类的原型对象中。”
var CheckObject = function(){};
CheckObject.prototype = {
checkName : function(){
// 验证姓名
return this;
},
checkEmail : function(){
// 验证邮箱
return this;
},
checkPassword : function(){
// 验证密码
return this;
}
}
“但使用时候也要先创建一下:”
var a = new CheckObject();
a.checkName().checkEmail().checkPassword();
小白回顾着这些从未见过的代码方式内心很激动,小铭见小白对JavaScript如此着迷,于是补充了两句。
“如果你看过prototype.js的代码,我想你会想到下面的书写方式。”
“prototype.js是什么?”小白问道。
“一款JavaScript框架,里面为我们方便地封装了很多方法,它最大的特点就是对源生对象(JavaScript语言为我们提供的对象类,如Function、Array、Object等等)的拓展,比如你想给每一个函数都添加一个检测邮箱的方法就可以这么做。”
Function.prototype.checkEmail = function(){
// 验证邮箱
}
“这样你在使用这个方法的时候就比较方便了,如果你习惯函数形式,那么你可以这么做。”
var f = function(){};
f.checkEmail();
“如果你习惯类的形式你也可以这么做。”
var f = new Function();
f.checkEmail();
“但是你这么做在我们这里是不允许的,因为你污染了原生对象Function,所以别人创建的函数也会被你创建的函数所污染,造成不必要的开销,但你可以抽象出一个统一添加方法的功能方法。”
Function.prototype.addMethod = function(name, fn){
this[name] = fn;
}
“这样如果你想添加邮箱验证和姓名验证方法你可以这样做。”
var methods = function(){};
或者
var methods = new Function();
methods.addMethod('checkName', function(){
// 验证姓名
});
methods.addMethod('checkEmail', function(){
// 验证邮箱
});
methods.checkName();
methods.checkEmail();
“呀,这种方式很奇特呀。不过我想链式添加方法,是不是在addMethod中将this返回就可以呀,这么做可以么?”
Function.prototype.addMethod = function(name, fn){
this[name] = fn;
return this;
}
“当然,所以你再想添加方法就可以这样了:”
var methods = function(){};
methods.addMethod('checkName', function(){
// 验证姓名
}).addMethod('checkEmail', function(){
// 验证邮箱
});
“那么,小白,我问你,我如果想链式使用你知道该如何做么?”
小白想了想说:“既然添加方法的时候可以将this返回实现,那么添加的每个方法将this返回是不是可以实现呢?”
于是小白这么写下:
var methods = function(){};
methods.addMethod('checkName', function(){
// 验证姓名
return this;
}).addMethod('checkEmail', function(){
// 验证邮箱
return this;
});
然后测试一下:
methods.checkName().checkEmail();
“真的可以呀!”小白兴奋地说。
“可是在你测试的时候,你用的是函数式调用方式?对于习惯于类式调用方式的同学来说,他们可以这样简单更改一下。”
Function.prototype.addMethod = function(name, fn){
this.prototype[name] = fn;
}
“此时我们还按照上一种方式添加方法。”
var Methods = function(){};
methods.addMethod('checkName', function(){
// 验证姓名
}).addMethod('checkEmail', function(){
// 验证邮箱
});
“但是我们在使用的时候要注意了,不能直接使用,要通过new关键字来创建新对象了。”
var m = new Methods();
m.checkEmail()
小白兴奋地看着这一行行的代码情不自禁地叫了一声“这正是一种艺术”。
小铭笑着说:“JavaScript是一种灵活的语言,当然函数在其中扮演着一等公民。所以使用JavaScript,你可以编写出更多优雅的艺术代码。”
在欢乐的学习中小白的第一天工作结束了,兴奋、痴迷、感慨。明天小白将去看看同事们丰富的编程世界。在那时小白将领略封装、继承的魅力。
小白工作第一天的故事结束,通过对小白与小铭对函数的多样化创建与使用,我们对JavaScript这门语言有了新的认识,“灵活性”是这门语言特有的气质,不同的人可以写出不同风格的代码,这是JavaScript给予我们的财富,不过我们要在团队开发中慎重挥霍,尽量保证团队开发代码风格的一致性,这也是团队代码易开发、可维护以及代码规范的必然要求。
真假对象一节中如何实现方法的链式调用呢?
试着定义一个可以为函数添加多个方法的addMethod方法。
试着定义一个既可为函数原型添加方法又可为其自身添加方法的addMethod方法。
第一天的经历使小白深深认识到校园学到的知识与实际工作中的偏差,所以想见识见识公司团队里大家都是如何书写代码并完成需求的。早晨走进公司的时候恰巧遇见了项目经理。
“早!小白,今天是你来的第二天,这一周你熟悉一下我们团队的项目吧。”项目经理对小白说。
“好呀,项目经理,我也正想跟大家学习学习呢。”于是项目经理带着小白将项目中的代码下载下来。可小白打开一看傻眼了:“函数,昨天探讨的函数呢?”小白想了半天还是没找到自己以前熟悉的代码。于是走去问小铭:“为何大家在解决需求时都不按照需求规定的功能写函数呢?怎么都是一个一个对象呢?”
“函数?对象?看来你还是习惯于按照传统流程编写一个一个函数来解决需求的方式。昨天跟你说过,那样做不利于团队开发,比如你昨天写的3个对输入框中输入的数据校验功能方法,用了3个函数,这是一种面向过程的实现方式,然而在这种方式中,你会发现无端地在页面中添加了很多全局变量,而且不利于别人重复使用。一旦别人使用以前提供的方法,你就不能轻易地去修改这些方法,这不利于团队代码维护。因此你现在要接受咱们团队这边的编程风格——面向对象编程。”
“面向对象编程?我不太理解,你可以跟我说一说么?”小白问道。
“面向对象编程就是将你的需求抽象成一个对象,然后针对这个对象分析其特征(属性)与动作(方法)。这个对象我们称之为类。面向对象编程思想其中有一个特点就是封装,就是说把你需要的功能放在一个对象里。比如你大学毕业你来公司携带的行李物品没有一件一件拿过来,而是要将他们放在一个旅行箱里,这样不论携带还是管理都会更方便一些。遗憾的是对于JavaScript这种解释性的弱类型语言没有经典强类型语言中那种通过class等关键字实现的类的封装方式,JavaScript中都是通过一些特性模仿实现的,但这也带来了极高的灵活性,让我们编写的代码更自由。”
“在JavaScript中创建一个类很容易,首先声明一个函数保存在一个变量里。按编程习惯一般将这个代表类的变量名首字母大写。然后在这个函数(类)的内部通过对this(函数内部自带的一个变量,用于指向当前这个对象)变量添加属性或者方法来实现对类添加属性或者方法,例如:”
var Book = function(id, bookname, price){
this.id = id;
this.bookname= bookname;
this.price = price;
}
“也可以通过在类的原型(类也是一个对象,所以也有原型prototype)上添加属性和方法,有两种方式,一种是一一为原型对象属性赋值,另一种则是将一个对象赋值给类的原型对象。但这两种不要混用。例如:”
Book.prototype.display = function(){
// 展示这本书
};
或者
Book.prototype = {
display : function(){}
};
“这样我们将所需要的方法和属性都封装在我们抽象的Book类里面了,当使用功能方法时,我们不能直接使用这个Book类,需要用new关键字来实例化(创建)新的对象。使用实例化对象的属性或者方法时,可以通过点语法访问,例如:”
var book = new Book(10, 'JavaScript设计模式', 50);
console.log(book.bookname) // JavaScript设计模式
小白看了看对类添加的属性和方法部分,感觉不是很理解,于是问:“通过this添加的属性和方法同在prototype中添加的属性和方法有什么区别呀?”
“通过this添加的属性、方法是在当前对象上添加的,然而JavaScript是一种基于原型prototype的语言,所以每创建一个对象时(当然在JavaScript中函数也是一种对象),它都有一个原型prototype用于指向其继承的属性、方法。这样通过prototype继承的方法并不是对象自身的,所以在使用这些方法时,需要通过prototype一级一级查找来得到。这样你会发现通过this定义的属性或者方法是该对象自身拥有的,所以我们每次通过类创建一个新对象时,this指向的属性和方法都会得到相应的创建,而通过prototype继承的属性或者方法是每个对象通过prototype访问到,所以我们每次通过类创建一个新对象时这些属性和方法不会再次创建。(如图2-1所示)。”
“哦,对了,解析图中的constructor又是指的什么呀。”
“constructor是一个属性,当创建一个函数或者对象时都会为其创建一个原型对象prototype,在prototype对象中又会像函数中创建this一样创建一个constructor属性,那么constructor属性指向的就是拥有整个原型对象的函数或对象,例如在本例中Book prototype中的constructor属性指向的就是Book类对象。”
▲图2-1 原型对象prototype
“原来是这样,”小白似乎明白些,“面向对象思想在学校里也学过,说的就是对一些属性方法的隐藏与暴露,比如私有属性、私有方法、共有属性、共有方法、保护方法等等,那么JavaScript中也有这些么?”
“你能想到这些很好。说明你有一定面向对象的基础了。不过你说的这些在JavaScript中没有显性的存在,但是我们可以通过一些灵活的技巧来实现它。”小铭继续解释说,“面向对象思想你可以想象成一个人,比如一位明星为了在社会中保持一个良好形象,她就会将一些隐私隐藏在心里,然而对于这位明星,她的家人认识她,所以会了解一些关于她的事情。外界的人不认识她,即使外界人通过某种途径认识她也仅仅了解一些她暴露出来的事情,不会了解她的隐私。如果想了解更多关于她的事情怎么办?对,还可以通过她的家人来了解,但是这位明星自己内心深处的隐私是永远不会被别人知道的。”
“那么在JavaScript中又是如何实现的呢?”小白问。
“由于JavaScript的函数级作用域,声明在函数内部的变量以及方法在外界是访问不到的,通过此特性即可创建类的私有变量以及私有方法。然而在函数内部通过this创建的属性和方法,在类创建对象时,每个对象自身都拥有一份并且可以在外部访问到。因此通过this创建的属性可看作是对象共有属性和对象共有方法,而通过this创建的方法,不但可以访问这些对象的共有属性与共有方法,而且还能访问到类(创建时)或对象自身的私有属性和私有方法,由于这些方法权利比较大,所以我们又将它看作特权方法。在对象创建时通过使用这些特权方法我们可以初始化实例对象的一些属性,因此这些在创建对象时调用的特权方法还可以看作是类的构造器。如下面的例子。”
// 私有属性与私有方法,特权方法,对象公有属性和对象共有方法,构造器
var Book = function(id, name, price){
//私有属性
var num = 1;
//私有方法
function checkId(){
};
//特权方法
this.getName = function(){};
this.getPrice = function(){};
this.setName = function(){};
this.setPrice = function(){};
//对象公有属性
this.id = id;
//对象公有方法
this.copy = function(){};
//构造器
this.setName(name);
this.setPrice(price);
};
小白心中暗喜:“原来是这样呀,通过JavaScript函数级作用域的特征来实现在函数内部创建外界就访问不到的私有化变量和私有化方法。通过new关键字实例化对象时,由于对类执行一次,所以类的内部this上定义的属性和方法自然就可以复制到新创建的对象上,成为对象公有化的属性与方法,而其中的一些方法能访问到类的私有属性和方法,就像例子中家人对明星了解得比外界多,因此比外界权利大,因而得名特权方法。而我们在通过new关键字实例化对象时,执行了一遍类的函数,所以里面通过调用特权方法自然就可以初始化对象的一些属性了。可是在类的外部通过点语法定义的属性和方法以及在外部通过prototype定义的属性和方法又有什么作用呢?”
“通过new关键字创建新对象时,由于类外面通过点语法添加的属性和方法没有执行到,所以新创建的对象中无法获取他们,但是可以通过类来使用。因此在类外面通过点语法定义的属性以及方法被称为类的静态共有属性和类的静态共有方法。而类通过prototype创建的属性或者方法在类实例的对象中是可以通过this访问到的(如图2.1新创建的对象的proto指向了类的原型所指向的对象),所以我们将prototype对象中的属性和方法称为共有属性和共有方法,如:”
//类静态公有属性(对象不能访问)
Book.isChinese = true;
//类静态公有方法(对象不能访问)
Book.resetTime = function(){
console.log('new Tiem')
};
Book.prototype = {
//公有属性
isJSBook : false,
//公有方法
display : function(){}
}
“通过new关键字创建的对象实质是对新对象this的不断赋值,并将prototype指向类的prototype所指向的对象,而类的构造函数外面通过点语法定义的属性方法是不会添加到新创建的对象上去的。因此要想在新创建的对象中使用isChinese就得通过Book类使用而不能通过this,如Book.isChinese,而类的原型prototype上定义的属性在新对象里就可以直接使用,这是因为新对象的prototype和类的prototype指向的是同一个对象。”
于是小白半信半疑地写下了测试代码:
var b = new Book(11,'JavaScript设计模式',50);
console.log(b.num); // undefined
console.log(b.isJSBook); // false
console.log(b.id); // 11
console.log(b.isChinese); // undefined
“真的是这样,类的私有属性num以及静态共有属性isChinese在新创建的b对象里是访问不到的。而类的共有属性isJSBook在b对象中却可以通过点语法访问到。”
“但是类的静态公有属性isChinese可以通过类的自身访问。”
console.log(Book.isChinese); // true
Book.resetTime(); // new Tiem
“有时我们经常将类的静态变量通过闭包来实现。”
// 利用闭包实现
var Book = (function() {
//静态私有变量
var bookNum = 0;
//静态私有方法
function checkBook(name) {
}
//返回构造函数
return function(newId, newName, newPrice) {
//私有变量
var name, price;
//私有方法
function checkID(id){}
//特权方法
this.getName = function(){};
this.getPrice = function(){};
this.setName = function(){};
this.setPrice = function(){};
//公有属性
this.id = newId;
//公有方法
this.copy = function(){};
bookNum++
if(bookNum > 100)
throw new Error('我们仅出版100本书.');
//构造器
this.setName(name);
this.setPrice(price);
}
})();
Book.prototype = {
//静态公有属性
isJSBook : false,
//静态公有方法
display : function(){}
};
“小白,你知道闭包么?”
“不太了解。你能说说么?”
“闭包是有权访问另外一个函数作用域中变量的函数,即在一个函数内部创建另外一个函数。我们将这个闭包作为创建对象的构造函数,这样它既是闭包又是可实例对象的函数,即可访问到类函数作用域中的变量,如bookNum这个变量,此时这个变量叫静态私有变量,并且checkBook()可称之为静态私有方法。当然闭包内部也有其自身的私有变量以及私有方法如price,checkID()。但是,在闭包外部添加原型属性和方法看上去像似脱离了闭包这个类,所以有时候在闭包内部实现一个完整的类然后将其返回,看下面的例子。”
// 利用闭包实现
var Book = (function() {
//静态私有变量
var bookNum = 0;
//静态私有方法
function checkBook(name) {}
//创建类
function _book(newId, newName, newPrice) {
//私有变量
var name, price;
//私有方法
function checkID(id){}
//特权方法
this.getName = function(){};
this.getPrice = function(){};
this.setName = function(){};
this.setPrice = function(){};
//公有属性
this.id = newId;
//公有方法
this.copy = function(){};
bookNum++
if(bookNum > 100)
throw new Error('我们仅出版100本书.');
//构造器
this.setName(name);
this.setPrice(price);
}
//构建原型
_book.prototype = {
//静态公有属性
isJSBook : false,
//静态公有方法
display : function(){}
};
//返回类
return _book;
})();
“哦,这样看上去更像一个整体。”
“对于你们初学者来说,在创建对象上由于不适应这种写法,所以经常容易忘记使用new而犯错误。”
“可是对于我们来说,这种错误发生也是不可避免的,毕竟不像你们工作了这么多年。但是你有什么好办法么?”
“哈哈,那是当然,如果你们犯错误有人实时监测不就解决了么,所以赶快找一位检察长吧。比如JavaScript在创建对象时有一种安全模式就完全可以解决你们这类问题。”
// 图书类
var Book = function(title, time, type){
this.title = title;
this.time = time;
this.type = type;
}
// 实例化一本书
var book = Book('JavaScript', '2014', 'js');
“小白,你猜book这个变量是个什么?”
“Book类的一个实例吧。”为了验证自己的想法,小白写下测试代码。
console.log(book); // undefined
“怎么会是这样?为什么是一个undefined(未定义)?”小白不解。
“别着急,你来看看我的测试代码。”
console.log(window.title); // JavaScript
console.log(window.time); // 2014
console.log(window.type); // js
“怎么样发现问题了么”,小铭问道。
“明明创建了一个Book对象,并且添加了title、time、type3个属性,怎么会添加到window上面去了,而且book这个变量还是undefined。”小白又看了看实例中的代码恍然大悟,“哦,原来是忘记了用new关键字来实例化了,可是为什么会出现这个结果呢?”
“别着急,首先你要明白一点,new关键字的作用可以看作是对当前对象的this不停地赋值,然而例子中没有用new,所以就会直接执行这个函数,而这个函数在全局作用域中执行了,所以在全局作用域中this指向的当前对象自然就是全局变量,在你的页面里全局变量就是window了,所以添加的属性自然就会被添加到window上面了,而我们这个book变量最终的作用是要得到Book这个类(函数)的执行结果,由于函数中没有return语句,这个Book类自然不会告诉book变量的执行结果了,所以就是undefined(未定义)。”
“原来是这样,看来创建时真是不小心呀,可是该如何避免呢?”小白感叹道。
“‘去找位检察长’呀,哈哈,使用安全模式吧。”
// 图书安全类
var Book = function(title, time, type){
// 判断执行过程中this是否是当前这个对象(如果是说明是用new创建的)
if(this instanceof Book){
this.title = title;
this.time = time;
this.type = type;
// 否则重新创建这个对象
}else{
return new Book(title, time, type);
}
}
var book = Book('JavaScript', '2014', 'js');
“好了小白,测试一下吧。”
console.log(book); // Book
console.log(book.title); // JavaScript
console.log(book.time); // 2014
console.log(book.type); // js
console.log(window.title); // undefined
console.log(window.time); // undefined
console.log(window.type); // undefined
“真的是这样呀,太好了,再也不用担心创建对象忘记使用new关键字的问题了。”
“好了说了很多,你也休息一下,好好回顾一下,后面还有个更重要的面向对象等着你——继承,这可是许多设计模式设计的灵魂。”
“小白,看继承呢?”小铭忙完自己的事情走过来。
“是呀,刚才学习类,发现每个类都有3个部分,第一部分是构造函数内的,这是供实例化对象复制用的,第二部分是构造函数外的,直接通过点语法添加的,这是供类使用的,实例化对象是访问不到的,第三部分是类的原型中的,实例化对象可以通过其原型链间接地访问到,也是为供所有实例化对象所共用的。然而在继承中所涉及的不仅仅是一个对象。”
“对呀,不过继承这种思想却很简单,如千年文明能够流传至今靠的就是传承,将这些有用的文化一年一年地流传下来,又如我们祖先一代一代地繁衍,才有了今天的我们。所以继承涉及的不仅仅是一个对象。如人类的传宗接代,父母会把自己的一些特点传给孩子,孩子具有了父母的一些特点,但又不完全一样,总会有自己的特点,所以父母与孩子又是不同的个体。”
“可是JavaScript并没有继承这一现有的机制,它又是如何实现的呢?”
“对呀,也正因为JavaScript少了这些显性的限制才使得其具有了一定的灵活性,所以我们可以根据不同的需求实现多样式的继承。比如常见的类式继承。”
// 类式继承
// 声明父类
function SuperClass(){
this.superValue = true;
}
// 为父类添加共有方法
SuperClass.prototype.getSuperValue = function(){
return this.superValue;
};
// 声明子类
function SubClass(){
this.subValue = false;
}
// 继承父类
SubClass.prototype = new SuperClass();
// 为子类添加共有方法
SubClass.prototype.getSubValue = function (){
return this.subValue;
};
“很像,真的很像!”小白很惊讶。
“像什么?”小铭不解地问。
“刚才看过的封装呀,不同的是这里声明了2个类,而且第二个类的原型prototype被赋予了第一个类的实例。”小白解释道。
“很对,继承很简单,就是声明2个类而已,不过类式继承需要将第一个类的实例赋值给第二个类的原型。但你知道为何要这么做么?”
“类的原型对象的作用就是为类的原型添加共有方法,但类不能直接访问这些属性和方法,必须通过原型prototype来访问。而我们实例化一个父类的时候,新创建的对象复制了父类的构造函数内的属性与方法并且将原型proto指向了父类的原型对象,这样就拥有了父类的原型对象上的属性与方法,并且这个新创建的对象可直接访问到父类原型对象上的属性与方法。如果我们将这个新创建的对象赋值给子类的原型,那么子类的原型就可以访问到父类的原型属性和方法。”小白还有些不自信。
“对,你分析得很准确。补充一点,你说的新创建的对象不仅仅可以访问父类原型上的属性和方法,同样也可访问从父类构造函数中复制的属性和方法。你将这个对象赋值给子类的原型,那么这个子类的原型同样可以访问父类原型上的属性和方法与从父类构造函数中复制的属性和方法。这正是类式继承原理。”
“原来是这样,但是我们要如何使用子类呢?”小白问道。
“使用很简单,像下面这样即可。”小铭说。
var instance = new SubClass();
console.log(instance.getSuperValue()); //true
console.log(instance.getSubValue()); //false
“另外,我们还可以通过instanceof来检测某个对象是否是某个类的实例,或者说某个对象是否继承了某个类。这样就可以判断对象与类之间的继承关系了。”小铭补充说。
“instanceof?它如何就知道对象与类之间的继承关系呢?”小白不解。
“instanceof是通过判断对象的prototype链来确定这个对象是否是某个类的实例,而不关心对象与类的自身结构。”
“原来是这样。”于是小白写下测试代码。
console.log(instance instanceof SuperClass); //true
console.log(instance instanceof SubClass); //true
console.log(SubClass instanceof SuperClass); //false
“我们说subClass继承superClass,可是为什么SubClass instanceof SuperClass得到的结果是false呢?”小白不解。
“前面说了,instanceof是判断前面的对象是否是后面类(对象)的实例,它并不表示两者的继承,这一点你不要弄混,其次我们看看前面的代码,你看我们在实现subClass继承superClass时是通过将superClass的实例赋值给subClass的原型prototype,所以说SubClass.prototype继承了superClass。”小铭解释说。
于是小白半信半疑地写下测试代码。
console.log(SubClass.prototype instanceof SuperClass); //true
“真的是这样。”小白惊呼。
“是呀。这也是类式继承的一个特点。问你一个问题,你所创建的所有对象都是谁的实例?”
“Object吗?”小白回答说。
“对,正是JavaScript为我们提供的原生对象Object,所以你看下面的检测代码返回的就是true。”
console.log(instance instanceof Object); //true
“哦,这么说Object就是所有对象的祖先了。”小白笑着说。
“哈哈,可是你知道吗,这种类式继承还有2个缺点。其一,由于子类通过其原型prototype对父类实例化,继承了父类。所以说父类中的共有属性要是引用类型,就会在子类中被所有实例共用,因此一个子类的实例更改子类原型从父类构造函数中继承来的共有属性就会直接影响到其他子类,比如你看下面的代码。”
function SuperClass(){
this.books = ['JavaScript', 'html', 'css'];
}
function SubClass(){}
SubClass.prototype = new SuperClass();
var instance1 = new SubClass();
var instance2 = new SubClass();
console.log(instance2.books); // ["JavaScript", "html", "css"]
instance1.books.push('设计模式');
console.log(instance2.books); // ["JavaScript", "html", "css", "设计模式"]
“instance1的一个无意的修改就会无情地伤害了instance2的book属性,这在编程中很容易埋藏陷阱。其二,由于子类实现的继承是靠其原型prototype对父类的实例化实现的,因此在创建父类的时候,是无法向父类传递参数的,因而在实例化父类的时候也无法对父类构造函数内的属性进行初始化。”
“那我们要如何解决这些问题呢?”小白好奇地追问。
“别着急,JavaScript是灵活的,自然也会有其他继承方法来解决,比如常见的构造函数继承。”
//构造函数式继承
// 声明父类
function SuperClass(id){
// 引用类型共有属性
this.books = ['JavaScript', 'html', 'css'];
// 值类型共有属性
this.id = id;
}
// 父类声明原型方法
SuperClass.prototype.showBooks = function(){
console.log(this.books);
}
// 声明子类
function SubClass(id){
// 继承父类
SuperClass.call(this, id);
}
// 创建第一个子类的实例
var instance1 = new SubClass(10);
// 创建第二个子类的实例
var instance2 = new SubClass(11);
instance1.books.push("设计模式");
console.log(instance1.books); // ["JavaScript", "html", "css", "设计模式"]
console.log(instance1.id); // 10
console.log(instance2.books); // ["JavaScript", "html", "css"]
console.log(instance2.id); // 11
instance1.showBooks(); // TypeError
“小白,注意这里。SuperClass.call(this, id);这条语句是构造函数式继承的精华,由于call这个方法可以更改函数的作用环境,因此在子类中,对superClass调用这个方法就是将子类中的变量在父类中执行一遍,由于父类中是给this绑定属性的,因此子类自然也就继承了父类的共有属性。由于这种类型的继承没有涉及原型prototype,所以父类的原型方法自然不会被子类继承,而如果要想被子类继承就必须要放在构造函数中,这样创建出来的每个实例都会单独拥有一份而不能共用,这样就违背了代码复用的原则。为了综合这两种模式的优点,后来有了组合式继承。”
“组合继承是不是说将这两种继承模式综合到一起呀?那么它又是如何做到的呢?”
“别着急,我们先总结一下之前两种模式的特点,类式继承是通过子类的原型prototype对父类实例化来实现的,构造函数式继承是通过在子类的构造函数作用环境中执行一次父类的构造函数来实现的,所以只要在继承中同时做到这两点即可,看下面的代码。”
// 组合式继承
// 声明父类
function SuperClass(name){
// 值类型共有属性
this.name = name;
// 引用类型共有属性
this.books = ["html", "css", "JavaScript"];
}
// 父类原型共有方法
SuperClass.prototype.getName = function(){
console.log(this.name);
};
// 声明子类
function SubClass(name, time){
// 构造函数式继承父类name属性
SuperClass.call(this, name);
// 子类中新增共有属性
this.time = time;
}
// 类式继承 子类原型继承父类
SubClass.prototype = new SuperClass();
// 子类原型方法
SubClass.prototype.getTIme = function(){
console.log(this.time);
};
“小白看到没,在子类构造函数中执行父类构造函数,在子类原型上实例化父类就是组合模式,这样就融合了类式继承和构造函数继承的优点,并且过滤掉其缺点,你测试看看。”
于是小白写下测试代码。
var instance1 = new SubClass("js book", 2014);
instance1.books.push("设计模式");
console.log(instance1.books); // ["html", "css", "JavaScript", "设计模式"]
instance1.getName(); // js book
instance1.getTime(); // 2014
var instance2 = new SubClass("css book", 2013);
console.log(instance2.books); // ["html", "css", "JavaScript"]
instance2.getName(); // css book
instance2.getTime(); // 2013
“真的是这样呀,”小白兴奋地说,“子类的实例中更改父类继承下来的引用类型属性如books,根本不会影响到其他实例,并且子类实例化过程中又能将参数传递到父类的构造函数中,如name。这种模式真的很强大,所以这应该是继承中最完美的版本吧?”
“还不是,因为我们在使用构造函数继承时执行了一遍父类的构造函数,而在实现子类原型的类式继承时又调用了一遍父类构造函数。因此父类构造函数调用了两遍,所以这还不是最完美的方式。”
“难道还有更好的方式么?”
“那当然,JavaScript很灵活嘛。不过在学习这种方式之前我们先学习一个简单而很常用的方式。”
“2006年道格拉斯·克罗克福德发表一篇《JavaScript中原型式继承》的文章,他的观点是,借助原型prototype可以根据已有的对象创建一个新的对象,同时不必创建新的自定义对象类型。大师的话理解起来可能很困难,不过我们还是先看一下他实现的代码吧。”
// 原型是继承
function inheritObject(o){
// 声明一个过渡函数对象
function F(){}
// 过渡对象的原型继承父对象
F.prototype = o;
// 返回过渡对象的一个实例,该实例的原型继承了父对象
return new F();
}
“这种方式怎么和类式继承有些像呢?”
“对,它是对类式继承的一个封装,其实其中的过渡对象就相当于类式继承中的子类,只不过在原型式中作为一个过渡对象出现的,目的是为了创建要返回的新的实例化对象。”
“如果是这样,是不是类式继承中的问题在这里也会出现呢?”小白追问。
“是这样的,”小铭接着说,“不过这种方式由于F过渡类的构造函数中无内容,所以开销比较小,使用起来也比较方便。当然如果你感觉有必要可以将F过渡类缓存起来,不必每次创建一个新过渡类F。当然这种顾虑也是不必要的。随着对这种思想的深入,后来就出现的Object.create()的方法。”
“创建的新对象会不会影响到父类对象呢?”于是小白写下测试用例(测试代码)。
var book = {
name: "js book",
alikeBook: ["css book", "html book"]
};
var newBook = inheritObject(book);
newBook.name = "ajax book";
newBook.alikeBook.push("xml book");
var otherBook = inheritObject(book);
otherBook.name = "flash book";
otherBook.alikeBook.push("as book");
console.log(newBook.name); //ajax book
console.log(newBook.alikeBook); //["css book", "html book", "xml book", "as book"]
console.log(otherBook.name); //flash book
console.log(otherBook.alikeBook); //["css book", "html book", "xml book", "as book"]
console.log(book.name); //js book
console.log(book.alikeBook); //["css book", "html book", "xml book", "as book"]
“跟类式继承一样,父类对象book中的值类型的属性被复制,引用类型的属性被共用。”小白感叹道。
“然而道格拉斯·克罗克福德推广的继承并不只这一种,他在此基础上做了一些增强而推出一种寄生式继承。”
“寄生式继承?这还头一次听说,它是怎么实现的?”
“不着急,大师对该模式论述的话我们就不深究了,我们还是看看这种继承的实现吧。”
// 寄生式继承
// 声明基对象
var book = {
name: "js book",
alikeBook: ["css book", "html book"]
};
function createBook(obj){
// 通过原型继承方式创建新对象
var o = new inheritObject(obj);
// 拓展新对象
o.getName = function(){
console.log(name);
};
// 返回拓展后的新对象
return o;
}
“看懂了吗?”小铭问小白,“其实寄生式继承就是对原型继承的第二次封装,并且在这第二次封装过程中对继承的对象进行了拓展,这样新创建的对象不仅仅有父类中的属性和方法而且还添加新的属性和方法。”
“哦,这种类型的继承果如其名,寄生大概指的就是像寄生虫一样寄托于某个对象内部生长。当然寄生式继承这种增强新创建对象的继承思想也是寄托于原型继承模式吧。”
“嗯,是这个道理,而这种思想的作用也是为了寄生组合式继承模式的实现。”
“寄生组合式继承?”小白好奇地问道。
“嗯,之前我们学习了组合式继承,那时候我们将类式继承同构造函数继承组合使用,但是这种方式有一个问题,就是子类不是父类的实例,而子类的原型是父类的实例,所以才有了寄生组合式继承。但是你知道是哪两种模式的组合么?”
“寄生当然是寄生式继承,寄生式继承依托于原型继承,原型继承又与类式继承相像,另外一种就不应该是这些模式了,所以另外一种继承模式应该是构造函数继承了吧。当然,子类不是父类实例的问题是由于类式继承引起的。”小白回答道。
“对,正是这两种继承,但是这里寄生式继承有些特殊,这里它处理的不是对象,而是类的原型。我们再次来看看道格拉斯·克罗克福德对寄生式继承的一个改造。”
/**
* 寄生式继承 继承原型
* 传递参数 subClass 子类
* 传递参数 superClass 父类
**/
function inheritPrototype(subClass, superClass){
// 复制一份父类的原型副本保存在变量中
var p = inheritObject(superClass.prototype);
// 修正因为重写子类原型导致子类的constructor属性被修改
p.constructor = subClass;
// 设置子类的原型
subClass.prototype = p;
}
“组合式继承中,通过构造函数继承的属性和方法是没有问题的,所以这里我们主要探究通过寄生式继承重新继承父类的原型。我们需要继承的仅仅是父类的原型,不再需要调用父类的构造函数,换句话说,在构造函数继承中我们已经调用了父类的构造函数。因此我们需要的就是父类的原型对象的一个副本,而这个副本我们通过原型继承便可得到,但是这么直接赋值给子类会有问题的,因为对父类原型对象复制得到的复制对象p中的constructor指向的不是subClass子类对象,因此在寄生式继承中要对复制对象p做一次增强,修复其constructor属性指向不正确的问题,最后将得到的复制对象p赋值给子类的原型,这样子类的原型就继承了父类的原型并且没有执行父类的构造函数。”
“看上去好复杂呀。”小白惊叹道。
“所以你要去测一测呀。测试很简单,与组合模式相比只有一个地方做了修改,就是子类原型继承父类原型这一处,测试看看吧。”
“好吧。”于是小白写下了测试用例。
// 定义父类
function SuperClass(name){
this.name = name;
this.colors = ["red", "blue", "green"];
}
// 定义父类原型方法
SuperClass.prototype.getName = function(){
console.log(this.name);
};
// 定义子类
function SubClass(name, time){
// 构造函数式继承
SuperClass.call(this, name);
// 子类新增属性
this.time = time;
}
// 寄生式继承父类原型
inheritPrototype(SubClass, SuperClass);
// 子类新增原型方法
SubClass.prototype.getTime = function(){
console.log(this.time);
};
// 创建两个测试方法
var instance1 = new SubClass("js book", 2014);
var instance2 = new SubClass("css book", 2013);
小白首先创建了父类,以及父类的原型方法,然后创建子类,并在构造函数中实现构造函数式继承,然后又通过寄生式继承了父类原型,最后又对子类添加了一些原型方法。
最后小白测试了一下,结果如下:
instance1.colors.push("black");
console.log(instance1.colors); //["red", "blue", "green", "black"]
console.log(instance2.colors); //["red", "blue", "green"]
instance2.getName(); //css book
instance2.getTime(); //2013
“现在你明白了吧,其实这种方式继承如图2-2所示,其中最大的改变就是对子类原型的处理,被赋予父类原型的一个引用,这是一个对象,因此这里有一点你要注意,就是子类再想添加原型方法必须通过prototype.对象,通过点语法的形式一个一个添加方法了,否则直接赋予对象就会覆盖掉从父类原型继承的对象了。”
▲图2-2 继承原理
“是这样呀,对了,我记得有一些面向对象语言中支持多继承,在JavaScript中能实现么?”
“嗯,不过是有一些局限性的。你知道,在JavaScript中继承是依赖于原型prototype链实现的,只有一条原型链,所以理论上是不能继承多个父类的。然而JavaScript是灵活的,通过一些技巧方法你却可以继承多个对象的属性来实现类似的多继承。”小铭接着说,“讲解多继承之前先跟你说一下当前很流行的一个用来继承单对象属性的extend方法。”
// 单继承 属性复制
var extend = function(target, source) {
// 遍历源对象中的属性
for (var property in source) {
// 将源对象中的属性复制到目标对象中
target[property] = source[property];
}
// 返回目标对象
return target;
};
“原来extend方法的实现就是对对象中的属性的一个复制过程呀。”小白惊讶地说。
“嗯,是这样,我们的这个extend方法是一个浅复制过程,他只能复制值类型的属性,对于引用类型的属性它无能为力。而在jquery等一些框架中实现了深复制,就是将源对象中的引用类型的属性再执行一遍extend方法而实现的。我们这里实现得比较简单,所以你测试也比较容易。”
于是小白写下如下测试代码。
var book = {
name : 'JavaScript设计模式',
alike : ['css', 'html', 'JavaScript']
}
var anotherBook = {
color : 'blue'
}
extend(anotherBook, book);
console.log(anotherBook.name); // JavaScript设计模式
console.log(anotherBook.alike); // ["css", "html", "JavaScript"]
anotherBook.alike.push('ajax');
anotherBook.name = '设计模式';
console.log(anotherBook.name); // 设计模式
console.log(anotherBook.alike); // ["css", "html", "JavaScript", "ajax"]
console.log(book.name); // JavaScript设计模式
console.log(book.alike); // ["css", "html", "JavaScript", "ajax"]
“真的是这样。但是多继承呢?”
“很容易,既然上面的方法可以实现对一个对象属性的复制继承,那么如果我们传递多个对象呢?”
// 多继承 属性复制
var mix = function() {
var i = 1, // 从第二个参数起为被继承的对象
len = arguments.length, // 获取参数长度
target = arguments[0], // 第一个对象为目标对象
arg; // 缓存参数对象
// 遍历被继承的对象
for(; i < len; i++){
// 缓存当前对象
arg = arguments[i];
// 遍历被继承对象中的属性
for (var property in arg) {
// 将被继承对象中的属性复制到目标对象中
target[property] = arg[property];
}
}
// 返回目标对象
return target;
};
“mix方法的作用就是将传入的多个对象的属性复制到源对象中,这样即可实现对多个对象的属性的继承。”
“这是实现方式真不错,可是使用的时候需要传入目标对象(第一个参数——需要继承的对象)。”
“当然你也可以将它绑定到原生对象Object上,这样所有的对象就可以拥有这个方法了。”
Object.prototype.mix = function(){
var i = 0, // 从第一个参数起为被继承的对象
len = arguments.length, // 获取参数长度
arg; // 缓存参数对象
// 遍历被继承的对象
for(; i < len; i++){
// 缓存当前对象
arg = arguments[i];
// 遍历被继承对象中的属性
for (var property in arg) {
// 将被继承对象中的属性复制到目标对象中
this[property] = arg[property];
}
}
}
“这样我们就可以在对象上直接调用了。如……”
otherBook.mix(book1, book2);
console.log(otherBook); // Object {color: "blue", name: "JavaScript设计模式", mix: function, about: "一本JavaScript书"}
“在JavaScript中实现的多继承是如此的美妙。”
“小铭,在面向对象编程中不是还有一种特性叫作多态么?在JavaScript中可以实现么?”
“多态,就是同一个方法多种调用方式吧。在JavaScript中也是可以实现的,只不过要对传入的参数做判断以实现多种调用方式,如我们定义一个add方法,如果不传参数则返回10,如果传一个参数则返回10+参数,如果传两个参数则返回两个参数相加的结果。”
//多态
function add(){
// 获取参数
var arg = arguments,
// 获取参数长度
len = arg.length;
switch(len){
// 如果没有参数
case 0:
return 10;
// 如果只有一个参数
case 1:
return 10 + arg[0];
// 如果有两个参数
case 2:
return arg[0] + arg[1];
}
}
// 测试用例
console.log(add()); // 10
console.log(add(5)); // 15
console.log(add(6,7)); // 13
“当然我们还可以让其转化成更易懂的类形式:”
function Add(){
// 无参数算法
function zero(){
return 10;
}
// 一个参数算法
function one(num){
return 10 + num;
}
// 两个参数算法
function two(num1, num2){
return num1 + num2;
}
// 相加共有方法
this.add = function(){
var arg = arguments,
// 获取参数长度
len = arg.length;
switch(len){
// 如果没有参数
case 0:
return zero();
// 如果只有一个参数
case 1:
return one(arg[0]);
// 如果有两个参数
case 2:
return two(arg[0], arg[1]);
}
}
}
// 实例化类
var A = new Add();
//测试
console.log(A.add()); // 10
console.log(A.add(5)); // 15
console.log(A.add(6,7)); // 13
“对于多态类,当我们调用add运算方法时,他会根据传参不同做相应运算,当然我们将不同运算方法封装在类内,这样代码更易懂。”
两天的学习让小白深深陶醉于一种新的编程方式——面向对象编程,可是这种编程方式又有什么用呢?下章我们将伴随小白看看面向对象编程在实战中的应用,并开始学习设计模式。
封装与继承是面向对象中的两个主要特性,继承即是对原有对象的封装,从中创建私有属性、私有方法、特权方法、共有属性、共有方法等,对于每种属性与每种方法特点是不一样的,有的不论对类如何实例化,它只创建一次,那么这类属性或者方法我们称之为静态的。有的只被类所拥有,那么这类属性和方法又是静态类方法与静态类属性。当然可被继承的方法与属性无外乎两类,一类在构造函数中,这类属性与方法在对象实例化时被复制一遍。另一类在类的原型对象中,这类属性与方法在对象实例化时被所有实例化对象所共用。
提到类的实例化我们就引出了继承,当然如果实例化的是对象那么则为对象继承,如果实例化的是类(当然类也是一种对象,只不过是用来创建对象的),那么就是一种类的继承。对于类的继承我们根据继承的方式又分为很多种,通过原型链继承的方式我们称之为类式继承,通过构造函数继承的方式我们称之为构造函数式继承,那么将这两种方式组合起来的继承方式我们称之为组合继承,由于类式继承过程中会实例化父类,这样如果父类构造函数极其复杂,那么这种方式对构造函数的开销是不值得的,此时有了一种新的继承方式,通过在一个函数内的过渡对象实现继承并返回新对象的方式我们称之为寄生式继承,此时我们在结合构造函数时继承,这样再融合构造函数继承中的优点并去除其缺点,得到的继承方式我们称之为寄生组合式继承。当然有时候子类对父类实现继承可以通过拷贝方法与属性的方式来实现,这就有了多继承,即将多个父类(对象)的属性与方法拷贝给子类实现继承。
对于面向对象中的多态,在JavaScript中实现起来就容易得多了,通过对传递的参数判断来决定执行逻辑,即可实现一种多态处理机制。
多继承中我们通过对对象属性与方法浅复制实现继承,想一想如何才能实现深复制呢?(浅复制中复制对象的方法对象实质是一种指向引用,所以在深复制中要把该对象中的引用类型属性细化成值类型拷贝到目标对象中。)