JavaScript設計模式之面向對象編程

爲了深刻地學習 javascript ,奔着一名標準 Web 開發人員的標準,想要深刻了解一下面向對象的編程思想,提升本身模塊化開發的能力,編寫可維護、高效率、可拓展的代碼,最近一直拜讀 《JavaScript設計模式》 ,對其重點內容作了概括與總結,若有總結的不詳細或者理解不透徹的,還望批評斧正~javascript

什麼是面向對象編程(OOP)?

簡單來講,面向對象編程就是將你的需求抽象成一個對象,而後對這個對象進行分析,爲其添加對應的特徵(屬性)與行爲(方法),咱們將這個對象稱之爲 。 面向對象一個很重要的特色就是封裝,雖然 javascript 這種解釋性的弱類型語言沒有像一些經典的強類型語言(例如C++,JAVA等)有專門的方式用來實現類的封裝,但咱們能夠利用 javascript 語言靈活的特色,去模擬實現這些功能,接下里咱們就一塊兒來看看~java

封裝

  • 建立一個類

javascript 中要建立一個類是很容易的,比較常見的方式就是首先聲明一個函數保存在一個變量中(通常類名首字母大寫),而後將這個函數(類)的內部經過對 this 對象添加屬性或者方法來實現對類進行屬性或方法的添加,例如:編程

//建立一個類
var Person = function (name, age ) {
	this.name = name;
	this.age = age;
}
複製代碼

咱們也能夠在類的原型對象(prototype)上添加屬性和方法,有兩種方式,一種是一一爲原型對象的屬性賦值,以一種是將一個對象賦值給類的原型對象:設計模式

//爲類的原型對象屬性賦值
Person.prototype.showInfo = function () {
    //展現信息
    console.log('My name is ' + this.name , ', I\'m ' + this.age + ' years old!');
}

//將對象賦值給類的原型對象
Person.prototype = {
    showInfo : function () {
	    //展現信息
	    console.log('My name is ' + this.name , ', I\'m ' + this.age + ' years old!');
	}
}
複製代碼

這樣咱們就將所須要屬性和方法都封裝在 Person 類裏面了,當咱們要用的時候,首先得須要使用 new 關鍵字來實例化(建立)新的對象,經過 . 操做符就可使用實例化對象的屬性或者方法了~數組

var person = new Person('Tom',24);
console.log(person.name)        // Tom
console.log(person.showInfo())  // My name is Tom , I'm 24 years old!
複製代碼

咱們剛說到有兩種方式來添加屬性和方法,那麼這兩種方式有啥不一樣呢?安全

經過 this 添加的屬性和方法是在當前對象添加的,而 javascript 語言的特色是基於原型 prototype 的,是經過 原型prototype 指向其繼承的屬性和方法的;經過 prototype 繼承的方法並非對象自身的,使用的時候是經過 prototype 一級一級查找的,這樣咱們經過 this 定義的屬性或者方法都是該對象自身擁有的,咱們每次經過 new 運算符建立一個新對象時, this 指向的屬性和方法也會獲得相應的建立,可是經過 prototype 繼承的屬性和方法是每一個對象經過 prototype 訪問獲得,每次建立新對象時這些屬性和方法是不會被再次建立的,以下圖所示:bash

其中 constructor 是一個屬性,當建立一個函數或者對象的時候都會給原型對象建立一個 constructor 屬性,指向擁有整個原型對象的函數或者對象。框架

若是咱們採用第一種方式給原型對象(prototype)上添加屬性和方法,執行下面的語句會獲得 true模塊化

console.log(Person.prototype.constructor === Person ) // true
複製代碼

那麼好奇的小夥伴會問,那我採用第二種方式給原型對象(prototype)上添加屬性和方法會是什麼結果呢?函數

console.log(Person.prototype.constructor === Person ) // false
複製代碼

臥槽,什麼鬼,爲何會產生這種結果?

