JS的面向對象


前言

前一陣面試,過程中發現問到一些很基礎的問題時候,自己並不能很流暢的回答出來。或者遇到一些基礎知識的應用,由於對這些點理解的不是很深入,拿着筆居然什么都寫不出來,於是有了回顧一下這些基礎知識的想法。

首先就是面試中經常會問到的,JS是怎么實現繼承的,其實問到繼承,面試官想問的可能還是你對JS面向對象的理解吧。

這一部分的主要參考資料:《JavaScript高級程序設計》、《JavaScript設計模式》
如果有什么錯誤的地方,也希望看到這篇文章的小伙伴給我指出來,謝謝 _

一、對象

1.1創建對象

Javascript是一種基於對象(object-based)的語言,你遇到的所有東西幾乎都是對象。
一個簡單的對象創建:

    var People = {
        name : "eavan",
        age : 24,
        getName : function(){
            alert(this.name);        //eavan
        }
    }

使用的時候就可以用People.name,獲取People這個對象的name屬性,或者是People.getName()來得到People的name值。
另一種對象創建方式:

    var People = new Object();
    People.name = "eavan";
    People.age = 24;
    People.getName = function(){
        alert(this.name);
    }

這里用到了new,就順便提一下在使用new的時候發生了什么,其實在使用new的時候,大致可以認為做了這三件事,看下面的代碼:

    var People  = {};                      //我們創建了一個空對象People
    People.__proto__ = Object.prototype;   //我們將這個空對象的__proto__成員指向了Object函數對象prototype成員對象
    Object.call(People);         //我們將Object函數對象的this指針替換成People,然后再調用Object函數

1.2封裝

簡單來說就是對一些屬性的隱藏域暴露,比如私有屬性、私有方法、共有屬性、共有方法、保護方法等等。而js也能實現私有屬性、私有方法、共有屬性、共有方法等等這些特性。

像java這樣的面向對象的編程語言一般會有一個類的概念,從而實現封裝。而javascript中沒有類的概念,JS中實現封裝主要還是靠函數。

首先聲明一個函數保存在一個變量里面。然后在這個函數(類)的內部通過對this變量添加屬性或者方法來實現對類添加屬相或者方法。

    var Person = function(){
        var name = "eavan";             //私有屬性
        function checkName(){};         //私有方法
    
        this.myName = "gaof";            //對象共有屬性
        this.myFriends = ["aa","bb","cc"];
        this.copy = function(){}         //對象共有方法
    
        this.getName = function(){       //構造器方法
            return name;
        };            
    }

純構造函數封裝數據的問題是:對像this.copy = function(){}這種方法的創建,其實在執行的時候大可不必綁定到特定的對象上去,將其定義到全局變量上也是一樣的,而且其過程相當於實例化了一個Function,也大可不必實例化這么多其實干同一件事的方法。而這個小問題的解決可以用原型模式來解決。

1.3理解原型

在每創建一個函數的時候,都會生成一個prototype屬性,這個屬性指向函數的原型對象。而其是用來包含特定類型的所有實例共享的屬性和方法。所以,直接添加在原型中的實例和方法,就會被所有實例所共享。

同樣還是上面的Person的例子,我們可以為其原型添加新的屬性和方法。

    Person.isChinese = true;                          //類的靜態共有屬性(對象不能訪問)
    Person.prototype.sex = "man" ;            //類的共有屬性
    Person.prototype.frends = ["gao","li","du"];
    Person.prototype.isBoy = function(){};    //類的共有方法

原型封裝數據的問題:對綁定在prototype上的引用類型的變量,由於被所有對象所共有,其中某一個對象對該數據進行修改,當別的對象訪問該數據的時候,所訪問到的值就是被修改后的。
比如如下代碼:

    var person1 = new Person();
    person1.frends.push("dd");
    console.log(person1.frends);    //["gao", "li", "du", "dd"]
    var person2 = new Person();
    person2.frends.push("ee");
    console.log(person2.frends);     //["gao", "li", "du", "dd", "ee"]

原本希望對person1和person2的friends屬性分別添加新的內容,結果二者的friends屬性居然是“公用”的!

綜上,最常見的方式應該是組合使用構造函數和原型模式,構造函數用於定義實例屬性,原型模式用於定義方法和共享的屬性。

每個類有三部分構成:第一部分是構造函數內,供實例對象化復制用。第二部分是構造函數外,直接通過點語法添加,供類使用,實例化對象訪問不到。第三部分是類的原型中,實例化對象可以通過其原型鏈間接訪問到,也是為所有實例化對象所共用。

在說到對象實例的屬性的時候,我們有一個問題,就是在訪問一個屬性的時候,這個屬性是屬於實例,還是屬於這個實例的原型的呢?

比如還是上面的例子,我們為person2實例增加一個sex屬性,這時候訪問person2的sex屬性時,得到的是我們增加的值。說明為對象實例添加一個屬性的時候,這個屬性就會屏蔽原型對象中保存的同名屬性。

       person2.sex = "woman";
        console.log(person1.sex);                //man
        console.log(person2.sex);                //woman

這個時候我們可以使用hasOwnProperty()方法來檢測一個屬性是存在於實例中,還是存在於原型中。如果實例中有這個屬性,hasOwnProperty()會返回true,而hasOwnProperty()並不會感知到原型中的屬性。所以可以用這個方法檢測屬性到底是存在於實例中還是原型中。

    console.log(person1.hasOwnProperty("sex"));        //原型中的屬性,返回false
    console.log(person2.hasOwnProperty("sex"));        //實例中的屬性,返回true

二、繼承

ECMAScript中描述了原型鏈的概念,並將原型鏈作為實現繼承的主要方法。其基本思想是利用原型讓一個引用類型繼承另一個引用類型的屬性和方法。

