Javascript 面向對象程序設計

1、理解對象

兩個屬性類型

一、數據屬性

數據屬性有四個描述其行爲的特性:bash

  • configurable:表示可否用delete關鍵字刪除;可否修改屬性的特性,或者修改成訪問器屬性。經過對象字面量定義的屬性,該特性默認爲true。
  • enumerable:可否經過for...in遍歷到該屬性。經過對象字面量定義的屬性,該特性默認爲true。
  • writable:可否修改屬性的值。經過對象字面量定義的屬性,該特性默認爲true。
  • value:該屬性的值。默認值爲undefined。

二、訪問器屬性

訪問器屬性有四個描述其行爲的特性:app

  • configurable:表示可否用delete關鍵字刪除;可否修改屬性的特性,或者修改成訪問器屬性。經過對象字面量定義的屬性,該特性默認爲true。
  • enumerable:可否經過for...in遍歷到該屬性。經過對象字面量定義的屬性,該特性默認爲true。
  • get:讀取屬性時調用該函數,默認爲undefined。
  • set:寫入屬性時調用該函數,默認爲undefined。

定義屬性以及屬性類型

一、定義單個屬性

var person = {};
Object.defineProperty(person,'name',{
    configurable:true,
    enumerable:true,
    writable:true,
    value:'Eallon',
})
複製代碼

值得注意的是,若是將configurable的值設置爲false,那麼該屬性將永遠不能再設置成true了。函數

二、定義多個屬性

var hero = {};
Object.defineProperties(person,{
    //不可修改的屬性
    name:{
        writable:false, 
        value:'VN'
    },
    // 能夠修改的屬性
    position:{
        writable:true,
        value:'ADC'
    },
    // 前面加"_"的屬性,咱們通常約定爲內部訪問的屬性
    _attackSpeed:{
      writable:true,
      value:0.8
    },
    // 訪問器屬性
    attackSpeed:{
        get:function(){
            return this._attackSpeed;
        },
        set:function(newValue){
            this._attackSpeed = newValue > 2.5 ? 2.5 : newValue;
        }
    }
})
複製代碼

讀取屬性的特性

var descriptor = Object.getOwnPropertyDescriptor(hero,'attackSpeed');
 console.log(descriptor);
複製代碼

2、建立對象

工廠模式

工廠模式,將建立對象的代碼封裝在一個簡單的函數中,而後返回新建立的對象。ui

function createHero(name,position){
    var hero = new Object();
    hero.name = name;
    hero.position = position;
    hero.say = function(){
        console.log("I am " + this.name);
    };
    return hero;
}
var VN = createHero('VN','ADC');
var EZ = createHero('EZ','Mid');
複製代碼

優勢:一套代碼能夠建立多個類似的對象。this

缺點:建立的對象都是屬於Object類,沒法標識其具體類型。spa

構造函數模式

function Hero(name,position){
        this.name = name;
        this.position = position;
        this.say = function () {
            console.log("I am " + this.name);
        }
    }
    var VN = new Hero("VN","ADC");
    var EZ = new Hero("EZ","Mid");
複製代碼

優勢:使用構造函數建立的對象已經能區分對象類型了,如VN和EZ都是Hero的實例。prototype

缺點:相同的方法,在多個實例上要重複建立,如say方法。 指針

原型模式

function Hero(){

    }
    Hero.prototype.name = "VN";
    Hero.prototype.position = "ADC";
    Hero.prototype.sup = ['蕾歐娜','布隆'];
    Hero.prototype.say = function () {
        console.log("我叫" + this.name + ",個人輔助們: "+ this.sup.toString() );
    };
    var VN = new Hero();
    var EZ = new Hero();

    EZ.sup.push('拉克絲');

    EZ.name = "EZ";

    VN.say();   // 我叫VN,個人輔助們: 蕾歐娜,布隆,拉克絲
    EZ.say();   // 我叫EZ,個人輔助們: 蕾歐娜,布隆,拉克絲
