原生javascript難點總結(1)---面向對象分析以及帶來的思考


  ------*本文默認讀者已有面向對象語言(OOP)的基礎*------

 

  我們都知道在面向對象語言有三個基本特征 :  封裝繼承多態。而js初學者一般會覺得js同其他類C語言一樣,有類似於Class這樣的關鍵字可以讓我們在js中更好的進行面向對象的操作。可事實並非如此。

  嚴格地說,我們並不能稱js是一種OOP語言,但是我們可以利用js里面的一些高級特性來進行OOP編程。

 

----封裝

  在js中,如何來創建一個對象呢?這非常簡單,我們只需要new一個已封裝好的函數(就是類C語言中的),就可以實例化一個對象了。

  那我們首先來構造這么一個"類",在構造之前必須知道一個類需要有"變量"和"方法",接着我們就來構造這個"類":

1 function Parent(){
2     this.name = "Parent";
3     this.sayName = function(){
4         console.log(this.name);
5     }
6 }
7 var p1 = new Parent();
8 p1.sayName();
View Code

   怎么樣,很簡單吧?這樣就封裝好了一個"類"了。但是這個類看起來很笨重,因為名字是固定的,所以我們需要進行修改並擴展。

1 function Parent(name){
2     this.name = name;
3     this.sayName = function(){
4         console.log(this.name);
5     }
6 }
7 var p1 = new Parent("yxy");
8 p1.sayName(); //控制台打印yxy
View Code

  這樣我們封裝的"類"就變得有變量,有方法,有外部參數,可復用。看上去已經非常完美了?

                                 

  試着這樣想想。每次創建一個對象,都會創建一個變量,同樣也都會創建一個方法。而這個方法對所有對象都只是同一個方法效果,為什么你還要去對這個方法創建多次呢?學過java或c++的人可能會想,你怎么知道這個方法是被創建了多次,而不是引用的同一個呢?嗯?我們來做個測試。

1 function Parent(name){
2     this.name = name;
3     this.sayName = function(){
4         console.log(this.name);
5     }
6 }
7 var p1 = new Parent("yxy");
8 var p2 = new Parent("danshengou");
9 console.log(p1.sayName == p2.sayName); //false
View Code

  利用"=="可以看到兩個方法是不同的,那就是被創建了多次。所以當我們創建了多個對象后,每個對象的每個方法都是不同的!這顯然會大大消耗內存,不利於web開發。那如何解決呢?

  前面的文章中我提到的js中的"類"都是帶雙引號的,原因很簡單,js不支持類(在ES6的規范中就可以支持了),但為了方便,我們可以稱之為"偽類"。但在我們之前的例子中都還不能說得上是偽類!因為完全就沒有方法的復用,不是嗎?接下來我們會引入一個概念性很強的一個術語:"原型"

  原型,prototype,每一個函數都有一個原型屬性,這個屬性是一個指針,指向一個對象,而這個對象的用途是包含可以由特定類型的所有實例共享的屬性和方法。確實是非常不好理解。簡單的說,原型屬性就是通過調用構造函數而創建的那個對象實例的原型對象如果你還不清楚,那我推薦你好好地看看<<javascript高級程序設計>>一書的第六章。

  在這里先記住為什么我們要在封裝中使用原型。如果你還對原型的概念模糊不清,不急,先理解使用它的好處。使用原型的好處是可以讓所有對象實例共享它所包含的屬性和方法。好像清晰多了?那我們來改裝一下前面提到的那個例子。

 1 function Parent(name){
 2     this.name = name;
 3 }
 4 Parent.prototype.sayName = function(){
 5     console.log(this.name);
 6 }
 7 var p1 = new Parent("yxy");
 8 p1.sayName(); //調用原型中的sayName方法,打印出yxy
 9 var p2 = new Parent("danshengou");
10 console.log(p1.sayName == p2.sayName); //true
11 console.log(p1.sayName === p2.sayName); //true
View Code

  在這個例子中,我們用原型模式構造了一個函數。我們實例化了兩個對象,用"=="和"==="比較了它的值和地址,發現都是一樣的。看來原型真的是很好用啊,大大節省了我們的內存空間。你可能會問,為什么不把變量也放到原型中呢?因為原型是所有對象所共享的,每個對象的屬性必須是該對象自己持有的,如果放到原型中,那這些對象便沒有任何區別了。這就是我們所謂的"組合繼承"。

  到目前為止,我們就成功地創建了一個比較完美的js"偽類"了。而且你應該對原型有了很大的了解。

 