緣由在於第二種方式是將一整個對象賦值給了原型對象(prototype),這樣會致使原來的原型對象(prototype)上的屬性和方法會被所有覆蓋掉(pass: 實際開發中兩種方式不要混用),那麼 constructor 的指向固然也發生了變化,這就致使了原型鏈的錯亂,所以,咱們須要手動修正這個問題,在原型對象(prototype)上手動添加上 constructor 屬性,從新指向 Person ,保證原型鏈的正確,即:

Person.prototype = {
		constructor : Person ,
		showInfo : function () {
			//展現信息
			console.log('My name is ' + this.name , ', I\'m ' + this.age + ' years old!'); } } console.log(Person.prototype.constructor === Person ) // true 複製代碼
  • 屬性與方法的封裝

在大部分面向對象的語言中,常常會對一些類的屬性和方法進行隱藏和暴露,因此就會有 私有屬性、私有方法、公有屬性、公有方法等這些概念~

ES6 以前, javascript 是沒有塊級做用域的,有函數級做用域,即聲明在函數內部的變量和方法在外部是沒法訪問的,能夠經過這個特性模擬建立類的 私有變量私有方法 ,而函數內部經過 this 建立的屬性和方法,在類建立對象的時候,每一個對象都會建立一份並可讓外界訪問,所以咱們能夠將經過 this 建立的屬性和方法看做是 實例屬性實例方法,然而經過 this 建立的一些方法們不但能夠訪問對象公有屬性和方法,還能訪問到類(建立時)或對象自身的私有屬性和私有方法,因爲權力這些方法的權力比較大,所以成爲 特權方法 ,經過 new 建立的對象沒法經過 . 運算符訪問類外面添加的屬性和和方法,只能經過類自己來訪問,所以,類外部定義的屬性和方法被稱爲類的 靜態公有屬性靜態公有方法 , 經過類的原型 prototype 對象添加的屬性和方法,其實例對象都是經過 this 訪問到的,因此咱們將這些屬性和方法稱爲 公有屬性公有方法,也叫 原型屬性原型方法

//建立一個類
	var Person = function (name, age ) {
    	    //私有屬性
    	    var IDNumber = '01010101010101010101' ;
    	    //私有方法
            function checkIDNumber () {}
            //特權方法
            this.getIDNumber = function () {}
            //實例屬性
            this.name = name;
            this.age = age;
            //實例方法
            this.getName = function () {}
	}

	//類靜態屬性
        Person.isChinese = true;
	//類靜態方法
        Person.staticMethod = function () {
            console.log('this is a staticMethod')
        }

        //公有屬性
	Person.prototype.isRich = false;
	//公有方法
        Person.prototype.showInfo = function () {}
複製代碼

經過 new 建立的對象只能訪問到對應的 實例屬性 、實例方法 、原型屬性 和 原型方法 ,而沒法訪問到類的靜態屬性和私有屬性,類的私有屬性和私有方法只能經過類自身方法,即:

var person = new Person('Tom',24);

        console.log(person.IDNumber) // undefined
        console.log(person.isRich)  // false
        console.log(person.name) // Tom
        console.log(person.isChinese) // undefined
        
        console.log(Person.isChinese) // true
        console.log(Person.staticMethod()) // this is a staticMethod
複製代碼
  • 建立對象的安全模式

咱們在建立對象的時候,若是咱們習慣了 jQuery 的方式,那麼咱們極可能會在實例化對象的時候忘記用 new 運算符來構造,而寫出來下面的代碼:

//建立一個類
	var Person = function (name, age ) {
		this.name = name;
		this.age = age;
	}
	
	var person = Person('Tom',24)
複製代碼

這時候 person 已經不是咱們指望的那樣,是 Person 的一個實例了~

console.log(person)  // undifined
複製代碼

那麼咱們建立的 nameage 都不知去向了,固然不是,他們被掛到了 window 對象上了,

console.log(window.name)  // Tom
    console.log(window.age)   // 24
複製代碼

