js:面向對象編程,帶你認識封裝、繼承和多態

本文首發於個人我的網站:cherryblog.sitejavascript

週末的時候深刻的瞭解了下javascript的面向對象編程思想,收穫頗豐,感受對面向對象編程有了那麼一丟丟的瞭解了~很開森php

什麼是面向對象編程

生動描述面向對象概念
生動描述面向對象概念

先上一張圖,能夠對面向對象有一個大體的瞭解,然而什麼是面向對象呢,用java中的一句經典語句來講就是:萬事萬物皆對象。面向對象的思想主要是以對象爲主,將一個問題抽象出具體的對象,而且將抽象出來的對象和對象的屬性和方法封裝成一個類。

面向對象是把構成問題事務分解成各個對象,創建對象的目的不是爲了完成一個步驟,而是爲了描敘某個事物在整個解決問題的步驟中的行爲。css

面向對象和麪向過程的區別

面向對象和麪向過程是兩種不一樣的編程思想,咱們常常會聽到二者的比較,剛開始編程的時候,大部分應該都是使用的面向過程的編程,可是隨着咱們的成長,仍是面向對象的編程思想比較好一點~
其實面向對象和麪向過程並非徹底相對的,也並非徹底獨立的。
我認爲面向對象和麪向過程的主要區別是面向過程主要是以動詞爲主,解決問題的方式是按照順序一步一步調用不一樣的函數。
而面向對象主要是以名詞爲主,將問題抽象出具體的對象,而這個對象有本身的屬性和方法,在解決問題的時候是將不一樣的對象組合在一塊兒使用。
因此說面向對象的好處就是可擴展性更強一些,解決了代碼重用性的問題。html

  • 面向過程就是分析出解決問題所須要的步驟,而後用函數把這些步驟一步一步實現,使用的時候一個一個依次調用就能夠了。
  • 面向對象是把構成問題事務分解成各個對象,創建對象的目的不是爲了完成一個步驟,而是爲了描敘某個事物在整個解決問題的步驟中的行爲。

有一個知乎的高票回答頗有意思,給你們分享一下~java

面向對象: 狗.吃(屎)
面向過程: 吃.(狗,屎)ios

具體的實現咱們看一下最經典的「把大象放冰箱」這個問題程序員

面向過程的解決方法

在面向過程的編程方式中實現「把大象放冰箱」這個問題答案是耳熟能詳的,一共分三步:編程

  1. 開門(冰箱);
  2. 裝進(冰箱,大象);
  3. 關門(冰箱)。

    面向對象的解決方法

  4. 冰箱.開門()
  5. 冰箱.裝進(大象)
  6. 冰箱.關門()

能夠看出來面向對象和麪向過程的側重點是不一樣的,面向過程是以動詞爲主,完成一個事件就是將不一樣的動做函數按順序調用。
面向對象是以主謂爲主。將主謂當作一個一個的對象,而後對象有本身的屬性和方法。好比說,冰箱有本身的id屬性,有開門的方法。而後就能夠直接調用冰箱的開門方法給其傳入一個參數大象就能夠了。
簡單的例子面向對象和麪向過程的好處還不是很明顯。bash

五子棋例子

下面是一個我認爲比較可以說明二者區別的一個栗子~:
例如五子棋,面向過程的設計思路就是首先分析問題的步驟:函數

  1. 開始遊戲
  2. 黑子先走
  3. 繪製畫面
  4. 判斷輸贏
  5. 輪到白子
  6. 繪製畫面
  7. 判斷輸贏
  8. 返回步驟2

把上面每一個步驟用分別的函數來實現,問題就解決了。

而面向對象的設計則是從另外的思路來解決問題。整個五子棋能夠分爲

  1. 黑白雙方,這兩方的行爲是如出一轍的
  2. 棋盤系統,負責繪製畫面

第一類對象(玩家對象)負責接受用戶輸入,並告知第二類對象(棋盤對象)棋子佈局的變化,棋盤對象接收到了棋子的i變化就要負責在屏幕上面顯示出這種變化,同時利用第三類對象(規則系統)來對棋局進行斷定。

