深入理解JavaScript之模擬“類”

  JavaScript並不是一門面向對象的編程語言。但是在無數人的努力之下,JavaScript漸漸的開始有了面向對象的特性(通過我們自定義的對象)。

  在JavaScript中從ES6開始提供了"class"關鍵字等等的與類有關的語法,但是,在JavaScript機制卻一直在阻止你使用近似類的語法。

  首先在看本章節之前要清楚的瞭解面向對象的四大特性,https://blog.csdn.net/qq_41889956/article/details/83999259

  1   混入

  顯示混入跟面向對象中的繼承差不多,我們可以這樣來理解面向對象中的繼承“繼承是子類對父類的複製”。但是在JavaScript中只有“對象”沒有類,那麼我們如何讓“對象”實現這一功能呢?

  其實按照之前繼承的理解,“繼承是子類對父類的複製”,JavaScript開發者也想出了一個用於模擬類複製行爲的方法,此方法叫做“混入”

混入分成兩種,一種是顯式,一種是隱式。

1.1  顯式混入

  由於JavaScript不會主動實現兩個對象之間的複製,因此需要我們手動實現複製功能。這個功能在許多庫和框架中被稱爲"extend(...)"

看下面的代碼

  //mixin只能主動選擇複製得對象
        function mixin(sourceObj,targetObj) {    //傳入源對象,目標對象
            for(var key in sourceObj){     //利用for in  循環遍歷出sourceObj中可枚舉得屬性
                if(!(key in targetObj)){        //當存在屬性不屬於目標類時,發生複製
                    targetObj[key]=sourceObj[key];
                }
            }
            return targetObj;
        }
        var Vehicle={
            engines:1,          //引擎
            ignition: function () {
                console.log("啓動我的引擎");
            },
            drive: function () {
                this.ignition();
                console.log("開始前進");
            }
        };
        var Car=mixin(Vehicle,{
            wheels:4,
            drive:function () {
                Vehicle.drive.call(this);        //顯式綁定該目標對象與原對象的this,derive()立即發生
                console.log("開始啓動"+this.wheels+"車輪")
            }
        });
        console.log(Car.engines);   //1
        console.log(Car.wheels);    //4

        Car.drive();
        //啓動我的引擎
        //開始前進
         //開始啓動4車輪




  Car對象就存在了"Vehicle"屬性和函數的副本了,記得我們前面在《深入JavaScript之對象》一章講過關於對象的複製,最簡單直接的方法就是使用"Object.assign(....)",這裏的函數複製只是複製引用。同樣的在我們顯式注入中同樣是複製函數的引用。相反,屬性"engines"和"wheels"則是實實在在的複製。

 Car已經有了drive屬性,所以這個屬性引用並沒有被"mixin"重寫,從而保留了Car中定義的同名屬性,實現了“子類”對“父類”屬性的重寫(在if(...)循環中判斷兩者有沒有屬性名相同的對象,有的話則忽略,不復制)。

  

①多態

  注意這裏存在的"Vehicle.drive.call(this)"---------這是多態中的一個顯式多態,在此例中,"Vehicle"中存在"drive(...)","Car"中也存在"drive(...)",爲了指明調用對象,我們必須使用絕對(不是相對),引用。我們通過名稱顯式指定"Vehicle"對象並調用它的"drive(...)"。

不然的話,就會出現,當我們想要調用"Vehicle"中的"drive(...)"時,會調用"Car"中的"drive(...)"。

假如,"Vehicle"中的不是"drive(...)"而是"pilot(....)"兩者時同樣的功能和語句,那麼在"Car"中就不需要顯式絕對引用"drive(...)"了,而是可以相對引用"pilot(...)"----------------這就是相對多態。

 

②混合複製

  在我們之前提到過的"mixin(...)"中,讓我們來回想一下,這是怎麼做的?

 function mixin(sourceObj,targetObj) {    //傳入源對象,目標對象
            for(var key in sourceObj){     //利用for in  循環遍歷出sourceObj中可枚舉得屬性
                if(!(key in targetObj)){        //當存在屬性不屬於目標類時,發生複製
                    targetObj[key]=sourceObj[key];
                }
            }
            return targetObj;
        }

  "for....in"會遍歷目標對象"sourceObj",將源對象的可枚舉屬性一一羅列出來,接着使用"if(...)"判斷目標對象(targetObj)中是否存在同名屬性,否的話,複製。最後輸出目標對象。由於我們是在目標對象初始化以後才進行復制的,所以要注意不要覆蓋目標對象的原有屬性。

  這時候問題來了,那麼,當我們先複製再對目標對象進行初始化呢?這樣是否就繞過了"if(....)"檢查,從而提升代碼的執行效率呢?