咱們在沒有使用 new 操做符來建立對象,當執行 Person 方法的時候,這個函數就在全局做用域中執行了,此時 this 指向的也就是全局變量,也就是 window 對象,因此添加的屬性都會被添加到 window 上,而咱們的 person 變量在獲得 Person 的執行結果時,因爲函數中沒有 return 語句, 默認返回了 undifined

爲了不這種問題的存在,咱們能夠採用安全模式解決,稍微修個一下咱們的類便可,

//建立一個類
	var Person = function (name, age) {
		// 判斷執行過程當中的 this 是不是當前這個對象 (若是爲真,則表示是經過 new 建立的)
		if ( this instanceof Person ) {
			this.name = name;
			this.age = age;
		} else {
			// 不然從新建立對象
			return new Person(name, age)
		}
	}
複製代碼

ok,咱們如今測試一下~

var person = Person('Tom', 24)
	console.log(person)         // Person
	console.log(person.name)    // Tom
	console.log(person.age)     // 24
	console.log(window.name)    // undefined
	console.log(window.age)     // undefined
複製代碼

這樣就能夠避免咱們忘記使用 new 構建實例的問題了~

pass:這裏我用的 window.name ,這個屬性比較特殊,它是 window 自帶的,用於設置或返回存放窗口的名稱的一個字符串,注意更換~

繼承

繼承也是面型對象的一大特徵,可是 javascript 中沒有傳統意義上的繼承,可是咱們依舊能夠藉助 javascript 的語言特點,模擬實現繼承

類式繼承

比較常見的一種繼承方式,原理就是咱們是實例化一個父類,新建立的對象會複製父類構造函數內的屬性和方法,並將圓形 __proto__ 指向父類的原型對象,這樣就擁有了父類原型對象上的方法和屬性,咱們在將這個對象賦值給子類的原型,那麼子類的原型就能夠訪問到父類的原型屬性和方法,進而實現了繼承,其代碼以下:

//聲明父類
    function Super () {
        this.superValue = 'super';
    }
    //爲父類添加原型方法
    Super.prototype.getSuperValue = function () {
        return this.superValue;   
    }
	
    //聲明子類
    function Child () {
        this.childValue = 'child';
    }
	
    //繼承父類
    Child.prototype = new Super();
    //爲子類添加原型方法
    Child.prototype.getChildValue = function () {
        return this.childValue;
    }
複製代碼

咱們測試一下~

var child = new Child();
    console.log(child.getSuperValue());  // super
    console.log(child.getChildValue());  // child
複製代碼

可是這種繼承方式會有兩個問題,第一因爲子類經過其原型 prototype 對其父類實例化,繼承父類,只要父類的公有屬性中有引用類型,就會在子類中被全部實例共用,若是其中一個子類更改了父類構造函數中的引用類型的屬性值,會直接影響到其餘子類,例如:

//聲明父類
    function Super () {
    	this.superObject = {
    		a: 1,
    		b: 2
    	}
    }


    //聲明子類
    function Child () {}
    
    //繼承父類
    Child.prototype = new Super();
    }

    var child1 = new Child();
    var child2 = new Child();
    console.log(child1.superObject);    // { a : 1 , b : 2 }
    child2.superObject.a = 3 ;
    console.log(child1.superObject);    // { a : 3,  b : 2 }
複製代碼

這會對後面的操做形成很大困擾!

第二,因爲子類是經過原型 prototype 對父類的實例化實現的,因此在建立父類的時間,沒法給父類傳遞參數,也就沒法在實例化父類的時候對父類構造函數內部的屬性進行初始化操做。

爲了解決這些問題,那麼就衍生出其餘的繼承方式。

構造函數繼承 利用 call 這個方法能夠改變函數的做用環境,在子類中調用這個方法,將子類中的變量在父類中執行一遍,因爲父類中是給 this 綁定的, 所以子類也就繼承了父類的實例屬性,即:

