JavaScript设计模式系列—基础篇(一)面向对象(封装、继承、多态)

转载请注明预见才能遇见的博客:http://my.csdn.net/

原文地址:https://blog.csdn.net/pcaxb/article/details/100562546

JavaScript设计模式系列—基础篇(一)面向对象(封装、继承、多态)

目录

JavaScript设计模式系列—基础篇(一)面向对象(封装、继承、多态)

1.静态类型语言和动态类型语言

2 多态

1.一段“多态”的JavaScript代码

2.对象的多态性

3. JavaScript的多态

4. 多态在面向对象程序设计中的作用

5. 设计模式与多态

3 封装

1.封装数据

2.封装实现

3.封装类型

4. 封装变化

4 原型模式和原型继承

1.原型模式

2.原型继承

3. 对象会记住它的原型

4. 如果对象无法响应某个请求,会把这个请求委托给它构造器的原型

5 原型继承的未来


按照 JavaScript设计者的本意,除了 undefined 之外,一切都应是对象。为了实现这一目标,number 、 boolean 、 string 这几种基本类型数据也可以通过“包装类”的方式变成对象类型数据来处理。

JavaScript 没有提供传统面向对象语言中的类式继承,而是通过原型委托的方式来实现对象与对象之间的继承。JavaScript 也没有在语言层面提供对抽象类和接口的支持。正因为存在这些跟传统面向对象语言不一致的地方,我们在用设计模式编写代码的时候,更要跟传统面向对象语言加以区别。

1.静态类型语言和动态类型语言

编程语言按照数据类型大体可以分为两类,一类是静态类型语言,另一类是动态类型语言

静态类型语言在编译时便已确定变量的类型,而动态类型语言的变量类型要到程序运行的时候,待变量被赋予某个值之后,才会具有某种类型。

静态类型语言的优点首先是在编译时就能发现类型不匹配的错误,编辑器可以帮助我们提前避免程序在运行期间有可能发生的一些错误。其次,如果在程序中明确地规定了数据类型,编译器还可以针对这些信息对程序进行一些优化工作,提高程序执行速度。

静态类型语言的缺点首先是迫使程序员依照强契约来编写程序,为每个变量规定数据类型,归根结底只是辅助我们编写可靠性高程序的一种手段,而不是编写程序的目的。类型的声明也会增加更多的代码,在程序编写过程中,这些细节会让程序员的精力从思考业务逻辑上分散开来。

动态类型语言的优点是编写的代码数量更少,看起来也更加简洁,程序员可以把精力更多地放在业务逻辑上面。虽然不区分类型在某些情况下会让程序变得难以理解,但整体而言,代码量越少,越专注于逻辑表达,对阅读程序是越有帮助的。

动态类型语言的缺点是无法保证变量的类型,从而在程序的运行期有可能发生跟类型相关的错误。

2 多态

多态的实际含义是:同一操作作用于不同的对象上面,可以产生不同的解释和不同的执行结果。换句话说,给不同的对象发送同一个消息的时候,这些对象会根据这个消息分别给出不同的反馈。

1.一段“多态”的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 有可能变成一个巨大的函数。

多态背后的思想是将“做什么”和“谁去做以及怎样去做”分离开来,也就是将“不变的事物”与 “可能改变的事物”分离开来。在这个故事中,动物都会叫,这是不变的,但是不同类型的动物具体怎么叫是可变的。把不变的部分隔离出来,把可变的部分封装起来,这给予了我们扩展程序的能力,程序看起来是可生长的,也是符合开放 — 封闭原则的,相对于修改代码来说,仅仅增加代码就能完成同样的功能,这显然优雅和安全得多。

2.对象的多态性

下面是改写后的代码,首先我们把不变的部分隔离出来,那就是所有的动物都会发出叫声:

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() ); // 咯咯咯

现在我们向鸭和鸡都发出“叫唤”的消息,它们接到消息后分别作出了不同的反应。如果有一天动物世界里又增加了一只狗,这时候只要简单地追加一些代码就可以了,而不用改动以前的makeSound 函数,如下所示:

var Dog = function(){};

Dog.prototype.sound = function(){
	console.log( '汪汪汪' );
};

makeSound( new Dog() ); // 汪汪汪

 3. JavaScript的多态

JavaScript的变量类型在运行期是可变的。一个 JavaScript对象,既可以表示 Duck 类型的对象,又可以表示 Chicken 类型的对象,这意味着 JavaScript对象的多态性是与生俱来的。