能夠明顯地看出,面向對象是以功能來劃分問題,而不是步驟。一樣是繪製棋局,這樣的行爲在面向過程的設計中分散在了總多步驟中,極可能出現不一樣的繪製版本,由於一般設計人員會考慮到實際狀況進行各類各樣的簡化。而面向對象的設計中,繪圖只可能在棋盤對象中出現,從而保證了繪圖的統一。

功能上的統一保證了面向對象設計的可擴展性。好比我要加入悔棋的功能,若是要改動面向過程的設計,那麼從輸入到判斷到顯示這一連串的步驟都要改動,甚至步驟之間的循序都要進行大規模調整。若是是面向對象的話,只用改動棋盤對象就好了,棋盤系統保存了黑白雙方的棋譜,簡單回溯就能夠了,而顯示和規則判斷則不用顧及,同時整個對對象功能的調用順序都沒有變化,改動只是局部的。

再好比我要把這個五子棋遊戲改成圍棋遊戲,若是你是面向過程設計,那麼五子棋的規則就分佈在了你的程序的每個角落,要改動還不如重寫。可是若是你當初就是面向對象的設計,那麼你只用改動規則對象就能夠了,五子棋和圍棋的區別不就是規則嗎?(固然棋盤大小好像也不同,可是你會以爲這是一個難題嗎?直接在棋盤對象中進行一番小改動就能夠了。)而下棋的大體步驟從面向對象的角度來看沒有任何變化。

固然,要達到改動只是局部的須要設計的人有足夠的經驗,使用對象不能保證你的程序就是面向對象,初學者或者很蹩腳的程序員極可能以面向對象之虛而行面向過程之實,這樣設計出來的所謂面向對象的程序很難有良好的可移植性和可擴展性。

封裝

面向對象有三大特性,封裝、繼承和多態。對於ES5來講,沒有class的概念,而且因爲js的函數級做用域(在函數內部的變量在函數外訪問不到),因此咱們就能夠模擬 class的概念,在es5中,類其實就是保存了一個函數的變量,這個函數有本身的屬性和方法。將屬性和方法組成一個類的過程就是封裝。

封裝:把客觀事物封裝成抽象的類,隱藏屬性和方法的實現細節,僅對外公開接口。

經過構造函數添加

javascript提供了一個構造函數(Constructor)模式,用來在建立對象時初始化對象。
構造函數其實就是普通的函數,只不過有如下的特色

  • 首字母大寫(建議構造函數首字母大寫,即便用大駝峯命名,非構造函數首字母小寫)
  • 內部使用this
  • 使用 new生成實例

經過構造函數添加屬性和方法實際上也就是經過this添加的屬性和方法。由於this老是指向當前對象的,因此經過this添加的屬性和方法只在當前對象上添加,是該對象自身擁有的。因此咱們實例化一個新對象的時候,this指向的屬性和方法都會獲得相應的建立,也就是會在內存中複製一份,這樣就形成了內存的浪費。

function Cat(name,color){
        this.name = name;
        this.color = color;
        this.eat = function () {
            alert('吃老鼠')
        }
    }複製代碼

生成實例:

var cat1 = new Cat('tom','red')複製代碼

經過this定義的屬性和方法,咱們實例化對象的時候都會從新複製一份

經過原型prototype

在類上經過 this的方式添加屬性和對象會致使內存浪費的問題,咱們就考慮,有什麼方法可讓實例化的類所使用的方法直接使用指針指向同一個方法。因而,就想到了原型的方式

Javascript規定,每個構造函數都有一個prototype屬性,指向另外一個對象。這個對象的全部屬性和方法,都會被構造函數的實例繼承。
也就是說,對於那些不變的屬性和方法,咱們能夠直接將其添加在類的prototype 對象上。

 function Cat(name,color){
    this.name = name;
    this.color = color;
  }
  Cat.prototype.type = "貓科動物";
  Cat.prototype.eat = function(){alert("吃老鼠")};複製代碼