//聲明父類
    function Super (value) {
    	this.value = value;
    	this.superObject = {
    		a: 1,
    		b: 2
    	}
    }

    //爲父類添加原型方法
    Super.prototype.showSuperObject = function () {
    	console.log(this.superValue);
    }

    //聲明子類
    function Child (value) {
    	// 繼承父類
        Super.call(this,value)
    }
    
    var child1 = new Child('Tom');
    var child2 = new Child('Jack');

    child1.superObject.a = 3 ;
    console.log(child1.superObject);    // { a : 3 , b : 2 }
    console.log(child1.value)           // Tom
    console.log(child2.superObject);    // { a : 1,  b : 2 }
    console.log(child2.value);          // Jack
複製代碼

Super.call(this,value) 這段代碼是構造函數繼承的精華,這樣就能夠避免類式繼承的問題了~

但這種繼承方式沒有涉及到原型 prototype , 因此父類的原型方法不會獲得繼承,而若是要想被子類繼承,就必需要放到構造函數中,這樣建立出來的每一個實例都會單獨擁有一份,不能共用,爲了解決這個問題,有了 組合式繼承。

組合式繼承

咱們只要在子類的構造函數做用環境中執行一次父類的構造函數,在將子類的原型 prorotype 對父類進行實例化一次,就能夠實現 組合式繼承 , 即:

//聲明父類
    function Super (value) {
    	this.value = value;
    	this.superObject = {
    		a: 1,
    		b: 2
    	}
    }
    
    //爲父類添加原型方法
    Super.prototype.showSuperObject = function () {
    	console.log(this.superObject);
    }
    
    //聲明子類
    function Child (value) {
    	// 構造函數式繼承父類 value 屬性
        Super.call(this,value)
    }
    
    //類式繼承
    Child.prototype = new Super();
    
    var child1 = new Child('Tom');
    var child2 = new Child('Jack');
    
    child1.superObject.a = 3 ;
    console.log(child1.showSuperObject());      // { a : 3 , b : 2 }
    console.log(child1.value)                   // Tom
    child1.superObject.b = 3 ;
    console.log(child2.showSuperObject());      // { a : 1,  b : 2 }
    console.log(child2.value);                  // Jack
複製代碼

這樣就能融合類式繼承和構造函數繼承的有點,而且過濾掉其缺點。 看起來是否是已經很完美了,NO , 細心的同窗能夠發現,咱們在使用構造函數繼承時執行了一遍父類的構造函數,而在實現子類原型的類式繼承時又調用了一父類的構造函數,那麼父類的構造函數執行了兩遍,這一點是能夠繼續優化的。

寄生組合式繼承

咱們上面學習了 組合式繼承 ,也看出了這種方式的缺點,因此衍生出了 寄生組合式繼承 ,其中 寄生 是寄生式繼承 ,而寄生式繼承依託於原型式繼承,所以學習以前,咱們得了解一下 原型式繼承寄生式繼承

原型式繼承跟類式繼承相似,固然也存在一樣的問題,代碼以下:

//原型式繼承
    function inheritObject (o) {
        // 聲明一個過渡函數對象
        function F () {}
    	// 過渡對象的原型繼承父對象
    	F.prototype = o;
        // 返回過渡對象的一個實例,該實例的原型繼承了父對象
    	return new F();
    }
    var Super = {
    	name : 'Super' ,
        object : {
    		a : 1 ,
            b : 2
        }
    }
    var child1 = inheritObject(Super);
    var child2 = inheritObject(Super);
    console.log(child1.object) // { a : 1 , b : 2 }
    child1.object.a = 3 ;
    console.log(child2.object) // { a : 3 , b : 2 }
複製代碼

寄生式繼承是對原型繼承的第二次封裝,並在封裝過程當中對對象進行了拓展,新對象就有了新增的屬性和方法,實現方式以下:

//原型式繼承
    function inheritObject (o) {
        // 聲明一個過渡函數對象
        function F () {}
    	// 過渡對象的原型繼承父對象
    	F.prototype = o;
        // 返回過渡對象的一個實例,該實例的原型繼承了父對象
    	return new F();
    }
    
    // 寄生式繼承
    // 聲明基對象
    var Super = {
    	name : 'Super' ,
        object : {
    		a : 1 ,
            b : 2
        }
    }
    
    function createChild (obj) {
    	// 經過原型繼承建立新對象
        var o = new inheritObject(obj);
        // 拓展新對象
        o.getObject = function () {
            console.log(this.object)
    	}
    	return o;
    }
