JavaScript設計模式之一:面向對象的Javascript

(本節內容摘自:Javascript設計模式與開發實踐一書,做爲本身的筆記保存,但願對有須要的朋友有用)編程

JavaScript沒有提供傳統面嚮對象語言中的類式繼承,而是經過原型委託的方式來實現對象與對象之間的繼承。設計模式

1、動態類型語言

編程語言按數據類型分類,大體能夠分爲靜態類型語言和動態類型語言。瀏覽器

靜態類型語言在編譯時已肯定變量類型,而動態語言類型的變量類型要到程序運行時被賦值後才能肯定,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

2、多態

多態(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中,函數是一等對象,函數自己也是對象,函數用來封裝行爲並能被四處傳遞,當咱們向函數發出「調用」消息時,這些函數會返回不一樣的執行結果。

2、封裝

封裝的目的就是將信息隱藏,通常咱們討論的是對數據和實現進行封裝,除此以外更普遍的是對封裝類型和封裝變化。

一、封裝數據

在其餘許多編程語言中提供了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來建立私有屬性。 

二、封裝實現

三、封裝類型

四、封裝變化

3、繼承

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());
相關文章
相關標籤/搜索