某一种动物能否发出叫声,只取决于它有没有 makeSound 方法,而不取决于它是否是某种类型的对象,这里不存在任何程度上的“类型耦合”。

4. 多态在面向对象程序设计中的作用

多态的最根本好处在于,你不必再向对象询问“你是什么类型”而后根据得到的答案调用对象的某个行为——你只管调用该行为就是了,其他的一切多态机制都会为你安排妥当。换句话说,多态最根本的作用就是通过把过程化的条件分支语句转化为对象的多态性,从而消除这些条件分支语句。

将行为分布在各个对象中,并让这些对象各自负责自己的行为,这正是面向对象设计的优点。

假设我们要编写一个地图应用,现在有两家可选的地图 API提供商供我们接入自己的应用。目前我们选择的是谷歌地图,谷歌地图的 API中提供了 show 方法,负责在页面上展示整个地图。示例代码如下:

var googleMap = {
	show: function(){
		console.log( '开始渲染谷歌地图' );
	}
};

var renderMap = function(){
	googleMap.show();
};

renderMap(); // 输出:开始渲染谷歌地图

后来因为某些原因,要把谷歌地图换成百度地图,为了让 renderMap 函数保持一定的弹性,我们用一些条件分支来让 renderMap 函数同时支持谷歌地图和百度地图:

var googleMap = {
	show: function(){
		console.log( '开始渲染谷歌地图' );
	}
};

var baiduMap = {
	show: function(){
		console.log( '开始渲染百度地图' );
	}
};

var renderMap = function( type ){
	if ( type === 'google' ){
		googleMap.show();
	}else if ( type === 'baidu' ){
		baiduMap.show();
	}
};

renderMap( 'google' ); // 输出:开始渲染谷歌地图
renderMap( 'baidu' ); // 输出:开始渲染百度地图

可以看到,虽然 renderMap 函数目前保持了一定的弹性,但这种弹性是很脆弱的,一旦需要替换成搜搜地图,那无疑必须得改动 renderMap 函数,继续往里面堆砌条件分支语句。

我们还是先把程序中相同的部分抽象出来,那就是显示某个地图:

var renderMap = function( map ){
	if ( map.show instanceof Function ){
		map.show();
	}
};

renderMap( googleMap ); // 输出:开始渲染谷歌地图
renderMap( baiduMap ); // 输出:开始渲染百度地图

现在来找找这段代码中的多态性。当我们向谷歌地图对象和百度地图对象分别发出“展示地图”的消息时,会分别调用它们的 show 方法,就会产生各自不同的执行结果。对象的多态性提示我们,“做什么”和“怎么去做”是可以分开的,即使以后增加了搜搜地图, renderMap 函数仍然不需要做任何改变,如下所示:

var sosoMap = {
	show: function(){
		console.log( '开始渲染搜搜地图' );
	}
};

renderMap( sosoMap ); // 输出:开始渲染搜搜地图

在这个例子中,我们假设每个地图 API提供展示地图的方法名都是 show ,在实际开发中也许不会如此顺利,这时候可以借助适配器模式来解决问题。

5. 设计模式与多态

在命令模式中,请求被封装在一些命令对象中,这使得命令的调用者和命令的接收者可以完全解耦开来,当调用命令的 execute 方法时,不同的命令会做不同的事情,从而会产生不同的执行结果。而做这些事情的过程是早已被封装在命令对象内部的,作为调用命令的客户,根本不必去关心命令执行的具体过程。

在组合模式中,多态性使得客户可以完全忽略组合对象和叶节点对象之前的区别,这正是组合模式最大的作用所在。对组合对象和叶节点对象发出同一个消息的时候,它们会各自做自己应该做的事情,组合对象把消息继续转发给下面的叶节点对象,叶节点对象则会对这些消息作出真实的反馈。

在策略模式中,Context并没有执行算法的能力,而是把这个职责委托给了某个策略对象。每个策略对象负责的算法已被各自封装在对象内部。当我们对这些策略对象发出“计算”的消息时,它们会返回各自不同的计算结果。

在 JavaScript 这种将函数作为一等对象的语言中,函数本身也是对象,函数用来封装行为并且能够被四处传递。当我们对一些函数发出“调用”的消息时,这些函数会返回不同的执行结果,这是“多态性”的一种体现,也是很多设计模式在 JavaScript 中可以用高阶函数来代替实现的原因。

3 封装

