學會了面向對象,還怕沒有對象?

面向對象是一種編程思想,咱們經過類(構造函數)和對象實現的面向對象編程,知足下述三個特定:封裝、繼承和多態。

封裝

封裝建立對象的函數

封裝即把實現一個功能的代碼封裝到一個函數中,之後實現這個功能,只須要執行該函數便可。實現低耦合,高內聚。javascript

如今咱們把屬性和方法封裝成一個對象:java

//建立一個對象
        var person = new Object();
        //添加屬性和方法
        person.name = "鋼鐵俠";
        person.sex = "男";
        person.showName = function(){
            alert("個人名字叫" + this.name);//個人名字叫鋼鐵俠
        }
        person.showSex = function(){
            alert("個人性別是" + this.sex);//個人性別是男
        }
        person.showName();
        person.showSex();

若是咱們想建立一個不一樣性別不一樣姓名的對象,就須要再寫一遍上述代碼:程序員

//建立一個對象
        var person2 = new Object();
        //添加屬性和方法
        person2.name = "猩紅女巫";
        person2.sex = "女";
        person2.showName = function(){
            alert("個人名字叫" + this.name);//個人名字叫猩紅女巫
        }
        person2.showSex = function(){
            alert("個人性別是" + this.sex);//個人性別是女
        }
        person2.showName();
        person2.showSex();

若是咱們想要建立多個對象的話,寫起來就很是麻煩,因此要去封裝建立對象的函數解決代碼重複的問題。編程

function createPerson(name, sex){
            var person = new Object();
            person.name = name;
            person.sex = sex;
            person.showName = function(){
                alert("我叫" + this.name);
            }
            person.showSex = function(){
                alert("我是" + this.sex + "的");
            }
            return person;
        }

而後生成實例對象,就等因而在調用函數:設計模式

var p1 = createPerson("鋼鐵俠", "男");
        p1.showName();//我叫鋼鐵俠
        p1.showSex();//我是男的

        var p2 = createPerson("猩紅女巫", "女");
        p2.showName();//我叫猩紅女巫
        p2.showSex();//我是女的

上述過程能夠類比爲開工廠生產酸奶:第一步:須要原料;第二步:加工酸奶;第三步:出廠售賣;咱們經過var聲明空對象的這一步就至關於第一步原料,添加屬性和函數就至關於第二步加工,經過return返回對象就至關於第三步出廠。這種符合上述一、二、3步驟的函數叫作工廠函數,這種設計函數的思路,叫作工廠設計模式。數組

經過new調用函數

官方函數建立對象的方法是經過new的方法,當咱們不使用new建立對象的時候,函數內部的this會指向窗口。app

function show(){
            alert(this);//[object Window]
        }
        show();

因此當咱們在函數內部給this.name賦值爲xxxx時,能夠經過window.name輸出xxxx,由於若是這個函數沒有主人的話它的主人就是window對象。iphone

function show(){
            alert(this);//[object Window]
            this.name = "xxxx";
        }
        show();
        alert(window.name);//xxxx

可是若是這個函數經過new運算符去調用,那麼這個函數中的this,就會指向新建立的對象。函數

function show(){
            alert(this);//[object Object]
        }
        var obj = new show();

當咱們經過new運算符去調用函數的時候,它首部和尾部會自動的生成如下兩步:一、原料操做:強制改變this指向this = new Object(); 三、出廠操做:將this返回return this;性能

function show(){
            // this = new Object();
            alert(this);//[object Object]
            this.name = "xxxx";
            // return this;
        }
        var obj = new show();
        alert(obj.name);//xxxx

因此如今咱們改造一下以前建立的函數,調用的時候所有都經過new去調用,而且將函數中的person改爲this。

function createPerson(name, sex){
            this.name = name;
            this.sex = sex;
            this.showName = function(){
                alert("我叫" + this.name);
            }
            this.showSex = function(){
                alert("我是" + this.sex + "的");
            }
        }
        var p1 = new createPerson("鋼鐵俠", "男");
        p1.showName();//我叫鋼鐵俠
        p1.showSex();//我是男的

        var p2 = new createPerson("猩紅女巫", "女");
        p2.showName();//我叫猩紅女巫
        p2.showSex();//我是女的

