JavaScript—设计模式与开发实践
第一章 面向对象的JavaScript
设计模式----前言
- 设计模式起源:“模式” 最早诞生于建筑学,哈佛大学建筑学博士士Christopher Alexander研究了为解决同一个问题而设计出的不同建筑解构,从中发现了那些高质量设计中的相似性,并且用“模式”来指代这种相似性。
- 设计模式定义:在面向对象软件设计过程中针对特定问题的简洁而优雅的就解决方案。
- 场景:这个问题发生的场景似曾相识,以前遇到过并解决过这个问题,但是我不知道怎么跟别人去描述它。我们非常希望给这个问题出现的场景和解决方案取一个统一的名字,当别人听到这个名字的时候,就知道我要表达什么。
- 学习模式的作用:
在软件设计中,模式是经过了大量实际项目验证的优秀解决方案。熟悉这些模式的程序员,对某些模式的理解也形成了条件反射当合适的场景出现时,很快就能找到某种方式作为解决方案 - 设计模式的适用性:
设计模式可能会带来代码量的增加,设计模式的作用时让人们写出可复用和可维护性高的程序。所有设计模式的实现都遵循一条原则,“找出程序中变化的地方,并将变化封装起来”。一个程序的设计总是可以分为可变的部分和不变的部分。当我们找出可变的部分,并且把这些部分封装起来,那么剩下的就是不变和稳定的部分。这些不变和稳定的部分时非常容易复用的。 - 分辨模式的关键是意图而不是结构
模式只有放到具体的环境中才有意义。比如我们的手机,把它当电话的时候,就是电话;把它当闹钟的时候,它就是闹钟;用它玩游戏的时候,它就是游戏机。有很多模式的类图和结构确实很相似,辨别模式的关键是这个模式出现的场景,以及为我们解决了什么问题。 - 模式发展
我学习模式的初衷是为了更好的了解面向对象的思想。相信自己会在学习过程中受益
面向对象基础知识
- JavaScript是动态类型的语言,动态类型语言的优点是编写的代码数量更少,看起来更加简洁,程序员可以把精力更多的放在业务上逻辑上面。虽然不区分类型在某些情况下会让程序变得难以理解,但整体而言,代码量越少,月专注于逻辑表达,对阅读程序越有帮助。
- 多态 :同以操作作用域不同对象上面时,可以产生不同的解释和不同的执行结果。换句话说,给不同的对象发送同一个消息的时候,这些对象会根据这个消息分别给出不同的反馈。
- 多态背后的思想:多态背后的思想是将“做什么”和“谁去做以及怎样去做”分离开来;也就是将“不变的事物”于“可能相变的食物”分离。
- 一段多态代码
var makeSound = function (animal){
if (animal instanceof Duck ){
console.log('嘎嘎嘎');
}else if(animal instanceof Chicken){
console.log('咯咯咯');
}
}
var Duck = function (){};
var Chicken = function (){};
makeSound( new Duck() );
makeSound( new Chicken() );
这段代码体现了“多态性”,分别向鸭和鸡发出“叫”的消息时,根据此消息做出了各自不同的反应。但这样的“多态”无法令人满意,如果后来增加了狗。此时狗的叫声“汪汪汪”,此时我们必须改动makeSound函数,才能让狗也发出叫声,当动物种类越来月多时,makeSound有可能变成一个巨大的函数。
在这个故事中,动物都会叫,这是不变的,但是不同类型的动物具体怎么叫是可变的。把不变的部分隔离,把可变的部分封装。同样修改代码来说,仅仅增加代码就能完成同样的功能,这显然要优雅和安全。
改写代码:先把不变的部分隔离,就是都会发出叫声
var makeSound = function (animal){
animal.sound();
}
然后把可变的部分各自封装起来:
var Duck = function(){}
Duck.prototype.sound = function (){
console.log('嘎嘎嘎');
}
var Chicken = function(){}
Chicken.prototype.sound = function(){
console.log('咯咯咯');
}
makeSound( new Duck() );//嘎嘎嘎
makeSound( new Chicken() );//咯咯咯
如果增加狗,如下所示:
var Dog = function(){};
Dog.prototype.sound = function(){
console.log('汪汪汪');
};
makeSound( new Dog() );
- 多态在面向对象程序设计中的作用
多态根本的作用就是通过把过程化的条件分支语句转化为对象的多态性,从而 消除这些条件分支语句.
在电影的拍摄现场,当导演喊出“action”时,主角开始背台词,照明师负责打灯 光,后面的群众演员假装中枪倒地,道具师往镜头里撒上雪花。在得到同一个消息时, 每个对象都知道自己应该做什么。如果不利用对象的多态性,而是用面向过程的方式来 编写这一段代码,那么相当于在电影开始拍摄之后,导演每次都要走到每个人的面前, 确认它们的职业分工(类型),然后告诉他们要做什么。如果映射到程序中,那么程序 中将充斥着条件分支语句。
将行为分布在各个对象中,并让这些对象各自负责自己的行为,这正是面向对象设计的优点。
实际应用:
假设我们要编写一个地图应用,先假设所有地图应用的API调用方法为show,现在有两家可选的地图 API提供商供我们接入自己的应用。 目前我们选择的是谷歌地图,谷歌地图的 API中提供了 show 方法,负责在页面上展示整个地图。
实例代码:
var googleMap = {
show:function(){
console.log('渲染谷歌地图');
}
};
var rederMap = function(){
googleMap.show();
}
renderMap(); //开始渲染谷歌地图
后来由于一些原因,把谷歌地图换成百度地图,选择用一些条件分支来让renderMap 函数同时支持谷歌地图和百度地图。
var googleMap = {
show:function(){
console.log('渲染谷歌地图');
}
};
var baiduMap = function(){
show:function(){
console.log('渲染百度地图')
}
}
var rederMap = function(type){
if(type === 'google'){
googleMap.show();
}else if(type === 'baidu'){
baiduMap.show();
}
}
renderMap('google'); //开始渲染谷歌地图
renderMap('baidu'); //开始渲染百度地图
但是一旦需要替换成搜搜地图,还要改动renderMap函数,继续向里面堆砌条件分支语句
先抽离相同的部分:显示地图
var renderMap = function(map){
if (map.show instanceof Function){
map.show();
}
}
renderMap( googleMap ); // 开始渲染谷歌地图
renderMap( baiduMap ); // 开始渲染百度地图
即使以后添加搜搜地图,renderMap函数不需要做出任何改变。
var sosoMap = {
show:function(){
console.log('渲染搜搜地图')
}
}
封装
- 封装的目的是将信息隐藏。一般而言,我们讨论的封装是封装数据和封装实现。这一节将讨 论更广义的封装,不仅包括封装数据和封装实现,还包括封装类型和封装变化。
- 封装数据:在许多语言的对象系统中,封装数据是由语法解析来实现的,这些语言也许提供了 private、 public、protected 等关键字来提供不同的访问权限。 但 JavaScript并没有提供对这些关键字的支持,我们只能依赖变量的作用域来实现封装特性, 而且只能模拟出 public 和 private 这两种封装性。 除了 ES6 中提供的 let 之外,一般我们通过函数来创建作用域,
var myObject = (function (){
var _name = 'sven'; //私有(private)变量
return {
getName:function(){
return _name; //公开(public)变量
}
}
})();
console.log(myObject.getName());// sven
console.log(myObject._name );// undefined
- 封装实现封装不仅仅是隐藏数据,还包括隐藏实现细节、设计细节以及隐藏对象的类型等。
- 封装类型
- 封装变化:通过封装变化的方式,把系统中稳定的部分和容易变化的隔离,在系统演变过程中,只需要替换哪些容易变化的部分,如果这些部分是已经封装好的,替换起来也想到对容易。
原型模式和基于原型继承的JavaScript对象系统
- 在以类为中心的面向对象编程语言中,类和对象的关系可以想象成铸模和铸件的关系,对象 总是从类中创建而来。而在原型编程的思想中,类并不是必需的,对象未必需要从类中创建而来, 一个对象是通过克隆另外一个对象所得到的。
- 使用克隆的原型模式:假设我们在编写一个飞机大战的网页游戏。某种飞机拥有分身技能,当它使用分身技能的时 候,要在页面中创建一些跟它一模一样的飞机。如果不使用原型模式,那么在创建分身之前,无 疑必须先保存该飞机的当前血量、炮弹等级、防御等级等信息,随后将这些信息设置到新创建的 飞机上面,这样才能得到一架一模一样的新飞机。
如果使用原型模式,我们只需要调用负责克隆的方法,便能完成同样的功能。原型模式的实现关键,是语言本身是否提供了clone方法。ECMAScript 5提供了Object.create 方法,可以用来克隆对象。
var Plane = function(){
this.blood = 100;
this.attackLevel = 1;
this.defenseLevel = 1;
};
var plane = new Plane();
plane.blood = new Plane();
plane.attackLevel = 10;
plane.defenseLevel = 7;
var clonePlane = Object.create(plane);
console.log(clonePlane);
// 输出:Object {{blood: 500, attackLevel: 10, defenseLevel: }
- JavaScript 基于原型的面向对象系统
- 原型编程范型的一些规则
- 所有的数据都是对象,
- 要得到一个对象,不是通过实例化类,而是找到一个对象作为原型并克隆它。
- 对象会记住它的原型
如果对象无法响应某个请求,它就会把这个请求委托给它自己的原型。
JavaScript中的原型继承
- JavaScript中有一一个跟对象存在,这些对象追根溯源都来源于这个根对象。
JavaScript中的根对象是Object.prototype对象,Object.prototype对象是一个空对象。在JS中遇到的每个对象,都是从Object.prototype上克隆的,Object.prototype对象就是他们的原型。 - 构造器:JavaScript中的函数既可以作为普通函数被调用,也可以作为构造器被调用。当使用new运算符来调用函数时,此时的函数就是一个构造器。new 运算符创建对象的过程,实际上也是克隆Object.prototype对象。
- 如果请求在一个链条中一次向后传递,那么每个节点都必须知道它的下一个节点,也就是每个对象应知道自己的原型。目前我们一直在讨论“对象的原型”,就 JavaScript的真正实现来说,其实并不能说对象有 原型,而只能说对象的构造器有原型。对于“对象把请求委托给它自己的原型”这句话,更好 的说法是对象把请求委托给它的构造器的原型。
- __proto__就是对象根“对象构造器的原型”联系起来的纽带,对象要通过 __proto__属性来记住它的构造器的原型,
- 如果对象无法响应某个请求,它会把这个请求委托给它的构造器的原型。遮日澳规则就是原型继承的精髓所在,当一个对象无法响应某个请求的时候,它会顺着原型链把请求传递下去,直到遇到一个可以处理请求的对象为止。
-ES6提供了Class语法,但这只是“语法糖”,其背后仍是通过原型机制来创建对象。
本章总结
介绍了了一个设计模式————原型模式。原型模式既是设计模式,也是编程泛型。介绍了JavaScript中的原型模式,原型模式十分重要,通过原型来实现的面向对象对象系统虽然简单,能力同样强大。