JavaScript設計模式

在學習使用Javascript以前,個人程序猿生涯裏面僅有接觸的編程語言是C#跟Java——忽略當年在大學補考了N次的C與VB。javascript

從靜態編程語言,轉到動態語言JS,剛開始的時候,遇到很多困難與錯誤。可能由於先入爲主,在JS編程之中,每每不禁自主地以C#的邏輯、編程思路、設計模式進行JS開發。html

時間長了,漸漸發現JS設計模式對於前端開發的重要性,但在分享JS設計模式以前,但願首先分享幾點我的以爲很重要的JS設計模式基礎。有些地方,可能研究得也不是很深刻,有什麼錯誤的話,望指正。前端

動態類型語言

靜態類型語言,在編譯階段,就須要肯定變量的類型。但動態類型語言,只有程序運行時,根據當時語言執行環境,變量纔會賦予其類型。java

因此C#,不能拋開runtime與編譯器單獨運行,Java也離不開JVM。同時也有一大堆問題須要考慮的,跨平臺、類型轉換、反射、Ioc、接口編程、抽象化、動態代理等等一大堆概念技術的產生,還不是由於靜態語言的強類型特性,須要更多的手段讓其獲得靈活多態的特性。編程

public class Main
    {
        public void Function()
        {
            //在編譯的時候,.net runtime必需要知道dog是一隻狗
            Dog dog = new Dog();
            Console.WriteLine(dog.Name);
        }
    }

但,JS只須要個文本編輯器,沒有編譯經過不經過的概念,加載了瀏覽器就能跑。segmentfault

var a = 1;
    console.log(typeof a);//number

    a = "a";
    console.log(typeof a);//string

原型模式

ECMAScript 5以前JS是沒有類的,W3C的大牛是有所發現的,因此ES6會新的類語法,可是各大瀏覽器(IE)猴年馬月纔跟進就不得而知了——絕對不是針對IE。設計模式

在靜態類型語言開發中,類是任意實體的結構,當須要這個實體的時候,系統依照這個結構,「畫」出這個==對象==。
譬如,要設計車,首先是要設計稿,設計稿就是類,而後照着設計稿生產的每一臺車就是具體的對象。
因此,靜態類型語言當中,獲得一個對象的最關鍵就是先有這個對象的設計稿(類),就是知道它的類型。瀏覽器

可是,JS不同。
JS是以原型模式做爲設計基礎。獲得一個對象以前,咱們不關心對象屬於什麼類型,它的設計稿是怎麼樣的,JS是經過拷貝一個其餘對象而得到對象的。安全

//定義超人是超人,超人能夠飛
    var superman = {
        name: "superman",
        fly: function () {
            console.log(this.name + " is flying");
        }
    };
    superman.fly();//superman is flying

    //萬一,想要個妹子呢,搞個女超人如何?女超人也會飛?要不就複製一個超人的對象吧。
    //經過Object.create複製一個超人,但定義它是女超人。
    var superwomen = Object.create(superman);

    superwomen.name = "superwomen";
    superwomen.fly();//superwomen is flying

    //DC的不喜歡,來個漫威吧,再搞個鋼鐵俠?鋼鐵俠也會飛
    var ironman = Object.create(superman);

    ironman.name = "ironman";
    ironman.fly();//ironman is flying

千萬別誤解了superman就是類,而superwomen、ironman就是superman實例化獲得的對象,superman、superwomen、ironman都是對象,superwomen、ironman都是superman對象拷貝的結果。閉包

一、全部的數據都是對象

上面的例子,經過var obj={}建立對象,其等價於var obj=new Object()。Object對象能夠建立對象,但不只是Object纔是對象,全部的數據都是對象,包括Number、Boolean、String、Function、Object。這是原型模式很重要的一條原則。
正是這一點,咱們也能夠利用function定義對象:function obj(){};

不過與var obj=new Object()不一樣,function定義的對象,是一個帶構造器的函數,經過new關鍵字調用構造器能夠獲取其原型對象。參考如下例子。

//定義一個對象
    var superman = {
        name: "superman",
        fly: function () {
            console.log(this.name + " is flying");
        }
    };
    superman.fly();//superman is flying

    //定義一個函數,同時也是一個帶構造器的函數
    function superman2() {
        this.name = "superman";
        this.fly = function () {
            console.log(this.name + " is flying");
        };
    };

    //錯誤:superman2是函數,fly()不是函數
    superman2.fly();//error:undefined is not a function

    //錯誤:superman2()是調用函數,但函數沒有返回帶fly()函數的對象
    superman2().fly();//error:Cannot read property 'fly' of undefined

    //正確:new不是C#實例化關鍵字,new是調用superman2的構造器並返回其對象
    var sm = new superman2();
    sm.fly();//superman is flying