複製代碼

咱們將二者的特色結合起來就出現了寄生組合式繼承,經過借用構造函數來繼承屬性,經過原型鏈的混成形式來繼承方法,

/**
    * 寄生組合式繼承
    * 傳遞參數
    *   childClass 子類
    *   superClass 父類
    * */
    
    //原型式繼承
    function inheritObject (o) {
       // 聲明一個過渡函數對象
       function F () {}
       // 過渡對象的原型繼承父對象
       F.prototype = o;
       // 返回過渡對象的一個實例,該實例的原型繼承了父對象
       return new F();
    }
    
    function inheritPrototype (childClass , superClass) {
        // 複製一份父類的原型保存在變量中
        var p = inheritObject(superClass.prototype);
        // 修復子類的 constructor
        p.constructor = childClass;
        // 設置子類的原型
        childClass.prototype = p;
    }
複製代碼

咱們須要繼承父類的原型,不須要在調用父類的構造函數,咱們只須要父類原型的一個副本,而這個副本咱們是能夠經過原型繼承拿到,若是直接賦值給子類對象,會致使子類的原型錯亂,由於父類的原型對象複製到 P 中的 constructor 指向的不是子類的對象,因此經行了修正,並賦值給子類的原型,這樣子類也就繼承了父類的原型,可是沒有執行父類的構造方法。

ok,測試一下:

// 定義父類
    function SuperClass (name) {
    	this.name = name;
    	this.object = {
    		a: 1,
    		b: 2
    	}
    }
    // 定義父類的原型
    SuperClass.prototype.showName = function () {
        console.log(this.name)
    }
    
    // 定義子類
    function ChildClass (name,age) {
        // 構造函數式繼承
        SuperClass.call(this,name);
        // 子類新增屬性
        this.age = age;
    }
    
    // 寄生式繼承父類原型
    inheritPrototype(ChildClass,SuperClass);
    // 子類新增原型方法
    ChildClass.prototype.showAge = function () {
        console.log(this.age)
    }
    
    //
    var child1 = new ChildClass('Tom',24);
    var child2 = new ChildClass('Jack',25);
    
    console.log(child1.object)  // { a : 1 , b : 2 }
    child1.object.a = 3 ;
    console.log(child1.object)  // { a : 3 , b : 2 }
    console.log(child2.object)  // { a : 1 , b : 2 }
    
    console.log(child1.showName())  // Tom
    console.log(child2.showAge())   // 25
複製代碼

如今沒問題了哈,以前的問題也都解決了,大功告成~

多繼承

JavaC++ 面向對象中會有多繼承你的概念,可是 javascript 的繼承是依賴原型鏈實現的,可是原型鏈只有一條,理論上是不能實現多繼承的。可是咱們能夠利用 javascript 的靈活性,能夠經過繼承多個對象的屬性來實現相似的多繼承。

首先,咱們來看一個比較經典的繼承單對象屬性的方法 —— extend

function extend (target,source) {
    //遍歷源對象中的屬性
    for( var property in source ){
    	//將源對象中的屬性複製到目標對象
        target[property] = source[property]
    }
     // 返回目標對象
    return target;
}
複製代碼

可是這個方法是一個淺複製過程,也就是說只能複製基本數據類型,對於引用類型的數據達不到預期效果,也會出現數據篡改的狀況:

var parent = {
	name: 'super',
	object: {
		a: 1,
		b: 2
	}
}

var child = {
	age: 24
}

extend(child, parent);

console.log(child);     //{ age: 24, name: "super", object: { a : 1 , b : 2 } }
child.object.a = 3;
console.log(parent);    //{ name: "super", object: { a : 3 , b : 2 } }
複製代碼

