(本节内容摘自:Javascript设计模式与开发实践一书,做为本身的笔记保存,但愿对有须要的朋友有用)编程
JavaScript没有提供传统面向对象语言中的类式继承,而是经过原型委托的方式来实现对象与对象之间的继承。设计模式
编程语言按数据类型分类,大体能够分为静态类型语言和动态类型语言。浏览器
静态类型语言在编译时已肯定变量类型,而动态语言类型的变量类型要到程序运行时被赋值后才能肯定,Javascript是一门典型的动态类型语言。编程语言
鸭子类型(duck typing),关于这个有一个故事:从前有个国王,他以为这个世界上鸭子的叫声很美妙,因而召集大臣要组建一个1000只鸭子组成的合唱团,大臣们找遍了全国却只有999只,最后大臣们发现有一只鸡,它的叫声跟鸭子如出一辙,因而这只鸡成为了鸭子合唱团的最后一员。函数
下面咱们用代码来模拟上面的这个故事:this
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); //恭喜加入合唱团
鸭子类型的概念在动态类型语言的面向对象设计中很是重要,利用它咱们能够在动态类型语言中实现“面向接口编程”,而不是“面向实现编程”。
google
多态(polymorphism),它的含义是同一操做做用于不一样的对象上,能够产生不一样的解释和不一样的执行效果,换句话说,给不一样的对象发送同一消息时,这些对象会根据这个消息分别给出不一样的反馈,下面举个栗子:spa
有一只鸭和一只鸡,它们都会叫,当主人向它们发出“叫”的指令时,鸭会“嘎嘎嘎”的叫,而鸡会“咯咯咯”的叫,两只动物会根据主人发出的同一指令,发出各自不一样的声音。prototype
下面咱们来看一段多态的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中,函数是一等对象,函数自己也是对象,函数用来封装行为并能被四处传递,当咱们向函数发出“调用”消息时,这些函数会返回不一样的执行结果。
封装的目的就是将信息隐藏,通常咱们讨论的是对数据和实现进行封装,除此以外更普遍的是对封装类型和封装变化。
一、封装数据
在其余许多编程语言中提供了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来建立私有属性。
二、封装实现
三、封装类型
四、封装变化
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());