二、對象的屬性請求,會從對象傳遞到對象原型

當須要得到對象屬性時,會首先請求對象自己,若是,對象自己沒有屬性能夠響應,則響應其對象原型prototype;若是prototype也沒有屬性能夠響應,則進一步請求原型的原型,從而造成一條原型鏈。固然,原型鏈的層級不能太長,具體長度沒有研究過,我以爲3級也差很少了。

//定義人
    function human() {
        this.sexy = "male";
    };

    //定義全部英雄的原型
    function heroBase() {
        this.trait = "strong cool";
    };

    //英雄的原型是人
    heroBase.prototype = new human();

    //定義超人
    function superman() {
        this.fly = function () {
            console.log("superman");
        };
    };

    //超人的原型就是英雄
    superman.prototype = new heroBase();

    console.log(new superman().sexy);//male
    console.log(new superman().trait);//strong cool

這個步驟就是:

  • 超人的性別是神馬?

  • 光看超人不知道,要看超人原型:英雄

  • 看英雄也不知道,要看英雄原型:人

  • 人的sexy屬性告訴咱們,他是男人

好吧,也許你發現了,上面例子是有bug的,超人是外星人;但你確定發現了,這不就是繼承嗎?繼承都有了,多態還能遠嗎?

三、JS就是經過原型模式實現繼承的,且是單一繼承

我我的的理解是,JS的繼承跟C#的繼承雖然實現形式不一樣,但有一點原則是相同的,單一繼承非多繼承。如上例子換成如下代碼:
superman.prototype = new heroBase();
superman.prototype = new human();
最終結果是,超人的原型首先定爲英雄,但又被人所覆蓋了。符合單一繼承的原則。

四、特別補充:關於__proto__prototype的區別

以前一直覺得搞懂了原型模式,最近看到一篇很好文章 [學習筆記] 小角度看JS原型鏈 夢禪,漲姿式了。

在 segmentfault 上看到這樣一道題目:

var F = function(){};
Object.prototype.a = function(){};
Function.prototype.b = function(){};
var f = new F();

問:f 能取到a,b嗎?原理是什麼?

關鍵理解:
prototype是對象原型
__proto__是對象構造器的原型
關於二者區別,推薦一遍博文,解釋很是詳細,理解js中的原型鏈,prototype與__proto__的關係

本身列了例子,應該比較容易理解

var F = function () { };
    Object.prototype.a = function () {
        console.log("a");
    };
    Function.prototype.b = function () {
        console.log("b");
    };
    var f = new F();

    //注意:對象的__proto__屬性,指向對象的父級構造器的prototype原型

    console.log(f);

    console.log(f.__proto__);                         //F.prototype
    console.log(F.prototype.__proto__);               //Ojbect.prototype
    console.log(Object.prototype.a());                //a

    console.log(f.__proto__.__proto__.a());           //a

    /****************************************************************/

    console.log(f.constructor);                       //F
    console.log(F.__proto__);                         //Function.prototype
    console.log(Function.prototype.b());              //b

    console.log(f.constructor.__proto__.b());         //b

    //結論:全部對象的__proto__都指向其構造器的prototype
    //結論:全部構造器/函數的__proto__都指向Function.prototype,它是一個空函數(Empty function)
    //結論:Function.prototype的__proto__最終指向Object.prototype
  1. 全部對象的__proto__都指向其構造器的prototype

  2. 全部構造器/函數的__proto__都指向Function.prototype,它是一個空函數(Empty function)

  3. Function.prototype的__proto__最終指向Object.prototype

多態

JS的多態是經過兩點實現的:一、原型模式實現繼承;二、動態類型實現泛型

回想一下,咱們剛學習C#的那個經典例子

public class Hero
    {
        public virtual void Fly()
        {
            Console.WriteLine("hero fly");
        }
    }

    public class SuperMan : Hero
    {
        public override void Fly()
        {
            base.Fly();
            Console.WriteLine("SuperMan fly");
        }
    }

    public class IronMan : Hero
    {
        public override void Fly()
        {
            base.Fly();
            Console.WriteLine("IronMan fly");
        }
    }

    public class Main
    {
        public void MainFunction()
        {
            LetsFly(new SuperMan());
            LetsFly(new IronMan());
        }

        public void LetsFly(Hero hero)
        {
            hero.Fly();
        }
    }

再來看看JS的實現

function superman() {
        this.fly = function () {
            console.log("superman fly");
        };
    }

    function bird() {
        this.fly = function () {
            console.log("bird fly");
        };
    };

    function letsFly(hero) {
        hero.fly();
    }

    var sm = new superman();
    var b = new bird();
    letsFly(sm);//superman fly
    letsFly(b);//bird fly;