封装的目的是将信息隐藏,封装应该被视为“任何形式的封装”,也就是说,封装不仅仅是藏数据,还包括隐藏实现细节、设计细节以及隐藏对象的类型等。一般封装是封装数据和封装实现。下面我们将讨论更广义的封装,不仅包括封装数据和封装实现,还包括封装类型和封装变化。

1.封装数据

在许多语言的对象系统中,封装数据是由语法解析来实现的,这些语言也许提供了 private 、public 、 protected 等关键字来提供不同的访问权限。

但 JavaScript并没有提供对这些关键字的支持,我们只能依赖变量的作用域来实现封装特性,而且只能模拟出 public 和 private 这两种封装性。除了 ECMAScript 6中提供的 let 之外,一般我们通过函数来创建作用域:

var myObject = (function(){
	var __name = 'sven'; // 私有(private)变量
	return {
		getName: function(){ // 公开(public)方法
			return __name;
		}
	}
})();

console.log( myObject.getName() ); // 输出:sven
console.log( myObject.__name ) // 输出:undefined

2.封装实现

从封装实现细节来讲,封装使得对象内部的变化对其他对象是不可见的,对象对它自己的行为负责,其他对象或者用户都不关心它的内部实现。封装使得对象之间的耦合变松散,对象之间只通过暴露的API接口来通信。当我们修改一个对象时,可以随意地修改它的内部实现,只要对外的接口没有变化,就不会影响到程序的其他功能。

3.封装类型

封装类型是静态类型语言中一种重要的封装方式。一般而言,封装类型是通过抽象类和接口来进行的。把对象的真正类型隐藏在抽象类或者接口之后,相比对象的类型,客户更关心对象的行为。在许多静态语言的设计模式中,想方设法地去隐藏对象的类型,也是促使这些模式诞生的原因之一。比如工厂方法模式、组合模式等。

当然在 JavaScript中,并没有对抽象类和接口的支持。JavaScript本身也是一门类型模糊的语言。在封装类型方面,JavaScript没有能力,也没有必要做得更多。

4. 封装变化

从设计模式的角度出发,封装更重要的层面体现为封装变化。考虑你怎样才能够在不重新设计的情况下进行改变。

通过封装变化的方式,把系统中稳定不变的部分和容易变化的部分隔离开来,在系统的演变过程中,我们只需要替换那些容易变化的部分,如果这些部分是已经封装好的,替换起来也相对容易。这可以最大程度地保证程序的稳定性和可扩展性。当我们想办法把程序中变化的部分封装好之后,剩下的即是稳定而可复用的部分了。

23种设计模式分别被划分为创建型模式、结构型模式和行为型模式。拿创建型模式来说,要创建一个对象,是一种抽象行为,而具体创建什么对象则是可以变化的,创建型模式的目的就是封装创建对象的变化。而结构型模式封装的是对象之间的组合关系。行为型模式封装的是对象的行为变化。

4 原型模式和原型继承

在以类为中心的面向对象编程语言中,对象总是从类中创建而来。而在原型编程的思想中,类并不是必需的,一个对象是通过克隆另外一个对象所得到的。原型模式不单是一种设计模式,也被称为一种编程泛型。

1.原型模式

如果我们想要创建一个对象,一种方法是先指定它的类型,然后通过类来创建这个对象。原型模式不关心对象的具体类型,而是找到一个对象,然后通过克隆来创建一个一模一样的对象。

原型模式的实现关键,是语言本身是否提供了 clone 方法。ECMAScript 5提供了 Object.create方法,可以用来克隆对象。代码如下:

//游戏中的飞机,超级飞机
var Plane = function(){
	this.blood = 100;
	this.attackLevel = 1;
	this.defenseLevel = 1;
};

var plane = new Plane();
plane.blood = 500;
plane.attackLevel = 10;
plane.defenseLevel = 7;

var clonePlane = Object.create( plane );
console.log( clonePlane );  // 输出:Object {blood: 500, attackLevel: 10, defenseLevel: 7}

//	//在不支持Object.create 方法的浏览器中,则可以使用以下代码:
//	Object.create = Object.create || function( obj ){
//		var F = function(){};
//		F.prototype = obj;
//		return new F();
//	}

2.原型继承

在 Chrome和 Firefox等向外暴露了对象 __proto__ 属性的浏览器下,我们可以通过下面这段代码来理解 new 运算的过程:

function Person( name ){
	this.name = name;
};