而後生成實例

var cat1 = new Cat("大毛","黃色");
  var cat2 = new Cat("二毛","黑色");
  alert(cat1.type); // 貓科動物
  cat1.eat(); // 吃老鼠複製代碼

這時全部實例的type屬性和eat()方法,其實都是同一個內存地址,指向prototype對象,所以就提升了運行效率。

在類的外部經過.語法添加

咱們還能夠在類的外部經過. 語法進行添加,由於在實例化對象的時候,並不會執行到在類外部經過. 語法添加的屬性,因此實例化以後的對象是不能訪問到. 語法所添加的對象和屬性的,只能經過該類訪問。

三者的區別

經過構造函數、原型和. 語法三者均可以在類上添加屬性和方法。可是三者是有必定的區別的。
構造函數:經過this添加的屬性和方法老是指向當前對象的,因此在實例化的時候,經過this添加的屬性和方法都會在內存中複製一份,這樣就會形成內存的浪費。可是這樣建立的好處是即便改變了某一個對象的屬性或方法,不會影響其餘的對象(由於每個對象都是複製的一份)。
原型:經過原型繼承的方法並非自身的,咱們要在原型鏈上一層一層的查找,這樣建立的好處是隻在內存中建立一次,實例化的對象都會指向這個prototype 對象,可是這樣作也有弊端,由於實例化的對象的原型都是指向同一內存地址,改動其中的一個對象的屬性可能會影響到其餘的對象
. 語法:在類的外部經過. 語法建立的屬性和方法只會建立一次,可是這樣建立的實例化的對象是訪問不到的,只能經過類的自身訪問

javascript也有private public protected

對於java程序員來講private public protected這三個關鍵字應該是很熟悉的哈,可是在js中,並無相似於private public protected這樣的關鍵字,可是咱們又但願咱們定義的屬性和方法有必定的訪問限制,因而咱們就能夠模擬private public protected這些訪問權限。
不熟悉java的小夥伴可能不太清楚private public protected概念(其餘語言我也不清楚有沒有哈,可是應該都是相似的~),先來科普一下小知識點~

  • public:public代表該數據成員、成員函數是對全部用戶開放的,全部用戶均可以直接進行調用
  • private:private表示私有,私有的意思就是除了class本身以外,任何人都不能夠直接使用,私有財產神聖不可侵犯嘛,即使是子女,朋友,都不可使用。
  • protected:protected對於子女、朋友來講,就是public的,能夠自由使用,沒有任何限制,而對於其餘的外部class,protected就變成private。

js中的private

由於javascript函數級做用域的特性(在函數中定義的屬性和方法外界訪問不到),因此咱們在函數內部直接定義的屬性和方法都是私有的。

js中的public

經過new關鍵詞實例化時,this定義的屬性和變量都會被複制一遍,因此經過this定義的屬性和方法就是公有的。
經過prototype建立的屬性在類的實例化以後類的實例化對象也是能夠訪問到的,因此也是公有的。

js中的protected

在函數的內部,咱們能夠經過this定義的方法訪問到一些類的私有屬性和方法,在實例化的時候就能夠初始化對象的一些屬性了。

new的實質

雖然不少人都已經瞭解了new的實質,那麼我仍是要再說一下new 的實質
var o = new Object()

  1. 新建一個對象o
  2. o. __proto__ = Object.prototype 將新建立的對象的__proto__屬性指向構造函數的prototype
  3. 將this指向新建立的對象
  4. 返回新對象,可是這裏須要看構造函數有沒有返回值,若是構造函數的返回值爲基本數據類型string,boolean,number,null,undefined,那麼就返回新對象,若是構造函數的返回值爲對象類型,那麼就返回這個對象類型

栗子~