LetsFly()方法,C#跟JS接受的一樣都是Hero這個對象

對於C#而言,LetsFly方法要求不論是神馬Hero,只要實現了基類Hero的Fly,它就能執行LetsFly方法,之後即便有再多的英雄,繼承Hero便可。

而JS,LetsFly方法要求不論是神馬對象,只要有Fly方法,它就能執行LetsFly方法,之後即便有再多的英雄,英雄會Fly便可。

對比之下,優缺點是顯而易見的,前者受到束縛,尤爲在複雜類型之間繼承與引用提現的更加凸顯,但類型安全性高;

後者無拘無束,但類型安全性低,如上例子,LetsFly傳入的可能不是Hero,極可能是Bird。

閉包與高階函數

關於閉包「closure」,剛開始接觸的時候,百度了N遍相關教程仍是看得我一頭霧水,
據說還會有內存泄露的問題,好吧,在開發中乾脆就不用吧。直到有一天,偶爾看到高階函數,對於閉包以及整個JS語言的理解能夠說產生了翻天覆地的變化。

在這以前,先給你們回顧一下,閉包之因此成爲閉包以前,有一個很重要的前提概念:變量的生命週期。
在函數體內,var定義的變量,變量屬於函數內,函數外沒法訪問;反之若是沒有定義var的變量,像x=12,x屬於全局變量。
全局變量的生命週期是永久的,由頁面加載到關閉;而內部變量,從函數執行到結束,GC檢測到內部變量沒有再被使用則會銷燬它。

var fn = function () {
        var a = "a";
        b = "b";

        var fni = function () {
            c = "c";
        }();
    }();

    //console.log(a);//a is not defined
    console.log(b);//b
    console.log(c);//c

關鍵就在這,若是內部變量在函數執行結束後,仍然有被使用呢?

解決方案就是——高階函數

高階函數名字看起來高端,實際很簡單,知足如下條件其一就是高階函數:

一、函數的參數是函數;

二、函數的返回值是函數;

function myFunction() {
        console.log("我是個函數");
    };
    //高階函數1——函數的參數是函數
    function fnHO1(fn) {
        fn();
    }
    //高階函數2——函數的返回值是函數
    function fnHO2() {
        return myFunction;
    };

    //調用高階函數1
    fnHO1(myFunction);//我是個函數

    //調用高階函數2
    fnHO2();//神馬都沒有發生

    //調用高階函數2,得到的是它的內部函數體
    var f2 = fnHO2();
    //再執行,纔是調用它的內部函數
    f2();//我是個函數

在高階函數的基礎,咱們來看看,史上最經典的閉包實例。

function add_outer() {
        var i = 1;

        //返回值是內部函數,inner調用了其外部的變量
        //因此inner執行結束時,i沒有被銷燬
        return function inner() {
            i++;
            return i;
        };
    };

    //執行add_outer獲取的是它的內部函數體inner,但沒有執行
    var inner = add_outer();

    //真正執行的時候,變量i沒有被銷燬,造成遞增
    console.log(inner());//2
    console.log(inner());//3
    console.log(inner());//4

我對於閉包的理解很簡單:==函數返回值是個內部函數,內部調用了函數的內部變量==。

兩個條件,缺一不可。若是將以上例子改一下,內部有函數,但不是返回值,就不是閉包了。

function add_outer() {
        var i = 1;

        //返回值不是函數,造成不了閉包
        function inner() {
            i++;
            console.log(i);
        };
        inner();
    };

    //執行屢次,值也不會變
    add_outer();//2
    add_outer();//2

也許你們會疑問,既然內部變量i在函數結束後仍然使用,致使GC沒法回收其內存,那不就是內存泄露嗎?
是的。其實咱們反過來想,若是咱們不使用閉包的方式實現以上累加的例子,改成使用全局變量存放變量i,全局變量i是否一樣也是不能被回收呢?!

//function add_outer() {
        var i = 1;

        function inner() {
            i++;
            return i;
        };
    //};

    ////執行add_outer獲取的是它的內部函數體inner,但沒有執行
    //var inner = add_outer();

    //真正執行的時候,變量i沒有被銷燬,造成遞增
    console.log(inner());//2
    console.log(inner());//3
    console.log(inner());//4

所以,閉包與高階函數,只是函數式編程的編寫方式,即便並非形成內存泄露的緣由。關於閉包與內存泄露的問題,請移步 http://www.cnblogs.com/yakun/p/3932026.h...

原型模式、閉包與高階函數應該能夠說是JS設計模式的基礎要領吧。在下一章,再分享一下JS的幾種經常使用設計模式。

仍是像前面所說的,有什麼地方說錯,望你們指正。

相關文章
相關標籤/搜索