構造函數

咱們把這種能夠建立對象的函數,叫作構造函數。(功能就是用來構造對象)

function Person(name, sex){
            this.name = name;
            this.sex = sex;
        }

爲了和別的函數,進行區分,咱們把構造函數首字母大寫。官方的構造函數:Array、Object、Date。

咱們經過typeof能夠看到官方經過new建立的Object、Array、Date本質上都是function函數。並且全部被該函數,建立的對象,對象的方法都是一套,arr1.push === arr2.push返回值是true。

var arr = new Array();
        var obj = new Object();
        var d = new Date();
        alert(typeof Array);//類型 function函數

可是經過調用函數生成的對象方法,彼此之間沒有聯繫,不能反映出它們是同一個原型對象的實例。alert(p1.showName === p2.showName);返回值爲false。

var arr1 = new Array(10, 20, 30);
        var arr2 = new Array(40, 50, 60);
        alert(arr1.push === arr2.push); //true

咱們聲明兩個數組

var arr1 = [10, 20, 30, 40, 50];
        var arr2 = [60, 70, 80, 90, 100];

給數組添加求和的函數

arr1.sum = function(){
            var res = 0;
            for(var i = 0; i < this.length; i++){
                res += this[i];
            }
            return res;
        }

調用arr1.sum能夠輸出arr1的和爲150,可是調用arr2.sum會系統報錯,提示arr1.sum不是一個函數。由於arr1和arr2是單獨的兩個對象,給arr1添加一個方法,arr2並不會擁有這個方法。因此咱們以前經過new調用函數生成對象後,他們的方法是相互獨立的。

alert(arr1.sum == arr2.sum);//false

每個實例對象,都有本身的屬性和方法的副本。這不只沒法作到數據共享,也是極大的資源浪費。

原型prototype

prototype對象的引入:全部實例對象須要共享的屬性和方法,都放在這個對象中,那些不須要共享的屬性和方法,就放在構造函數中。以此來模擬類。

因此想讓arr2也擁有求和函數就須要再從新寫一個arr2.sum,這樣就會形成浪費,咱們想讓對象共用一個方法,這時候就須要引入原型prototype。在JS中一切皆對象,函數也是對象。 每個被建立的函數,都有一個官方內置的屬性,叫作prototype(原型)對象 ,咱們輸出一下show.protoype,獲得結果[object Object]

function show(){
        }
        alert(show.protoype);//[object Object]

全部實例對象須要共享的屬性和方法,都放在這個對象裏面;那些不須要共享的屬性和方法,就放在構造函數裏面。

若是,咱們想要讓該函數建立出來的對象,公用一套函數,那麼咱們應該將這套函數,添加該函數的prototype原型。 因此咱們若是想讓兩個數組都擁有求和的方法,就須要將這個方法添加在Array的原型上。

Array.prototype.sum = function(){
            var res = 0;
            for(var i = 0; i < this.length; i++){
                res += this[i];
            }
            return res;
        }

如今arr1和arr2均可以使用這個函數,而且arr1.sum == arr2.sum,他們使用的這個函數都是原型上的同一個方法。

alert(arr1.sum());//150
        alert(arr2.sum());//400
        alert(arr1.sum == arr2.sum);//true

咱們能夠經過混合法,讓用戶自定義構造函數,封裝一個能夠建立對象的函數,而且調用的是同一個方法。

function Person(name, sex){
            //this = new Object();
            this.name = name;
            this.sex = sex;
            //return this;
        }

        //函數,必須,添加在這個函數的prototype原型
        Person.prototype.showName = function(){
            alert("我叫" + this.name);
        }
        Person.prototype.showSex = function(){
            alert("我是" + this.sex + "的");
        }
        var p1 = new Person("鋼鐵俠", '男');
        var p2 = new Person("猩紅女巫", "女");

        p1.showName();//我叫鋼鐵俠
        p1.showSex();//我是男的
        p2.showName();//我叫猩紅女巫
        p2.showSex();//我是女的

        alert(p1.showName == p2.showName); //true