var Book = function (id, name, price) {
        //private(在函數內部定義,函數外部訪問不到,實例化以後實例化的對象訪問不到)
        var num = 1;
        var id = id;
        function checkId() {
            console.log('private')
        }
        //protected(能夠訪問到函數內部的私有屬性和私有方法,在實例化以後就能夠對實例化的類進行初始化拿到函數的私有屬性)
        this.getName = function () {
            console.log(id)
        }
        this.getPrice = function () {
            console.log(price)
        }

        //public(實例化的以後,實例化的對象就能夠訪問到了~)
        this.name = name;
        this.copy = function () {
            console.log('this is public')
        }

    }

    //在Book的原型上添加的方法實例化以後能夠被實例化對象繼承
    Book.prototype.proFunction = function () {
        console.log('this is proFunction')
    }

    //在函數外部經過.語法建立的屬性和方法,只能經過該類訪問,實例化對象訪問不到
    Book.setTime = function () {
        console.log('this is new time')
    }
    var book1 = new Book('111','悲慘世界','$99')
    book1.getName();        // 111 getName是protected,能夠訪問到類的私有屬性,因此實例化以後也能夠訪問到函數的私有屬性
    book1.checkId();        //報錯book1.checkId is not a function
    console.log(book1.id)   // undefined id是在函數內部經過定義的,是私有屬性,因此實例化對象訪問不到
    console.log(book1.name) //name 是經過this建立的,因此在實例化的時候會在book1中複製一遍name屬性,因此能夠訪問到
    book1.copy()            //this is public
    book1.proFunction();    //this is proFunction
    Book.setTime();         //this is new time
    book1.setTime();        //報錯book1.setTime is not a function複製代碼

繼承

繼承:子類可使用父類的全部功能,而且對這些功能進行擴展。繼承的過程,就是從通常到特殊的過程。

其實繼承都是基於以上封裝方法的三個特性來實現的。

類式繼承

所謂的類式繼承就是使用的原型的方式,將方法添加在父類的原型上,而後子類的原型是父類的一個實例化對象。

//聲明父類
    var SuperClass = function () {
        var id = 1;
        this.name = ['javascript'];
        this.superValue = function () {
            console.log('superValue is true');
            console.log(id)
        }
    };

    //爲父類添加共有方法
    SuperClass.prototype.getSuperValue = function () {
        return this.superValue();
    };

    //聲明子類
    var SubClass = function () {
        this.subValue = function () {
            console.log('this is subValue ')
        }
    };

    //繼承父類
    SubClass.prototype = new SuperClass() ;

    //爲子類添加共有方法
    SubClass.prototype.getSubValue= function () {
        return this.subValue()
    };

    var sub = new SubClass();
    var sub2 =  new  SubClass();

    sub.getSuperValue();   //superValue is true
    sub.getSubValue();     //this is subValue

    console.log(sub.id);    //undefined
    console.log(sub.name);  //javascript

    sub.name.push('java');  //["javascript"]
    console.log(sub2.name)  //["javascript", "java"]複製代碼

其中最核心的一句代碼是SubClass.prototype = new SuperClass() ;
類的原型對象prototype對象的做用就是爲類的原型添加共有方法的,可是類不能直接訪問這些方法,只有將類實例化以後,新建立的對象複製了父類構造函數中的屬性和方法,並將原型__proto__ 指向了父類的原型對象。這樣子類就能夠訪問父類的publicprotected 的屬性和方法,同時,父類中的private 的屬性和方法不會被子類繼承。

敲黑板,如上述代碼的最後一段,使用類繼承的方法,若是父類的構造函數中有引用類型,就會在子類中被全部實例共用,所以一個子類的實例若是更改了這個引用類型,就會影響到其餘子類的實例。
提一個小問題~爲何一個子類的實例若是更改了這個引用類型,就會影響到其餘子類的實例呢,在javascript中,什麼是引用類型呢,引用類型和其餘的類型又有什麼區別呢?

構造函數繼承

正式由於有了上述的缺點,纔有了構造函數繼承,構造函數繼承的核心思想就是SuperClass.call(this,id),直接改變this的指向,使經過this建立的屬性和方法在子類中複製一份,由於是單獨複製的,因此各個實例化的子類互不影響。可是會形成內存浪費的問題