複製代碼

在構造函數的prototype上定義的屬性是全部實例共用的,每一個實例都有一個內部指針(__proto__)來指向構造函數的prototype對象。也就是說,構造函數的prototype與全部實例的__proto__是指向同一個內存地址的。code

將共用的方法定義到prototype上是再合適不過了,這樣就解決了構造函數中定義的方法在實例初始化時重複建立的問題,如今全部的實例都統一使用原型上的say方法,你們都不須要再本身單首創建say方法了。cdn

可是,若是將一個引用類型的屬性定義到prototype上會存在隱患,好比EZ將「拉克絲」加入到了輔助列表,結果VN獲取輔助列表時發現本身也有「拉克絲」了,這是由於EZ將「拉克絲」加入到prototype的sup中了,全部的實例都會受到影響。

固然,若是隻是基本類型的屬性,卻是問題也不大,好比EZ定義了name="EZ",其實EZ只是在本身的實例上定義了一個新的name屬性,而並無更改prototype中的name屬性,prototype的name屬性仍是VN。

組合使用構造函數 + 原型模式(最經常使用)

單獨使用構造函數或原型模式都會有一些問題,可是將二者結合使用取長補短,則能夠完美解決對象的建立問題,咱們把上面的例子用組合方式再寫一遍。

function Hero(name,position){
        this.name = name;
        this.position = position;
        this.sup = ['蕾歐娜','布隆'];
    }

    Hero.prototype.say = function () {
        console.log("我叫" + this.name + ",個人輔助們: "+ this.sup.toString() );
    };
    var VN = new Hero('VN','ADC');
    var EZ = new Hero('EZ','Mid');

    EZ.sup.push('拉克絲');

    VN.say();   // 我叫VN,個人輔助們: 蕾歐娜,布隆
    EZ.say();   // 我叫EZ,個人輔助們: 蕾歐娜,布隆,拉克絲
複製代碼

如今,咱們在構造函數中定義實例特有的屬性(name、position、sup),在構造函數的prototype上定義全部實例共用的方法(say)。VN和EZ本身維護本身的輔助們,互不干涉,可是他們可使用共同的say方法。

動態原型模式

function Hero(name,position){
        this.name = name;
        this.position = position;
        this.sup = ['蕾歐娜','布隆'];
        if(typeof this.say !== "function"){
            Hero.prototype.say = function () {
                console.log("我叫" + this.name + ",個人輔助們: "+ this.sup.toString() );
            };
        }
    }
    
    var VN = new Hero('VN','ADC');
    var EZ = new Hero('EZ','Mid');

    EZ.sup.push('拉克絲');

    VN.say();   // 我叫VN,個人輔助們: 蕾歐娜,布隆
    EZ.say();   // 我叫EZ,個人輔助們: 蕾歐娜,布隆,拉克絲
複製代碼

動態原型模式,將關於prototype的定義寫到了構造函數中,這樣的好處是看起來封裝性和總體性更好一點。

在構造函數中,先判斷say函數是否已經存在,也就是說say函數是在VN實例化的時候建立到prototype中的,等到EZ實例化的時候,prototype中已經存在say函數了,就不會再次建立了。

寄生構造函數模式

function Hero(name,position){
        var hero = new Object();
        hero.name = name;
        hero.position = position;
        hero.sup = ['蕾歐娜','布隆'];
        hero.say = function () {
            console.log("我叫" + this.name + ",個人輔助們: "+ this.sup.toString() );
        };
        return hero;
    }

    var VN = new Hero('VN','ADC');
    var EZ = new Hero('EZ','Mid');
複製代碼