面向對象編程案例

如今咱們要測試100輛不一樣品牌的汽車,記錄他們在道路上行駛的性能指數。

建立一個能夠構造各式各樣車的構造函數

function Car(type, name, speed){
            this.type = type;
            this.name = name;
            this.speed = speed;
        }

在Car的原型上添加功能:讓車跑在路上,計算時速。

Car.prototype.run = function(road){
            alert(`一輛${this.type}品牌的${this.name}系列,時速爲${this.speed}km/h的車,跑在長度爲${road.length}km的${road.name},最終的成績是${road.length / this.speed}小時`);
        }

建立一個能夠構造各式各樣馬路的構造函數

function Road(name, length){
            this.name = name;
            this.length = length;
        }

添加第一個測試用例car1:

var kuahaidaqiao = new Road("跨海大橋", 1000);
        var car1 = new Car("大衆", "PASSAT", 100);
        car1.run(kuahaidaqiao);//一輛大衆品牌的PASSAT系列,時速爲100km/h的車,跑在長度爲1000km的跨海大橋,最終的成績是10小時

這就是面向對象編程,只寫一遍代碼,以後再要建立對象,只須要調用封裝好的函數。類和對象是面向對象編程的兩個語法,是面向對象實現的基礎,可是在JS中沒有類的概念,一切皆對象,全部的實例都是由Object構造函數構造出來的。因此當咱們有類的需求的時候,咱們自創了一個構造函數,來替代類的存在,因此構造函數的本質就是類。

Prototype模式的驗證方法

爲了配合prototype屬性,Javascript定義了一些關鍵字,幫助咱們使用它。

  1. instanceof

    格式:對象 instanceof 構造函數

    功能:判斷這個對象是不是後面這個構造函數構造的。若是是,返回true;不然,返回false。

    alert(car1 instanceof Car);//true
            alert(car1 instanceof Road);//false
            alert(car1 instanceof Object);//true
  2. isPrototypeOf()

    格式:構造函數.prototype.isPrototypeOf(對象)

    功能:判斷某個proptotype對象是否擁有某個實例,若是是,返回true;不然,返回false。

    alert(Car.prototype.isPrototypeOf(car1)); //true
            alert(Road.prototype.isPrototypeOf(kuahaidaqiao)); //true
  3. hasOwnProperty()

    格式:對象.hasOwnProperty("屬性")

    功能:實例對象一旦建立,將自動引用prototype對象的屬性和方法。也就是說,實例對象的屬性和方法,分紅兩種,一種是本地的,另外一種是引用的。每一個實例對象都有一個hasOwnProperty()方法,用來判斷某一個屬性究竟是本地屬性,仍是繼承自prototype對象的屬性。

    Car.prototype.color = "white";
            alert(car1.hasOwnProperty("name")) //true
            alert(car1.hasOwnProperty("color")) //false
  4. in運算符

    格式:"屬性"in對象

    功能:in運算符能夠用來判斷,某個實例是否含有某個屬性,不論是不是本地屬性。

    alert("type" in car1); // true
            alert("length" in car1); // false

    in運算符還能夠用來遍歷某個對象的全部屬性,包括添加在它身上的方法。

    for(var prop in car1) { alert("car1["+prop+"]="+car1[prop]); }

ECMA6語法糖 class類

ES6提供了簡單的定義類的語法糖class

構造函數的寫法:

function Iphone(size, color){
            this.size = size;
            this.color = color;
        }

        Iphone.prototype.show = function(){
            alert(`您選擇了一部${this.color}顏色的,內存大小是${this.size}GB的手機`);
        }

        var iphone1 = new Iphone(64, "玫瑰金");
        iphone1.show();//您選擇了一部玫瑰金顏色的,內存大小是64GB的手機

類的寫法:

class Iphone{
            constructor(size, color){
                this.size = size;
                this.color = color;
            }
            show(){
                alert(`您選擇了一部${this.color}顏色的,內存大小是${this.size}GB的手機`);
            }
        }

        var iphone2 = new Iphone(256, "黑色");
        iphone2.show();//您選擇了一部黑色顏色的,內存大小是256GB的手機

