面向对象的JavaScript
JavaScript 没有提供传统面向对象语言中的类式继承,而是通过原型委托的方式来实现对象与对象之间的继承。JavaScript 也没有在语言层面提供对抽象类和接口的支持。正因为存在这些跟传统面向对象语言不一致的地方,我们在用设计模式编写代码的时候,更要跟传统面向对象语言加以区别。所以在正式学习设计模式之前,我们有必要先了解一些 JavaScript在面向对象方面的知识。
动态类型语言和鸭子类型
-
静态类型语言
- 优点:
- 编译时就能发现类型不匹配的错误,编辑器可以帮助我们提前避免程序在运行期间有可能发生的一些错误。
- 如果在程序中明确地规定了数据类型,编译器还可以针对这些信息对程序进行一些优化工作,提高程序执行速度。
- 缺点:
- 迫使程序员依照强契约来编写程序,为每个变量规定数据类型。
- 类型的声明也会增加更多的代码,在程序编写过程中,这些细节会让程序员的精力从思考业务逻辑上分散开来。
- 优点:
-
动态类型语言
-
优点:
- 编写的代码数量更少,看起来也更加简洁,程序员可以把精力更多地放在业务逻辑上面。
-
缺点:
- 无法保证变量的类型,从而在程序的运行期有可能发生跟类型相关的错误。
-
-
鸭子类型
鸭子类型指导我们只关注对象的行为,而不关注对象本身,也就是关注 HAS-A, 而不是 IS-A。
IS--A基于类继承或接口实现,IS-A是表达这句话的一种方式:“这个东西是那个东西的一种”。例如:野马是一种马。(是一个)
HAS-A关系是基于用法(即引用)而不是继承。换句话说,如果类A中的代码具有对类B实例的引用,则“类A HAS-A类B”。例如:马有缰绳。(有一个)
下面这个故事更清晰的阐明此概念
从前在 JavaScript王国里,有一个国王,他觉得世界上最美妙的声音就是鸭子的叫声,于是国王召集大臣,要组建一个 1000 只鸭子组成的合唱团。大臣们找遍了全国,终于找到 999只鸭子,但是始终还差一只,最后大臣发现有一只非常特别的鸡,它的叫声跟鸭子一模一样,于是这只鸡就成为了合唱团的最后一员。
多态
多态的实际含义是:同一操作作用于不同的对象上面,可以产生不同的解释和不同的执行结果。换句话说,给不同的对象发送同一个消息的时候,这些对象会根据这个消息分别给出不同的反馈。
多态背后的思想是将“做什么”和“谁去做以及怎样去做”分离开来,也就是将“不变的事物”与 “可能改变的事物”分离开来。
把不变的部分隔离出来,把可变的部分封装起来,这给予了我们扩展程序的能力,程序看起来是可生长的,也是符合开放 — 封闭原则的,相对于修改代码来说,仅仅增加代码就能完成同样的功能,这显然优雅和安全得多。
一段“多态”的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());
对象的多态性
- 隔离和封装
var Duck = function() {},
Chicken = function() {},
Dog = function() {},
mackSoundObj = function(animal) {
animal.sound();
};
Duck.prototype.sound = function() {
console.log('嘎嘎嘎');
};
Chicken.prototype.sound = function() {
console.log('咯咯哒');
};
Dog.prototype.sound = function() {
console.log('汪汪汪');
};
mackSoundObj(new Duck());
mackSoundObj(new Chicken());
mackSoundObj(new Dog());
类型检查和多态
使用继承得到多态效果
- 继承
- 实现继承
- 接口继承
JavaScript的多态
多态在面向对象程序设计中的作用
设计模式与多态
封装
封装的目的是将信息隐藏。一般而言,我们讨论的封装是封装数据和封装实现,其实封装应该被视为“任何形式的封装”。
- 封装数据和封装实现
- 封装类型和封装变化
封装数据
var myObject = (function() {
var __name = 'sven'; // 私有(private)变量
return {
getName: function() { // 公开(public)方法
return __name;
}
}
})();
console.log(myObject.getName()); // 输出:sven
console.log(myObject.__name) // 输出:undefined
封装实现
封装不仅仅是隐藏数据,还包括隐藏实现细节、设计细节以及隐藏对象的类型等。
- 隐藏数据
- 隐藏实现细节、设计细节
- 隐藏对象的类型
封装类型
封装类型是静态类型语言中一种重要的封装方式。一般而言,封装类型是通过抽象类和接口来进行的 。把对象的真正类型隐藏在抽象类或者接口之后,相比对象的类型,客户更关心对象的行为。在许多静态语言的设计模式中,想方设法地去隐藏对象的类型,也是促使这些模式诞生的原因之一。比如工厂方法模式、组合模式等。
封装变化
“找到变化并封装之”。
“考虑你的设计中哪些地方可能变化,这种方式与关注会导致重新设计的原因相反。它不是考虑什么时候会迫使你的设计改变,而是考虑你怎样才能够在不重新设计的情况下进行改变。这里的关键在于封装发生变化的概念,这是许多设计模式的主题。”
——《设计模式》
创建型模式:封装创建对象的变化
结构型模式:封装的是对象之间的组合关系
行为型模式:封装的是对象的行为变化
JavaScript 对象系统
- 原型模式和基于原型继承的 JavaScript 对象系统
在以类为中心的面向对象编程语言中,类和对象的关系可以想象成铸模和铸件的关系,对象总是从类中创建而来。而在原型编程的思想中,类并不是必需的,对象未必需要从类中创建而来,一个对象是通过克隆另外一个对象所得到的。
原型模式不单是一种设计模式,也被称为一种编程泛型。
使用克隆的原型模式
- 原型模式不仅仅是一种设计模式,也是一种编程范型。
- 原型模式的目的并非在于需要得到一个一模一样的对象,而是提供了一种便捷的方式去创建某个类型的对象,克隆只是创建这个对象的过程和手段。
- 每一个对象都是基于另外一个对象的克隆。
var Plane = function() {
this.blood = 100; //血量
this.attackLevel = 1; // 攻击等级
this.defenseLevel = 1; //防御等级
};
var plane = new Plane();
plane.blood = 500;
plane.attackLevel = 10;
plane.defenseLevel = 4;
//不支持ES5的兼容处理
Object.create = Object.creat || function(obj) {
var F = function() {};
F.prototype = obj;
return new F();
}
var clonePlane = Object.create(plane);
console.log(clonePlane);
克隆是创建对象的手段
利用“吸血鬼系统”例子可以映射Javascript的原型链,根对象(Object)为“吸血鬼祖先”。
体验Io语言
原型编程范型的一些规则
基于原型链的委托机制就是原型继承的本质。
JavaScript中的原型继承
- 所有的数据都是对象。
- 要得到一个对象,不是通过实例化类,而是找到一个对象作为原型并克隆它。
- 对象会记住它的原型。
- 如果对象无法响应某个请求,它会把这个请求委托给它自己的原型。
- JavaScript 中的根对象是
Object.prototype
对象。Object.prototype
对象是一个空的对象。 - 其实并不能说对象有原型,而只能说对象的构造器有原型。
利用 ECMAScript 5提供的 Object.getPrototypeOf 来查看对象的原型
var obj1 = new Object();
var obj2 = {};
console.log(Object.getPrototypeOf(obj1) === Object.prototype); // 输出:true
console.log(Object.getPrototypeOf(obj2) === Object.prototype); // 输出:true
用 new
运算符从构造器中得到一个对象
function Personal(name){
this.name=name;
}
Personal.prototype.getName = function(){
return this.name;
}
var aaa = new Personal('steven');
console.log(aaa.name);//steven
console.log(aaa.getName());//steven
console.log(Object.getPrototypeOf(aaa) === Personal.prototype); //true
原型继承的未来
设计模式是对语言不足的补充,如果要使用设计模式,不如去找一门更好的语言。
- 通过 Object.create 来创建对象的效率并不高,通常比通过构造函数创建对象要慢。
- 通过设置构造器的prototype 来实现原型继承的时候,除了根对象 Object.prototype 本身之外,任何对象都会有一个原型。
- 通过 Object.create( null ) 可以创建出没有原型的对象。
网友评论