Person.prototype.getName = function(){
	return this.name;
};

var objectFactory = function(){
	var obj = new Object(), // 从Object.prototype 上克隆一个空的对象
	Constructor = [].shift.call( arguments ); // 取得外部传入的构造器,此例是Person
	obj.__proto__ = Constructor.prototype; // 指向正确的原型
	var ret = Constructor.apply( obj, arguments ); // 借用外部传入的构造器给obj 设置属性
	return typeof ret === 'object' ? ret : obj; // 确保构造器总是会返回一个对象
};

var a = objectFactory( Person, 'sven' );

console.log( a.name ); // 输出:sven
console.log( a.getName() ); // 输出:sven
console.log( Object.getPrototypeOf( a ) === Person.prototype ); // 输出:true

我们看到,分别调用下面两句代码产生了一样的结果:

var a = objectFactory( A, 'sven' );
var a = new A( 'sven' );

3. 对象会记住它的原型

目前我们一直在讨论“对象的原型”,就 JavaScript 的真正实现来说,其实并不能说对象有原型,而只能说对象的构造器有原型。对于“对象把请求委托给它自己的原型”这句话,更好的说法是对象把请求委托给它的构造器的原型。

JavaScript 给对象提供了一个名为 __proto__ 的隐藏属性,某个对象的 __proto__ 属性默认会指向它的构造器的原型对象。

var a = new Object();
console.log ( a.__proto__=== Object.prototype ); // 输出:true

实际上, __proto__ 就是对象跟“对象构造器的原型”联系起来的纽带。正因为对象要通过__proto__ 属性来记住它的构造器的原型,所以我们用上一节的 objectFactory 函数来模拟用 new创建对象时, 需要手动给 obj 对象设置正确的 __proto__ 指向。

obj.__proto__ = Constructor.prototype;

通过这句代码,我们让 obj.__proto__ 指向 Person.prototype ,而不是原来的 Object.prototype 。

4. 如果对象无法响应某个请求,会把这个请求委托给它构造器的原型

这条规则即是原型继承的精髓所在。虽然 JavaScript 的对象最初都是由 Object.prototype 对象克隆而来的,但对象构造器的原型并不仅限于 Object.prototype 上,而是可以动态指向其他对象。这样一来,当对象 a 需要借用对象 b 的能力时,可以有选择性地把对象 a 的构造器的原型指向对象 b ,从而达到继承的效果。当我们期望得到一个“类”继承自另外一个“类”的效果时,往往会用下面的代码来模拟实现:

var A = function(){};
A.prototype = { name: 'sven' };
var B = function(){};
B.prototype = new A();
var b = new B();
console.log( b.name ); // 输出:sven

首先,尝试遍历对象 b 中的所有属性,但没有找到 name 这个属性。查找 name 属性的请求被委托给对象 b 的构造器的原型,它被 b. __proto__ 记录着并且指向B.prototype ,而 B.prototype 被设置为一个通过 new A() 创建出来的对象。在该对象中依然没有找到 name 属性,于是请求被继续委托给这个对象构造器的原型A.prototype 。在 A.prototype 中找到了 name 属性,并返回它的值。

和把 B.prototype 直接指向一个字面量对象相比,通过 B.prototype = new A() 形成的原型链比之前多了一层。但二者之间没有本质上的区别,都是将对象构造器的原型指向另外一个对象,继承总是发生在对象和对象之间。

5 原型继承的未来

设计模式在很多时候其实都体现了语言的不足之处。Peter Norvig 曾说,设计模式是对语言不足的补充,如果要使用设计模式,不如去找一门更好的语言。这句话非常正确。不过,作为Web前端开发者,相信 JavaScript在未来很长一段时间内都是唯一的选择。

Object.create 是原型模式的天然实现,使用 Object.create 来完成原型继承看起来更能体现原型模式的精髓。目前大多数主流浏览器都提供了 Object.create 方法。但是通过 Object.create 来创建对象的效率并不高,通常比通过构造函数创建对象要慢。通过设置构造器的prototype 来实现原型继承的时候,除了根对象 Object.prototype 本身之外,任何对象都会有一个原型。而通过 Object.create( null ) 可以创建出没有原型的对象。

详细参考:https://blog.csdn.net/pcaxb/article/details/100668246

 

JavaScript设计模式系列—基础篇(一)面向对象(封装、继承、多态)

博客地址:https://blog.csdn.net/pcaxb/article/details/100562546

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值