(本節內容摘自: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());