(本节内容摘自:Javascript设计模式与开发实践一书,作为自己的笔记保存,希望对有需要的朋友有用)
JavaScript没有提供传统面向对象语言中的类式继承,而是通过原型委托的方式来实现对象与对象之间的继承。
一、动态类型语言
编程语言按数据类型分类,大致可以分为静态类型语言和动态类型语言。
静态类型语言在编译时已确定变量类型,而动态语言类型的变量类型要到程序运行时被赋值后才能确定,Javascript是一门典型的动态类型语言。
鸭子类型(duck typing),关于这个有一个故事:从前有个国王,他觉得这个世界上鸭子的叫声很美妙,于是召集大臣要组建一个1000只鸭子组成的合唱团,大臣们找遍了全国却只有999只,最后大臣们发现有一只鸡,它的叫声跟鸭子一模一样,于是这只鸡成为了鸭子合唱团的最后一员。
下面我们用代码来模拟上面的这个故事:
var duck = {
duckSinging: function() {
console.log('嘎嘎嘎');
}
};
var chicken = {
duckSinging: function() {
console.log('咯咯咯');
}
};
var choir = []; //合唱团
var joinChoir = function(animal) {
if(animal && typeof animal.duckSinging === 'function') {
choir.push(animal);
console.log('恭喜加入合唱团');
console.log('合唱团已有成员数量:' + choir.length);
}
};
joinChoir(duck); //恭喜加入合唱团
joinChoir(chicken); //恭喜加入合唱团
鸭子类型的概念在动态类型语言的面向对象设计中非常重要,利用它我们可以在动态类型语言中实现“面向接口编程”,而不是“面向实现编程”。
二、多态
多态(polymorphism),它的含义是同一操作作用于不同的对象上,可以产生不同的解释和不同的执行效果,换句话说,给不同的对象发送同一消息时,这些对象会根据这个消息分别给出不同的反馈,下面举个栗子:
有一只鸭和一只鸡,它们都会叫,当主人向它们发出“叫”的指令时,鸭会“嘎嘎嘎”的叫,而鸡会“咯咯咯”的叫,两只动物会根据主人发出的同一指令,发出各自不同的声音。
下面我们来看一段多态的Javascript代码:
var makeSound = function(animal) {
if(animal instanceof Duck) {
console.log('嘎嘎嘎');
}else if(animal instanceof Chicken) {
console.log('咯咯咯');
}
};
var Duck = funcdtion(){};
var Chicken = function(){};
makeSound(new Duck()); //嘎嘎嘎
makeSound(new Chicken()); //咯咯咯
多态背后的思想就是把“做什么”和“谁去做及怎样去做”分离开,也就是将“不变的事”和“可能改变的事”分离开。很显然,上面的代码有问题,如果我们再增加一只狗,就要改动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()); //咯咯咯
如果我们需要增加一只动物,那么我们只需要增加代码即可,而不需要去改动makeSound函数
var Dog = function(){};
Dog.prototype.sound = function(){
console.log('汪汪汪');
}
makeSound(new Dog()); //汪汪汪
由此可见,Javascript的多态性是与生俱来的,它作为一门动态类型语言,既不会检查对象类型,也不会检查参数类型,从上面的例子看出,我们既可以往makeSound函数里传递duck参数,也可以传递chicken参数,所以,一种动物是否能发出声音,只取决于它有没有makeSound方法,而不取决于它是否是某种类型的对象。
下面我们再来看一个在实际项目中可能会遇到的例子,假设我们要编写一个地图应用,有两家地图API可供选择,他们都提供了show方法,代码如下:
var googleMap = {
show: function(){
console.log('开始渲染谷歌地图');
}
};
var renderMap = function(){
googleMap.show();
};
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'); //开始渲染百度地图
OK,现在问题来了,如果我再增加一个搜搜地图呢?那就要改动renderMap函数,继续在里面添加条件分支语句,所以,看下面的代码:
var googleMap = {
show: function(){
console.log('开始渲染谷歌地图');
}
};
var baiduMap = {
show: function(){
console.log('开始渲染百度地图');
}
};
//把相同的部分抽象出来,也就是显示地图
var renderMap = function(map){
if(map.show instanceof Function){
map.show();
}
};
renderMap(googleMap); //开始渲染谷歌地图
renderMap(baiduMap); //开始渲染百度地图
这时,我们如果需要添加其他的地图API
var sosoMap = {
show: function(){
console.log('开始渲染搜搜地图');
}
};
renderMap(sosoMap);
在Javascript中,函数是一等对象,函数本身也是对象,函数用来封装行为并能被四处传递,当我们向函数发出“调用”消息时,这些函数会返回不同的执行结果。
二、封装
封装的目的就是将信息隐藏,一般我们讨论的是对数据和实现进行封装,除此之外更广泛的是对封装类型和封装变化。
1、封装数据
在其他许多编程语言中提供了private、public、protected等关键字来实现封装,但Javascript没有,我们只有依靠变量的作用域来实现,而且只能模拟出public和private这两种封装特性
var myObject = (function(){
var _name = 'sven'; //私有(private)变量
return {
getName: function(){ //公开(public)方法
return _name;
}
}
})();
console.log(myObject.getName()); //sven
console.log(myObject._name); //undefined
另外,在ES6中,可以通过Symbol来创建私有属性。
2、封装实现
3、封装类型
4、封装变化
三、继承
Javascript中继承是基于原型模式的,而像Java、C++等这些是基于类的的面向对象语言,我们要创建一个对象,必须先定义一个Class,然后从这个Class里实例化一个对象出来。然而Javascript中并没有类,所以,在JavaScript中对象是被克隆出来的,也就是一个对象通过克隆另一个对象来创建自己。
我们假设编写一个网页版的飞机大战游戏,这个飞机拥有分身技能,当使用这个技能时,页面上会出现多个同样的飞机,这时我们就需要使用到原型模式来克隆飞机。ES5提供了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, attatckLevel: 10, defenseLevel: 7}
//在不支持Object.create方法的浏览器中,使用以下代码:
Object.create = Object.create || function(obj){
var F = function(){};
F.prototype = obj;
return new F();
}
原型继承遵循以下原则,Javascript也不例外:
a、所有的数据都是对象
b、要得到一个对象,不是通过实例化类,而是找到一个对象作为原型并克隆它
c、对象会记住它的原型
d、如果对象无法响应某个请求,它会把这个请求委托给它自己的原型
Javascript中存在一个根对象Object.prototype,它是一个空对象,所有的对象都是从这个根对象中克隆而来的,Object.prototype就是它们的原型。
var obj1 = new Object();
var obj2 = {};
//利用ES5提供的Object.getPrototypeOf方法来查看它们的原型
console.log(Object.getPrototypeOf(obj1) === Object.prototype); //true
console.log(Object.getPrototypeOf(obj2) === Object.prototype); //true
通过new运算符从构造器中得到一个对象,看下面的代码:
function Person(name){
this.name = name;
};
Person.prototype.getName = function(){
return this.name;
};
var a = new Person('Kaindy');
console.log(a.name); //Kaindy
console.log(a.getName()); //Kaindy
console.log(Object.getPrototypeOf(a) === Person.prototype); //true
上面代码中的Person并不是一个类,而是函数构造器,Javascript的函数既可以作为普通函数使用,也可以作为构造器调用,当使用new运算符时,函数就成了构造器,这个创建对象的过程,也就是先克隆了Object.prototype,然后再做其他的一些操作。
在Javascript中,每个对象都会记住它的原型,准确的说,应该是对象的构造器有原型。每个对象都有一个名为__proto__的隐藏属性,这个属性会指向它的构造器的原型对象
var a = new Object();
console.log(a.__proto__ === Object.prototype); //true
实际上,每个对象就是通过自身隐藏的__proto__属性来记住自己的构造器原型.
如果对象无法响应请求,它会把这个请求委托给它的构造器的原型,我们来看下面的代码:
var obj = {name: 'Kaindy'};
var A = function(){};
A.prototype = obj;
var a = new A();
console.log(a.name); //Kaindy
我们来看下引擎做了什么,
首先,我们需要打印出对象a的name属性,尝试遍历对象a的所有属性,但没找到name
接着,对象a把查找name属性这个请求委托给了它自己的构造器原型,也就是a.__proto__,而a.__proto__指向了A.prototype,A.prototype被设置为了对象obj。
最后在obj中找到了name属性,并返回它的值。
结束:Object.create是原型模式的天然实现,目前大多数主流浏览器都支持此方法,但它效率并不高,比通过构造函数创建对象要慢,最新的ES6带来了Class语法,看起来像一门基于类的语言,但原理还是通过原型机制来创建对象,看下面的代码:
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.geName() + ' says ' + dog.speak());