多說無益,我們來看看代碼

//mixin只能主動選擇複製得對象
        function mixin(sourceObj,targetObj) {    //傳入源對象,目標對象
            for(var key in sourceObj){     //利用for in  循環遍歷出sourceObj中可枚舉得屬性
                    targetObj[key]=sourceObj[key];
            }
            return targetObj;
        }
        var Vehicle={
            engines:1,          //引擎
            ignition: function () {
                console.log("啓動我的引擎");
            },
            drive: function () {
                this.ignition();
                console.log("開始前進");
            }
        };
           //先複製
        var Car=mixin(Vehicle,{});             //創建一個新的空對象,將原對象傳進去

        //再初始化對象
        mixin( {wheels:4,     //利用mixin(...)將對象的初始值傳入
            drive:function () {
            Vehicle.drive.call(this);        //強制綁定該目標對象與原對象的this
            console.log("開始啓動"+this.wheels+"車輪")}
            },Car);


      console.log(Car.engines);   //1
        console.log(Car.wheels);    //4

        Car.drive();
        //啓動我的引擎
        //開始前進
         //開始啓動4車輪

  從結果可以看出來,兩者運行結果都一樣,只是複製的先後順序不同。

  注意這裏,在你對目標對象初始化時,千萬不要用普通的賦值操作"Car={wheels:4......}"而要用"mixin(...)"賦值,否則會出現"TypeError"

  其實“先複製再初始化”並不好用而且效率更低,不如“先初始化在複製”。

  “混入”這一個名稱來源於這個過程的另一個解釋:Car中混入了"Vehicle"的內容,就好比你在冰激淋上塗了巧克力屑一樣。

  複製操作完成以後,"Car"和"Vehicle"就分離了,我們往"Car"中添加屬性不會影響到"Vehicle",反過來意識一樣(這裏其實這兩者之間時會有點微妙聯繫的,比如引用同一個對象)。

  由於複製時的函數,爲函數引用,所以兩者本質上說都是引用同一種函數而已並不能完全模擬面向對象屬性中的複製。當這個共享函數發生改變,例如增添了什麼屬性,那麼源對象和目標對象都會受到影響。

 

1.2  隱式注入

  隱式注入與顯式注入不同之處在於,隱式注入並不需要你編寫注入代碼,而是通過"this"綁定中的顯式綁定,".call"強制目標對象借用原對象的內容。

  看以下代碼:

 var Something={
        cool: function () {
            this.greeting="Hello word";
            this.count=this.count?this.count+1:1;
        }
    };
    Something.cool();   //對象的屬性時函數時要先進行引用
    console.log(Something.greeting);    //Hello  word
    console.log(Something.count);       //1
    var  Another={
        cool:function () {
            Something.cool.call(this);
        }
    };
    Another.cool();
    console.log(Another.greeting);   //Hello  word
    console.log(Another.count);     //1   count不是共享狀態

通過構造函數調用或者方法調用中使用"Something.cool.call(this)",我們實際上“借用”了"Something.cool()"並在"Another"的上下文環境中調用它,於是"this"理所應當的指向了當前對象。因此,我們將"Something.cool()"行爲注入到了"Another"中。

  雖然這類技術運用了"this"綁定的規則,重新使"this"綁定對象發生變化,但是這種方法仍然是不太靈活,比如受"this"的約束。因此我們儘量避免採用這種結構,保持代碼的完整性。

 

總結:類是一種設計模式。許多語言有着良好的面向對象的特性,在JavaScript中也有類似的結構,但是JavaScript中的“類”與其他的不一樣。

  類就是某一種事物的特徵,如電器,實例就是事物的具體化,如電器的實例:冰箱。

  繼承意味着複製,將原對象的屬性和行爲特徵(函數)複製到目標對象中。

  在JavaScript中不會自動創建對象的副本。

  因此在JavaScript中實現繼承,就需要我們自己動手創建,繼承在JavaScript中我們叫做“注入”。

  注入分爲兩種。

  ①顯式注入:利用for...in循環遍歷原對象,將不同的屬性複製到目標對象中。

  顯式注入通常有兩種方式,一是先初始化再複製,二是先複製再初始化。都一種性能要優於第二種。

  ②隱式注入:利用"this"綁定規則,".call"強制使this對象發生改變。

  上述的方法所複製的函數都是函數的引用,即函數是共享的

  總的來說,在JavaScript中無法真正模擬到類的複製行爲。因爲對象(以及函數)只能複製引用,無法複製被引用的對象或者函數本身。

  不建議在JavaScript中模擬面向對象的特性,會導致諸多的問題和隱患

 

 

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章