順着這個思路,要實現多繼承,就要將傳入的多個對象的屬性複製到源對象中,進而實現對多個對象的屬性繼承,咱們能夠參考 jQuery 框架中的 extend 方法,對咱們上面的函數進行改造~

//判斷一個對象是不是純對象
function isPlainObject(obj) {
    var proto, Ctor;

    // (1) null 確定不是 Plain Object
    // (2) 使用 Object.property.toString 排除部分宿主對象,好比 window、navigator、global
    if (!obj || ({}).toString.call(obj) !== "[object Object]") {
        return false;
    }

    proto = Object.getPrototypeOf(obj);

    // 只有從用 {} 字面量和 new Object 構造的對象,它的原型鏈纔是 null
    if (!proto) {
        return true;
    }

    // (1) 若是 constructor 是對象的一個自有屬性,則 Ctor 爲 true,函數最後返回 false
    // (2) Function.prototype.toString 沒法自定義,以此來判斷是同一個內置函數
    Ctor = ({}).hasOwnProperty.call(proto, "constructor") && proto.constructor;
    return typeof Ctor === "function" && Function.prototype.toString.call(Ctor) === Function.prototype.toString.call(Object);
}

function extend() {
    var name, options, src, copy, clone, copyIsArray;
    var length = arguments.length;
    // 默認不進行深拷貝
    var deep = false;
    // 從第二個參數起爲被繼承的對象
    var i = 1;
    // 第一個參數不傳佈爾值的狀況下,target 默認是第一個參數
    var target = arguments[0] || {};
    // 若是第一個參數是布爾值,第二個參數是 target
    if (typeof target == 'boolean') {
        deep = target;
        target = arguments[i] || {};
        i++;
    }
    // 若是target不是對象,咱們是沒法進行復制的,因此設爲 {}
    if (typeof target !== "object" && !( typeof target === 'function')) {
        target = {};
    }

    // 循環遍歷要複製的對象們
    for (; i < length; i++) {
        // 獲取當前對象
        options = arguments[i];
        // 要求不能爲空 避免 extend(a,,b) 這種狀況
        if (options != null) {
            for (name in options) {
                // 目標屬性值
                src = target[name];
                // 要複製的對象的屬性值
                copy = options[name];

                // 解決循環引用
                if (target === copy) {
                    continue;
                }

                // 要遞歸的對象必須是 plainObject 或者數組
                if (deep && copy && (isPlainObject(copy) ||
                    (copyIsArray = Array.isArray(copy)))) {
                    // 要複製的對象屬性值類型須要與目標屬性值相同
                    if (copyIsArray) {
                        copyIsArray = false;
                        clone = src && Array.isArray(src) ? src : [];

                    } else {
                        clone = src && isPlainObject(src) ? src : {};
                    }

                    target[name] = extend(deep, clone, copy);

                } else if (copy !== undefined) {
                    target[name] = copy;
                }
            }
        }
    }

    return target;
};
複製代碼

該方法默認是淺拷貝,即:

var parent = {
    name: 'super',
    object: {
        a: 1,
        b: 2
    }
}
var child = {
    age: 24
}

extend(child,parent)
console.log(child); // { age: 24, name: "super", object: { a : 1 , b : 2 } }
child.object.a = 3;
console.log(parent) // { name: "super", object: { a : 3 , b : 2 } }
複製代碼

咱們只須要將第一個參數傳爲 true , 就能夠深複製了,即:

extend(true,child,parent)
    console.log(child); // { age: 24, name: "super", object: { a : 1 , b : 2 } }
    child.object.a = 3;
    console.log(parent) // { name: "super", object: { a : 1 , b : 2 } }
複製代碼

ok~這些就是 javascript 中面向對象的一些知識,能仔細看到這裏的小夥伴,相信大家對 javascript 中面向對象編程有了進一步的認識和了解,也爲後面的設計模式的學習奠基了基礎,接下來也會繼續分享 javascript 中不一樣的設計模式,歡迎喜歡的小夥伴持續關注~

相關文章
相關標籤/搜索