咱們發現Hero函數裏面的代碼跟工廠模式差很少,惟一不一樣的是調用方式不一樣,該模式是用new關鍵字來調用Hero函數的。雖然是用構造函數的方式初始化的實例,可是獲得的對象卻與構造函數以及構造函數的原型沒有任何關係,跟普通對象沒有什麼區別。因此,儘可能不要用這種方式建立對象,由於徹底沒有必要。

穩妥構造函數模式

function Hero(name,position){
        var hero = new Object();
        hero.say = function () {
            console.log("我叫" + name + ",我打" + position);
        };
        return hero;
    }

    var VN = Hero('VN','ADC');
    var EZ = Hero('EZ','Mid');
    VN.say();   // 我叫VN,我打ADC
    EZ.say();   // 我叫EZ,我打Mid
複製代碼

穩妥構造函數模式,不使用new和this關鍵字,直接調用Hero函數。調用Hero時傳進去的參數,在返回的hero對象中沒法直接訪問,只能經過say方法來訪問內部的成員變量。


3、繼承

原型鏈

原型鏈是什麼?
每一個實例對象都有一個指向構造函數原型的內部指針(__proto__),實例可使用原型中的屬性和方法,若是咱們把子類構造函數的prototype指向父類的一個實例,那麼子類實例的__proto__的__proto__就是父類構造函數的prototype,那麼子類實例也可使用父類構造函數的prototype中定義的屬性和方法了。像這樣,實例與構造函數原型之間造成的這個鏈條就是原型鏈。

function Hero(name,gender) {
        this.name = name;
        this.gender = gender;
        if(typeof this.say != 'function'){
            Hero.prototype.say = function () {
                console.log("I am " + this.name + ", I am " + this.gender);
            }
        }
    }
    function ADC(name,gender) {
        this.name = name;
        this.gender = gender;
        this.position = "ADC";  // 子類擴展的屬性
    }
    ADC.prototype = new Hero();
    var VN = new ADC("VN",'female');
複製代碼

使用原型鏈的繼承方式,咱們實現了子類能夠複用父類原型上的屬性和方法。
可是,咱們仍是會面臨如下幾個問題:

  • 父類實例上的屬性和方法被放到了子類原型上,ADC的原型上有父類的實例屬性name和gender。
  • 初始化父類實例給子類原型賦值時,不知道怎麼傳參數。也所以,ADC原型的name和gender都是undefined。
  • 子類構造函數中重複寫了一遍父類構造函數的代碼

借用構造函數

function Hero(name,gender) {
        this.name = name;
        this.gender = gender;
        if(typeof this.say != 'function'){
            Hero.prototype.say = function () {
                console.log("I am " + this.name + ", I am " + this.gender);
            }
        }
    }
    function ADC(name,gender) {
        Hero.apply(this,arguments);
        this.position = 'ADC';
    }
    ADC.prototype = new Hero();
    var VN = new ADC("VN",'female');
複製代碼

爲了解決子類構造函數中重複寫父類構造函數代碼的問題,咱們在子類ADC的構造函數中用apply或者call方法調用父類構造函數Hero,而後再聲明子類ADC本身的屬性position。
此處應注意,爲了防止調用父類構造函數時覆蓋子類本身的屬性,咱們應該像上面同樣,先調用父類構造函數,再聲明子類本身的屬性。

組合繼承

上面的例子其實就已是組合繼承的例子了。咱們在子類的構造函數中調用父類的構造函數來使子類也有父類同樣的實例屬性,而後將父類的一個實例賦值給子類的原型,從而使子類實例與父類原型之間產生了一條原型鏈。這樣就使得子類既繼承了父類原型的屬性,也繼承了父類實例的屬性。
這種組合繼承的方式是使用最普遍的,但也仍是沒有解決子類原型中出現父類實例屬性的問題,雖然這個問題不影響正常使用,但總以爲不太完美,下面會有更加完美的解決方案。O(∩_∩)O

原型式繼承