//構造函數繼承
    //聲明父類
    function SuperClass(id) {
        var name = 'javascript'
        this.books=['javascript','html','css'];
        this.id = id
    }

    //聲明父類原型方法
    SuperClass.prototype.showBooks = function () {
        console.log(this.books)
    }

    //聲明子類
    function SubClass(id) {
        SuperClass.call(this,id)
    }

    //建立第一個子類實例
    var subclass1 = new SubClass(10);
    var subclass2 = new SubClass(11);

    console.log(subclass1.books);
    console.log(subclass2.id);
    console.log(subclass1.name);   //undefined
    subclass2.showBooks();複製代碼

組合式繼承

咱們先來總結一下類繼承和構造函數繼承的優缺點

類繼承 構造函數繼承
核心思想 子類的原型是父類實例化的對象 SuperClass.call(this,id)
優勢 子類實例化對象的屬性和方法都指向父類的原型 每一個實例化的子類互不影響
缺點 子類之間可能會互相影響 內存浪費

因此組合式繼承就是汲取二者的優勢,即避免了內存浪費,又使得每一個實例化的子類互不影響。

//組合式繼承
    //聲明父類
    var SuperClass = function (name) {
        this.name = name;
        this.books=['javascript','html','css']
    };
    //聲明父類原型上的方法
    SuperClass.prototype.showBooks = function () {
        console.log(this.books)
    };

    //聲明子類
    var SubClass = function (name) {
        SuperClass.call(this, name)

    };

    //子類繼承父類(鏈式繼承)
    SubClass.prototype = new SuperClass();

    //實例化子類
    var subclass1 = new SubClass('java');
    var subclass2 = new SubClass('php');
    subclass2.showBooks();
    subclass1.books.push('ios');    //["javascript", "html", "css"]
    console.log(subclass1.books);  //["javascript", "html", "css", "ios"]
    console.log(subclass2.books);   //["javascript", "html", "css"]複製代碼

寄生組合繼承

那麼問題又來了~組合式繼承的方法當然好,可是會致使一個問題,父類的構造函數會被建立兩次(call()的時候一遍,new的時候又一遍),因此爲了解決這個問題,又出現了寄生組合繼承。
剛剛問題的關鍵是父類的構造函數在類繼承和構造函數繼承的組合形式中被建立了兩遍,可是在類繼承中咱們並不須要建立父類的構造函數,咱們只是要子類繼承父類的原型便可。因此說咱們先給父類的原型建立一個副本,而後修改子類constructor屬性,最後在設置子類的原型就能夠了~

//原型式繼承
    //原型式繼承其實就是類式繼承的封裝,實現的功能是返回一個實例,改實例的原型繼承了傳入的o對象
    function inheritObject(o) {
        //聲明一個過渡函數對象
        function F() {}
        //過渡對象的原型繼承父對象
        F.prototype = o;
        //返回一個過渡對象的實例,該實例的原型繼承了父對象
        return new F();
    }
    //寄生式繼承
    //寄生式繼承就是對原型繼承的第二次封裝,使得子類的原型等於父類的原型。而且在第二次封裝的過程當中對繼承的對象進行了擴展
    function inheritPrototype(subClass, superClass){
        //複製一份父類的原型保存在變量中,使得p的原型等於父類的原型
        var p = inheritObject(superClass.prototype);
        //修正由於重寫子類原型致使子類constructor屬性被修改
        p.constructor = subClass;
        //設置子類的原型
        subClass.prototype = p;
    }
    //定義父類
    var SuperClass = function (name) {
        this.name = name;
        this.books = ['javascript','html','css']
    };
    //定義父類原型方法
    SuperClass.prototype.getBooks = function () {
        console.log(this.books)
    };

    //定義子類
    var SubClass = function (name) {
        SuperClass.call(this,name)
    }

    inheritPrototype(SubClass,SuperClass);

    var subclass1 = new SubClass('php')複製代碼
相關文章
相關標籤/搜索