我们平常开发的时候用的最多的是面向过程编程,对于面向对象编程和常用的设计模式却很少使用,对于一个初、中级前端来说,学习js面向对象和js设计模式是向高级进阶的必经之路,所以有必要系统的学习js设计模式。
设计模式的定义
定义:在面向对象软件设计过程中针对特定的问题的简洁而优雅的解决方案。
通俗一点说,设计模式是在某种场合下对某个问题的一种解决方案。如果再通俗一点说,设计模式就是给面向对象软件开发中的一些好的设计取个名字。
所有的设计模式都遵循一条规则:“找出程序中变化的东西,并将变化封装起来”。
基础知识
在学习设计模式之前,需要了解一些相关的知识,例如一些面向对象的基础知识、this等重要概念,还需要掌握一些函数式编程的技巧。这些都是学习设计模式的必要铺垫。
面向对象的javascript
- 动态类型语言
编程语言按照数据类型大体可以分为两类,一类是静态类型语言,另一类是动态类型语言。
静态类型语言在编译时便已确定变量的类型,而动态类型语言的变量类型要到程序运行的时候,待变量被赋予某个值之后,才会具有某种类型,javascript是一门典型的动态类型语言。
动态类型语言对变量类型的宽容给实际编码带来了很大的灵活性,由于无需进行类型检测,我们可以尝试调用任何对象的任意方法,而无需去考虑它原本是否被设计为拥有该方法。
“面向接口编程”是设计模式中最重要的思想。
- 多态
多态的实际含义:同一操作作用于不同的对象上面,可以产生不同的解释和不同的执行结果。换句话说,给不同的对象发送同一消息的时候,这些对象会根据这个消息分别给出不同的反馈。
举例说明一下
主人家里养了两只动物,分别是一只鸭和一只鸡,当主人向它们发出“叫”的命令时,鸭会“嘎嘎嘎”地叫,而鸡会“咯咯咯”地叫。这两只动物都会以自己的方式来发出叫声。它们同样“都是动物,并且可以发出叫声”,但根据主人的指令,它们会各自发出不同的叫声。
- 多态--对象的多态性
首页我们把不变的部分隔离出来,那就是所有动物都会发出叫声:
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() ); // 汪汪汪
java等静态语言需要向上转型来实现多态(类型检查),而javascript的多态是与生俱来的。
- 多态--多态在面向对象程序设计中的作用
多态最根本的作用就是通过把过程化的条件分支语句转化为对象的多态,从而消除这些条件分支语句。
将行为分布在各个对象中,并让这些对象各自负责自己的行为,这正是面向对象设计的优点。
对象的多态性告诉我们"做什么"和“怎么做”可以分开。
- 多态--设计模式与多态
绝大部分的设计模式的实现都离不开多态性思想
在javascript这种将函数作为一等对象的语言中,函数本身也是对象,函数用来封装行为并且能够被四处传递。当我们对一些函数发出“调用”的消息时,这些函数会返回不同的执行结果,这是“多态性”的一种体现,也是很多设计模式在javascript中可以用高阶函数来代替实现的原因。
- 封装
封装的目的是将信息隐藏,一般而言,我们讨论的封装是封装数据和封装实现。这里我们将讨论更广义的封装,不仅包括封装数据和封装实现,还包括封装类型和封装变化。
- 封装--封装数据
除了ECMAScript6中提供的let之外,一般我们通过函数来创建作用域:
var myObject=(function(){
var _name='sven'; // 私有(private)变量
return {
getName:function(){ // 公共(public)方法
return _name;
}
}
})
console.log( myObject.getName() ); // 输出sven
console.log(myObject._name); // 输出undefined
另外在ECAMScript6中,还可以通过Symbol创建私有属性。
- 封装--封装实现
封装的目的是将信息隐藏,封装应该被视为“任何形式的封装”,也就是说,封装不仅仅是隐藏数据,还包括隐藏实现细节、设计细节以及隐藏对象的类型等。
封装使得对象之间的耦合变松散,对象之间只通过暴露的API接口来通信。当我们修改一个对象时,可以随意地修改它的内部实现,只要对外的接口没有变化,就不会影响到程序的其他功能。
封装实现的例子很多,比如我们编写了一个each函数,它的作用就是遍历一个聚合对象,使用这个each函数的人不用关心它的内部是怎样实现的,只要它提供的功能正确便可以。即使each函数修改了内部源代码,只要对外的接口或者调用方式没有变化,用户就不用关心它内部实现的改变。
- 封装--封装类型
封装类型是静态类型语言中的一种重要的封装方式,在封装类型方面,javascript没有能力,也没有必要做得更多。
- 封装--封装变化
封装更重要的表现层面为封装变化,当我们想办法把程序中变化的部分封装好之后,剩下的即是稳定而可复用的部分了。
- 原型模式和基于原型继承的javascript对象系统
在原型编程的思想中,类并不是必需的,对象未必需要从类中创建而来,一个对象是通过克隆另外一个对象所得到的。
- 使用克隆的原型模式
ECMAScript 5提供了Object.create方法,可以用来克隆对象。代码如下:
var Plan=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();
}
- Javascript中的原型继承
Javascript遵守原型编程的基本规则:
1.所有数据都是对象
2.要得到一个对象,不是通过实例化类,而是找到一个对象作为原型并克隆它。
3.对象会记住它的原型。
4.如果对象无法响应某个请求,它会把这个请求委托给他自己的原型。
- 所有数据都是对象
基本数据类型除undefined之外,其他都是对象,number、boolean、string 这几种基本类型数据也可以通过“包装类”的方式变成对象类型数据来处理,我们不能说在 JavaScript 中所有的数据都是对象,但可以说绝大部分数据都是对象。
Javascript中的根对象是Object.prototype对象,Object.prototype对象是一个空对象,我们javascript遇到的每一个对象,实际上都是从Object.prototype对象克隆而来的,Object.prototype对象就是它们的原型。比如下面的obj1对象和obj2对象:
var obj1=new Object();
var obj2={};
// 可以利用ECMAScript5提供的Object.getPrototypeOf来查看这两个对象的原型:
console.log( Object.getPrototypeOf( obj1 ) ===Object.prototype ); // 输出:true
console.log( Object.getPrototypeOf( obj2 ) ===Object.prototype ); // 输出:true
- 对象会记住它的原型
对象把请求委托给它自己的原型这句话,更好的说法是对象把请求委托给它的构造器的原型。
javaScript给对象提供了一个名为_proto_的影藏属性,某个对象的_proto_属性默认会指向它的构造器的原型对象,即{Constructor.prototype},在一些浏览器中,_proto_被公开出来,我们可以在Chrome或者Firefox上用这段代码来验证:
var a=new Object();
console.log( a._proto_===Object.prototype ); // 输出:true
对象通过_proto_属性来记住它的构造器的原型
- 如果对象无法响应某个请求,它会把这个请求委托给它的构造器的原型
当我们期望得到一个“类”继承自另外一个“类”的效果时,往往会用下面的代码来模拟实现
var A=function(){};
A.prototype={ name:'sven' };
var B=function(){};
B.prototype=new A();
var b=new B();
console.log(b.name); // 输出sven
我们来看看执行这段代码的时候,引擎做了哪些事情。
1.首先遍历b对象中的所有属性,没有找到name属性
2.查找name属性的请求委托给b对象构造器的原型,b对象的_proto_属性记录着构造器的原型B.prototype,B.prototype被设置为new A()创建出来的对象;
3.在该对象中依然没有找到name属性,于是请求被委托给这个对象构造器的原型A.prototype
4.在A.prototype中找到了name属性,并返回它的值
如果查找b.address 原型链不会无限长,请求到达A.prototype 找不到address属性 ,然后传递到A.prototype的构造器原型Object.prototype,Object的原型是null 所以返回undefined。
a.address // 输出undefined
ECMA6中的继承,其背后仍是通过原型机制来创建对象
class Animal{
constructor(name){
this.name=name;
}
getName() {
return this.name;
}
}
class Dog extends Animal{
constructor(name){
super(name);
}
speak() {
return 'woof';
}
}
var dog=new Dog("Scamp");
console.log(dog.getName()+'say'+dog.speak());
- 小结
原型模式是javaScript的第一个设计模式,它是构成javascript语言的根本,也是一种编程泛型。