function object(o) {
        function F() {}
        F.prototype = o;
        return new F();
    }

    var ADC = {
        position:'ADC',
        say:function () {
            console.log("I am " + this.position);
        }
    };
    var VN = object(ADC);
    VN.name = 'VN';
複製代碼

原型式繼承的思路是,以一個已知的對象爲原型建立一個新的對象,而後在新的對象上能夠擴展新的屬性。

ECMAscript5 新增的Object.create()方法規範化了原型化繼承。該方法接受兩個參數,第二個參數是擴展在實例上的屬性,第二個參數的格式與Object.defineProperties的第二個參數的格式同樣。若是隻傳第一個參數,則與上面的object函數的行爲同樣。
上面的object例子,能夠這樣寫:

var ADC = {
        position: 'ADC',
        say: function () {
            console.log("I am " + this.position);
        }
    };
    var VN = Object.create(ADC, {
        name: {
            writable: false,
            value: 'VN'
        }
    });
複製代碼

寄生式繼承

function createHero(obj) {
    var clone = Object.create(obj);
    clone.attack = function () {
        console.log("走位,A")
    };
    return clone;
}
var hero = {
    name:'hero',
    say: function () {
        console.log(this.name);
    }
};
var vn = createHero(hero);
複製代碼

寄生式繼承與原型式繼承的區別是,在函數內部還擴展了實例屬性。

寄生組合式繼承

重頭戲來了!!!寄生組合式繼承是目前最理想的繼承方式。我的認爲,該方式也是最完美的繼承方式,由於該方式基本上解決了以前咱們遇到的全部問題。

// 繼承prototype
    function inheritPrototype(Sub, Super) {
        var prototype = Object.create(Super.prototype);
        prototype.constructor = Sub;
        Sub.prototype = prototype;
    }
    
    function Hero(name, gender) {
        this.name = name;
        this.gender = gender;
        if (typeof this.say != 'function') {
            Hero.prototype.say = function () {
                console.log(this.name);
            }
        }
    }
    
    function ADC(name, gender) {
        Hero.apply(this, arguments);
        this.position = "ADC";
    }

    inheritPrototype(ADC, Hero);
    
    ADC.prototype.attack = function () {
        console.log("ADC要走A");
    };
    
    var vn = new ADC("vn", 25);
複製代碼

該模式與組合式繼承相比的精髓之處體如今inheritPrototype函數中,此處咱們再也不是實例化一個Hero實例直接賦值給ADC的prototype,由於這樣作的話,ADC的prototype中會出現Hero實例的屬性,這不是咱們想要的。咱們是這樣作的,咱們以寄生的方式獲得一個以Hero的prototype對象爲原型的實例對象,該對象沒有實例方法,內部指針指向Hero的prototype。而後咱們將該實例對象的constructor屬性指向ADC,再而後賦值給ADC的prototype,這樣就完美實現了原型的繼承。

上面代碼中的Object.create方法的調用是關鍵一環,那它作了什麼?
首先它以Hero的prototype對象爲原型建立了一個構造函數,而後用新建立的構造函數實例化一個對象,那麼該實例對象的內部指針__proto__是指向Hero的prototype的(這是咱們的目的),而後把該對象返回。咱們拿到該對象後,要把該對象的constructor指向子類ADC,由於此時該對象的constructor是指向父類Hero的。再而後,就是把該實例對象賦值給ADC的prototype即實現了原型的繼承。(此處很重要,因此囉嗦了好幾遍O(∩_∩)O)

總結下寄生組合繼承的思路:

  1. 經過在子類構造函數中調用父類構造函數來繼承實例屬性,使用apply或者call方法實現。
  2. 獲取一個以父類prototype對象爲原型的實例對象(使用Object.create方法實現),將該對象的constructor屬性指向子類構造函數(由於此時該屬性的constructor仍是指向父類構造函數的)。
  3. 將第2步獲得的實例對象賦值給子類的prototype,實現原型的繼承。
相關文章
相關標籤/搜索