----繼承

  在你了解了整個封裝過程后,你對js可能很失望了,既然有這樣復雜的封裝,那繼承肯定也很難了。

  可是,繼承的語法其實很簡單。

  我們先來看看一個繼承:

 1 function Parent(name){
 2     this.name = name;
 3 }
 4 Parent.prototype.sayName = function(){
 5     console.log(this.name);
 6 }
 7 
 8 function Child(name,position){
 9     this.position = position;     
10     Parent.call(this,name);      
11 }
12 Child.prototype = new Parent();
13 Child.prototype.sayPos = function(){
14     console.log(this.position);
15 }
16 
17 var p1 = new Child("yxy","student");
18 p1.sayName();
19 p1.sayPos();
View Code

  有兩個地方很吸引我們眼球。

  1.Parent.call(this,name)

  2.Child.prototype = new Parent();

  細講可能容易繞暈,我從繼承的概念入手,首先,子對象繼承的是父對象的變量和方法。那我們可以很清晰地從代碼作用范圍看到,Parent.call(this,name)在構造函數中,顯然是在繼承變量。而Child.prototype = new Parent(),這個Parent對象不帶參數地創建,顯然是在繼承原型。這樣說是不是好理解多了?

  Child.prototype = new Parent(),這條語句很簡單,我不再去細究它。這里重點要講的是Parent.call(this,name)這個東西。

  Parent.call(this,name)還有另外幾種寫法:

    1.Parent.call(this,arguments[0]);

    2.Parent.apply(this,[name]);

    3.Parent.apply(this,arguments);

  雖然是四個寫法都不同,但是效果都是一樣的 : 將父類構造函數的上下文引用到子類的構造函數上下文中

  call和apply,都可以用來代替另一個對象調用一個方法。兩者可將一個函數的對象上下文從初始的上下文改變為由this指定的新對象。又有點繞?簡單地說,就是this對象在當前的上下文中執行了一次Parent函數,並且把參數(this后面那個參數)傳遞給Parent函數。

  我們都知道函數怎么暴露自己內部的屬性,就是把它執行一次,就可以在它的父級作用域中訪問到。那理解這個apply,call的作用機理就很簡單了。

  繼承語法很簡單,但是其繼承機制還是需要花時間去理解的。

 

 

 

  終於我們把有關js面向對象的步驟給很詳細的做了一次,可能大家覺得自己終於可以開始模擬一些有難度的面向對象的實例了。這一次我並不會反對大家,但是你難道沒看出來少了什么嗎?我來運行一下代碼:

 1 function Parent(name){
 2     this.name = name;
 3 }
 4 Parent.prototype.sayName = function(){
 5     console.log(this.name);
 6 }
 7 
 8 function Child(name,position){
 9     this.position = position;
10     //Parent.call(this,name);
11     Parent.apply(this,[name]);
12 }
13 Child.prototype = new Parent();
14 Child.prototype.sayPos = function(){
15     console.log(this.position);
16 }
17 
18 var p1 = new Child("yxy","student");
19 console.log(p1.name); //yxy
20 console.log(p1.position); //student
View Code

  嗯?我明明想的是將變量私有化啊,可是為什么能訪問到呢?細心地人可能早就在繼承那個部分的時候就想到了,函數只要一執行,我的變量,方法都會暴露在外面了!其實這根本就不是封裝啊!只是裝而已。

  沒錯,js並沒有private,public這些關鍵字來定義屬性和方法的私有和公有性質。可能你會很失望很失望,前面做了那么多,得來的是一個半瘸子的面向對象。其實不光你這么想,很多開發人員也覺得,因此他們用復雜的名字來命名一些變量,以保證其不會那么容易被訪問到。這不失為一種辦法,但是難道就沒有別的辦法?

  仔細想想,還有什么辦法能將模擬private,public這樣的操作呢?我給出一個例子和一個概念,然后大家可以在這上開始自己對js真正面向對象的思考。

  概念 : "模塊模式"

  代碼 : 

 1 var Parent = function(name,publicAge){
 2     var myName = name;
 3     return {
 4         age : publicAge,
 5         sayName : function(){
 6             console.log(myName);
 7         }
 8     }; 
 9 };
10 
11 var p1 = Parent("yxy","20");
12 p1.sayName(); //yxy
13 console.log(p1.age); //20 
14 console.log(p1.myName); //undefined
View Code

  當你能理解封裝中訪問等級的時候,javascript真正的思考才剛剛開始...

  下一篇文章將會講到"由封裝引出的模塊化思考以及閉包的運用"。

  

 

  

  

 

  

  

 


免責聲明!

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



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