2.1 原型鏈繼承

如下代碼:

    function Super(){
        this.val = true;
        this.arr = ["a"];
    }
    function Sub(){
            //...
    }
    Sub.prototype = new Super();
    
    var sub = new Sub();
    console.log(sub.val)        //true

以上代碼定義了Super和Sub兩個類型,繼承的核心就一句話:Sub.prototype = new Super() 將父類的一個實例賦給子類的原型。這樣子類就能夠使用父類實例所擁有的方法和父類原型中的方法。

這種情況要想給子類添加自己的方法或者是覆蓋父類中某個方法的時候,一定要在放在替換原型語句后面。否則寫在原型上的方法都會丟失。

而且在給子類添加新方法的時候,不能使用字面量的方式添加新方法,這樣會導致繼承無效。
如:

    Sub.prototype = new Super();
    Sub.prototype = {                        //錯誤的方式
        getVal : function(){
            //...
        }
    }

以上代碼剛剛把Super的實例賦給原型,緊接着又將原信替換成一個對象字面量,導致現在原型包含的是一個Object的實例,並非Super的實例,因此原型鏈被切斷了,Sub和Super已經沒有關系了。

原型鏈的問題:
最主要的問題有兩個:一是由於引用類型的原型屬性會被所有實例所共享,所以通過原型鏈繼承時,原型變成了另一個類型的實例,原先的實例屬性也就變成了現在的原型屬性,如下代碼:

    function Super(){
        this.friends = ["peng","gao"];
    }
    function Sub(){
            //...
    }
    Sub.prototype = new Super();
    var sub1 = new Sub();
    var sub2 = new Sub();
    sub1.friends.push("du");
    console.log(sub2.friends);            //["peng", "gao", "du"]

這個例子說明的就是上面的問題,子類的所有實例共享了父類中的引用類型屬性。

原型鏈繼承的另一個問題是在創建子類行的實例的時候,沒法向父類的構造函數傳遞參數。

2.2 構造函數繼承

具體實現:

    function Super(){
        this.val = true;
        this.arr = ["a"];
    }
    function Sub(){
           Super.call(this);
    }
    var sub = new Sub();
    console.log(sub.val)        //true

這種模式這是解決了原型鏈繼承中出現的兩個問題,它可以傳遞參數,也沒有了子類共享父類引用屬性的問題。
但這種模式也有他的問題,那就是在父類原型中定義的方法,其實是對子類不可見的。

2.3組合繼承

既然上述的兩種方式各有各自的局限性,將它倆整合到一起是不是會好一點呢,於是就有了組合繼承。

    function Super(){
        this.val = true;
        this.arr = ["a"];
    }
    function Sub(){
           Super.call(this);                    //{2}
    }
    Sub.prototype = new Super();                //{1}
    Sub.prototype.constructor = Sub;            //{3}
    var sub = new Sub();
    console.log(sub.val)        //true

組合繼承還有一個要注意的地方:
在代碼{3}處,將子類原型的constructor屬性指向子類的構造函數。因為如果不這么做,子類的原型是父類的一個實例,所以子類原型的constructor屬性就丟失了,他會順着原型鏈繼續往上找,於是就找到了父類的constructor所以它指向的其實是父類。

這種繼承方式是使用最多的一種方式。
這種繼承方式解決了上兩種方式的缺點,不會出現共享引用類型的問題,同時父類原型中的方法也被繼承了下來。

如果要說起有什么缺點我們發現,在執行代碼{1}時,Sub.prototype會得到父類型的val和arr兩個屬性。他們是Super的實例屬性,只不過現在在Sub的原型上。而代碼{2}處,在創建Sub實例的時候,調用Super的構造函數,又會在新的對象上創建屬性val和arr,於是,這兩個屬性就屏蔽了原型中兩個同名屬性。

2.4寄生組合式繼承

對於上面的問題,我們也有解決辦法,不是在子類原型中多了一份父類的屬性和方法么,那我原型中就只要父類原型中的屬性和方法,這里我們引入了一個方法:

    function inheritObject(obj){
        var F = function(){};
        F.prototype = obj;
        return new F();
    }

這個方法創建了一個對象臨時性的構造函數,然后將傳入的對象作為這個構造函數的原型,最后返回這個臨時類型的一個新實例。

我們可以設想,如果用這個方法拷貝一份父類的原型屬性給子類,是不是就避免了上面提到的子類原型中多了一份父類構造函數內的屬性。看如下代碼:

    function Super(){
        this.val = 1;
        this.arr = [1];
    }
    Super.prototype.fun1 = function(){};
    Super.prototype.fun2 = function(){};
    
    function Sub(){
        Super.call(this);
    }
    var p = inheritObject(Super.prototype);         //{1}
    p.constructor = Sub;                            //{2}
    Sub.prototype = p;                              //{3}
     
    var sub = new Sub();

基本思路就是:不必為了指定子類型的原型而調用父類的夠着函數,我們需要的無非就是父類原型的一個副本而已。本質上就是復制出父類的一個副本,然后再將結果指定給子類型的原型。

三、多態

所謂多態,就是同一個方法的多種調用方式,在javascript中,通過arguments對象對傳入的參數做判斷就可以實現多種調用方式。

例子:

    function Add(){
        function zero(){
            return 10;
        }
        function one(num){
            return 10 + num;
        }
    function    two(num1, num2){
        return num1 + num2;
    }
    this.add = function(){
        var arg = arguments,
                len = arg.length;
        switch (len){
            case 0:
                return zero();
            case 1:
                return one(arg[0]);
            case 2:
                return two(arg[0], arg[1]);
            }
        }
    }
    var A = new Add();
    console.log(A.add());                //10
    console.log(A.add(5));              //15
    console.log(A.add(6, 7));          //13


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM