(本节内容摘自: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()); //svenconsole.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); //trueconsole.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); //Kaindyconsole.log(a.getName()); //Kaindyconsole.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());