繼承

因爲全部的實例對象共享同一個prototype對象,那麼從外界看起來,prototype對象就好像是實例對象的原型,而實例對象則好像"繼承"了prototype對象同樣。這就是Javascript繼承機制的設計思想。

繼承一方面是爲了實現面向對象,另外一方面爲了幫助你們更高效的編寫代碼,可讓一個構造函數繼承另外一個構造函數中的屬性和方法。

首先咱們定義一個People類

function Person(name, sex){
            this.name = name;
            this.sex = sex;
        }
        Person.prototype.showName = function(){
            alert("我叫" + this.name);
        }
        Person.prototype.showSex = function(){
            alert("我是" + this.sex);
        }

如今咱們要在Person類的基礎上建立一個Worker類,擁有Person類的所有屬性和方法,同時添加它本身的屬性job,咱們能夠經過如下幾種方法實現繼承。

function Person(name, sex){
            this.name = name;
            this.sex = sex;
        }
        Person.prototype.showName = function(){
            alert("我叫" + this.name);
        }
        Person.prototype.showSex = function(){
            alert("我是" + this.sex);
        }

call/apply

第一種方法也是最簡單的方法,使用call或apply方法,將父對象的構造函數綁定在子對象上,這種繼承Person的方式叫作構造函數的假裝,由於Person對象本來應該只爲new的Person對象服務,可是它如今還能夠被Worker對象使用。

function Worker(name, sex, job){
            //繼承Person的屬性
            Person.call(this, name, sex); 
            this.job = job;//添加本身的屬性
        }
        var w1 = new Worker("小明", "男", "程序員");

call和apply的區別在於參數形式不一樣,call(obj, pra, pra)後面是單個參數。apply(obj, [args])後面是數組,做用都是強制改變this的指向。

function Worker(name, sex, job){
            //繼承Person的屬性
            //構造函數的假裝
            Person.apply(this, arguments);
            this.job = job;
        }

如今Worker想繼承Person上的方法,Person方法都放在prototype中,prototype本質是對象,存儲的是引用數據類型,因此咱們不能直接將父級的方法賦值給子,繼承只能是單向的,子繼承父,可是不能影響父。

每個構造函數身上都會有一個prototype原型,咱們能夠將Person身上的prototype遍歷,在遍歷的過程當中將Person身上的函數取出,放入Worker的prototype中,這樣它們就不會互相影響了。

for(var i in Person.prototype){
            Worker.prototype[i] = Person.prototype[i];
        }

同時經過prototype來拓展本身的方法

Worker.prototype.showJob = function(){
            alert("我是幹" + this.job + "工做的");
        }

直接調用父級的構造函數繼承

第二種方法更常見,使用prototype屬性。若是"Worker"的prototype對象,指向一個Person的實例,那麼全部"Worker"的實例,就能繼承Person了。

Worker.prototype = new Person();

每個prototype對象都有一個constructor屬性,指向它的構造函數。若是沒有"Worker.prototype = new Person();"這一行,Worker.prototype.constructor是指向Worker的;加了這一行之後,Worker.prototype.constructor指向Person。

alert(Worker.prototype.constructor == Person) //true

更重要的是,每個實例也有一個constructor屬性,默認調用prototype對象的constructor屬性。所以,在運行"Worker.prototype = new Person();"這一行以後,w1.constructor也指向Person。

var w1 = new Worker("小明", "男", "程序員");
        alert(w1.constructor == Worker.prototype.constructor) //true
        alert(w1.constructor == Person.prototype.constructor) //true
        alert(w1.constructor == Person) //true

這顯然會致使繼承鏈的紊亂(w1明明是用構造函數Worker生成的),所以咱們必須手動糾正,將Worker.prototype對象的constructor值改成Worker。這是很重要的一點,編程時務必要遵照。

Worker.prototype.constructor = Worker
        alert(w1.constructor == Worker.prototype.constructor) //true
        alert(w1.constructor == Person.prototype.constructor) //false
        alert(w1.constructor == Person) //false

Object.create()拷貝繼承

上面是採用prototype對象,實現繼承。咱們也能夠換一種思路,純粹採用"拷貝"方法實現繼承。簡單說,若是把父對象的全部屬性和方法,拷貝進子對象。Object.create()相似於數組中的concat方法,能夠建立一個新對象。這樣就能夠將父對象的prototype對象中的屬性,一一拷貝給Child對象的prototype對象。

//拷貝原有對象,建立新對象
        Worker.prototype = Object.create(Person.prototype);

多態

建立一個父親的構造函數

function Father(name, sex, age){
        this.name = name;
        this.sex = sex;
        this.age = age;
    }
    Father.prototype.sing = function(){
        alert(`父親的民歌唱得很是好`);
    }

再建立一個兒子構造函數,繼承父親的屬性和方法

function Son(name, sex, age, degree){
        //構造函數的假裝
        Father.call(this,name, sex, age);

        //拓展本身的屬性
        this.degree = degree;

    }

    //繼承方法
    //原型鏈
    for(var i in Father.prototype[i]){
        Son.prototype[i] = Father.prototype[i];
    }

經過new建立一個兒子對象,執行son1.sing()調用父親原型上的方法。

var son1 = new Son("小明", "男", 20, "本科");
    son1.sing();//父親的民歌唱得很是好

如今咱們重寫父級繼承的函數

Son.prototype.sing = function(){
        alert(`唱搖滾`);
    }

再次調用son1.sing(),執行的是son1本身添加的sing方法。

var son1 = new Son("小明", "男", 20, "本科");
    son1.sing();/唱搖滾

對於父親自身的sing方法沒有影響,在子級重寫的方法只在子級生效。

var f2 = new Father("大明", "男", 40);
    f2.sing();//父親的民歌唱得很是好

如今咱們再來看繼承和多態的概念,其實它們都是繼承某一部分,是同一件事情的兩個側重。繼承側重於從父級繼承到屬性和方法,而多態側重於本身拓展的屬性和方法,也就是重寫的內容。簡單來講凡是跟父級同樣的部分叫繼承,不同的部分叫多態。

注意:雖然Son繼承了Father的屬性和方法,可是經過Son構造函數new出來的對象不屬於Father。

alert(son1 instanceof Son) //true
    alert(f2 instanceof Father) //true
    alert(son1 instanceof Father) //false
    alert(son1 instanceof Object) //true

一樣的案例使用ECMA6 class類的方法來實現繼承和多態,對比原來的方法更簡單,更形象。

class Father{
            constructor(name, sex, age){
                this.name = name;
                this.sex = sex;
                this.age = age;
            }

            //聲明方法
            sing(){
                alert("會唱民歌");
            }
        }

        /*
            繼承Father建立一個子類Son
            經過extends繼承
        */
    
        class Son extends Father{
            constructor(name, sex, age, degree){
                super(name, sex, age);
            }
            sing(){
                alert("唱搖滾");
            }
        }

        var son1 = new Son("小明", "男", 30, "本科");
        alert(son1.name);//小明
        son1.sing();//唱搖滾

        var f1 = new Father("大明", "男", 40);
        alert(f1.name);//大明
        f1.sing();//會唱民歌

原型鏈

在javascript中,每一個對象都有一個指向它的 原型(prototype)對象的內部連接。每一個原型對象又有本身的原型,直到某個對象的原型爲null爲止,組成這條鏈的最後一環。

在構造函數有一個prototype的屬性,經過構造函數構造出來的對象有_ _proto _ _屬性,指向構造該對象的構造函數的原型,它還有一個名字叫魔術變量。

也就是說經過Son構造函數構造出來的對象son1的_ _proto _ _徹底等於Son的原型prototype。

alert(son1.__proto__ === Son.prototype);//true
alert(f2.__proto__ === Father.prototype);//true

這也就解釋了爲何經過同一個構造函數構造出來的對象,使用的都是同一套函數,由於經過該構造函數構造出來全部對象的_ _proto _ _就指向該構造函數的原型。

相關文章
相關標籤/搜索