談一談原生JS中的【面向對象思想】

       【 重點提早說:面向對象的思想很重要!】
        最近開始接觸學習後臺的PHP語言,在接觸到PHP中的面向對象相關思想以後,忽然想到以前曾接觸的JS中的面向對象思想,無奈記性太差,便去翻了翻資料,花了點時間梳理下之前接觸過的OOP相關知識點,也但願在的PHP的學習中能相互對比,加深理解。
接下來可要進入化冰之路-PHP篇了,過幾天我將會再發一篇PHP中有關OOP的相關知識點梳理學習,但願你們放平心態,面向OOP,共同進步!
 

1、學習前,你該知道這些基礎知識~

 一、 語言的分類:
     一般狀況下我麼所涉及的計算機語言可大體分三類
          ①、面向機器:彙編語言。
          ②、面向過程:C語言
          ③、面向對象:Java、C++、PHP等。
二、 區分理解面向對象/面向過程  
     不少的初學者在剛接觸到面向對象這個概念時,老是感受其定義很籠統抽象,也比較難理解,咱們能夠將其和麪向過程比較理解記憶。
          ①、面向過程:專一於如何去解決一個問題的過程步驟,編程的特色是由一個個的函數去實現每一步的過程步驟,沒有類和對象的概念。
          ②、面向對象:專一於由哪個對象來解決這個問題,編程特色是出現了一個個的類,從類中拿到對象,由這個對象去解決具體問題
      舉個可能不是很恰當的例子來理解一下吧,相信不少的園友經歷過家裝吧,新買的房子交付了,就要考慮着手去裝修了。如今擺在咱們面前的有兩種方法:一是交給家裝公司來作,在你信得過的狀況下,只要告訴他你想要獲得什麼效果,剩下的讓他來作便可。二是你本身多費點功夫,去作設計圖,去跑家裝市場,去學習下如何在短時間內稱爲一個合格的設計師和施工人員。
     咱們以此類比,第一種方法中咱們只須要找個一家合適的家裝公司(一個對象),即面向對象。在第二種方法中咱們須要考慮不少解決問題的方法(函數),即面向過程。因而可知兩者的區別:   對於調用者來講,面向過程須要調用者本身去實現各類函數。 而面向對象,只須要告訴調用者對象中具體方法的功能,不須要調用者去了解方法中的實現細節。
 三、 面向對象的三大特徵       繼承、封裝、多態
      注意:JS能夠模擬實現繼承和封裝,可是不能模擬實現多態,故js是基於事件的,基於對象的語言。
四、 類和對象的概念 
                  (1)、類:一類具備相同特徵(屬性)和行爲(方法)的集合;
                                          例如:人類--->:     屬性:身高、姓名、體重
                                                                       方法:吃、喝、拉、撒
                   (2)、對象:從類中拿出具備肯定屬性值和方法的個體叫作對象:
                                              例如:張三--->:身高:180cm 體重:70kg    方法:說話--->我叫張三
                   (3)、類和對象的關係:
                                                類是抽象的,對象是具體的
                                                類是對象的抽象化,對象是類的具體化;
                     當咱們對人類的每個屬性都進行了具體的賦值,那麼就能夠說張三是由人類產生的對象.
五、建立一個類並實例化出對象
                      ①、建立一個類(構造函數):類名必須使用大駝峯法則。即首字母大寫;
                          function 類名 (屬性1){
                              this.屬性1 = 屬性1;
                              this.方法 = function(){
                                  方法中調用自身的屬性,必須使用this.屬性;
                              }
                          }
                       ②、經過類,實例化(new關鍵字)出一個對象:
                        var obi = new 類名 (屬性1 的實參);
                        obj.屬性;調用屬性
                        obj.方法();調用方法
                                       
                       ③、注意事項:
                            >>>經過類名,new出一個對象的過程,叫作"類的實例化";
                            >>>類中的this會在實例化的時候,指向新new出的對象。
                            >>>因此,this.屬性   this.方法 其實是將屬性和方法綁定在新new出的對象上;
                            >>>在類中訪問自身的屬性,必須使用this.屬性調用。若是直接使用變量名則沒法訪問該屬性值;
                            >>>類名必須使用大駝峯法則,注意與普通函數區分;           
咱們經過例子來看一下:
 //建立一個類的步驟以下: //★①、建立一個類(構造函數)
function Person (name,age) {
this.name = name;//前一個是自定義函數中的屬性,後一個是調用函數的形參,可用來傳遞實參;
this.age = age;//在類中訪問自身的屬性,必須使用this.屬性調用。
this.say = function (content) { alert("我叫"+this.name+"今年"+this.age+"歲了!我說了一句話"+content); } }
// ★②、類的實例化: var calcifer = new Person("calcifer",23); calcifer.say("哈哈哈!
");

 

/*上面的也能夠寫成:
        var calcifer = new Person();
        calcifer.name= "louver";
        calcifer.age = 14;
        calcifer.say("哈哈哈!");
        須要注意的是,賦值必須放在函數調用以前,不然結果爲undefined;
*/
運行後頁面會自動彈窗輸出:
 
     
 
 
 
 六、 類和對象的兩個重要屬性;
                       ①、 constructor:返回當前對象的構造函數;
                            calcifer.constructor  返回的是上面聲明的類;
                       ②、 instanceof:A instanceof B   檢測一個對象(A)是否是一個類(B)的一個實例;
                            calcifer instanceof Person;√  louver是函數Person的實例化;
                            calcifer instanceof Object;√  全部對象都是Object的一個實例;
                            Person instanceof Object;√  函數自己也是一個對象;
七、補充: 狹義對象與廣義對象
    
①、只有屬性和方法,除此以外,沒有任何其餘的內容;
 var obj= { } var obj = new object(); 
②、廣義對象:除了用字面量聲明的基本數據類型以外,JS中萬物皆對象。 換句話說。只要是能添加屬性和方法的變量,均可以稱爲對象。 
eg: △ 使用字面量聲明 var s = "111";//不是對象
        s.name = "aaa";
        console.log(typeof(s))//檢測爲string字符串類型;
        console.log(s.name) //undefined 字面量聲明的字符串不是對象,不能添加屬性; △ 使用關鍵字聲明 
        var m = new String();// 是對象
        m.name = "aaa";
        console.log(typeof(m))//檢測爲Object類型,由於m能夠添加屬性及方法,而s不行。 console.log(m.name) //aaa 使用new聲明的字符串是對象類型,能夠添加屬性和方法;

 


2、繼承與實現繼承的方式

 一、 什麼叫作繼承?
            使用一個子類繼承另外一個父類,那麼子類能夠自動擁有父類中的全部屬性和方法,這個過程叫作繼承;
            >>>繼承的兩方,發生在兩個類之間;
 二、 使用call bind apply實現繼承
           首先咱們先來了解一下call bind apply這三個函數,先看一下共同點:經過函數名調用這三個函數,能夠強行將函數中的this指定爲某一個對象;
它們的區別主要在於寫法的不一樣:接受func的參數列表的形式不一樣,除此以外,功能上沒有差異!
               ① call寫法:func.call(func的this指向的obj,func參數1,func參2,....);
               ② apply寫法:func.apply(func的this指向的obj,[func參數1,func參2,....]);//接收一個數組形式
               ③ bind寫法:func.bind(func的this指向的obj),(func參數1,func參2,....);
           接下來咱們主要看一下如何使用這三個函數實現繼承:(以call爲例)
實現步驟:
               ① 定義父類
                   function Parent (name){}
               ② 定義子類時,在子類中使用三個函數,調用父類,將父類函數中的this,指向爲子類函數的this
               
                   function Son (num,name){
                           this.num = num;
                         Person.call(this,name);
                     }
               ③ 實例化子類時,將自動繼承父類屬性
                     var s = new Son(12,"zhangsan");
代碼示例以下:
 function Person (name,age) {
     this.name =name;
     this.age =age;
     this.say = function () {
     alert("我叫"+this.name);
 } } 
   this.study = function () {
    alert("我叫"+this.name+"我今年"+this.age+"歲了!個人學號是"+this.num); 
} 
Person.call(this,name,age);
 }
 var s = new Student(12,"calcifer",24); 
 s.say(); s.study();

 

在瀏覽器中代碼運行結果以下:
第一張效果圖表示子類已經繼承了父類中的say()方法
第二張圖表示子類同時繼承了父類中的屬性:
 二、 使用for-in循環擴展Object實現繼承
            廢話很少說,咱們直奔主題,先講一下咱們這個方法實現繼承的思路吧,這個方法的關鍵點其實在於 經過for-in循環將父類對象的全部屬性和方法,所有賦給子類對象。
固然即便不擴展Object,也能經過簡單的循環實現操做;下面咱們詳細展現下步驟:
  ① 聲明一個父類
                   function Parent (){}
      聲明一個子類
                   function Son (){}   
  ② 經過prototype給Object類添加一個擴展方法:
               Object.prototype.extend = function  (parent) {
                           for(var i in parent){
                               this[i] = parent[i];    
                     }
               }
  ③ 分別拿到父類對象和子類對象:
                var p = new Parent();
                 var s = Son();
    ④ 用子類對象調用擴展方法,實現從繼承操做:
                   s.extend(p)
代碼示例以下:
function Person(name,age){ this.name = name; this.age = age; this.say = function(){ alert("我叫"+this.name); } } function Student(no){ this.no = no; this.study = function(){ alert("我在學習!"); } } var p = new Person("張三",12); var s = new Student("1234567"); for (var i in p) { s[i] = p[i]; } console.log(s);
咱們一樣能夠手寫繼承方法以下:
Object.prototype.extend1 = function(parent){ for(var i in parent){ this[i] = parent [i]; } } var p = new Person("張三",12); var s = new Student("1234567"); s.extend1(p); console.log(s);
二者最終的執行效果是同樣的:在控制檯上的輸出以下所示:
 
咱們能夠看到實例化的子類對象s中同時具備了父類對象的屬性以及方法,實現了繼承。
三、 使用原型實現繼承
       使用原型實現繼承其實就是 將父類對象,賦值給子類的prototype,那麼父類對象的屬性和方法就會出如今子類的prototype中, 那麼實例化子類時,子類的prototype又會到子類的__proto__中。
代碼示例以下:
function Person (name,age) { this.name = name, this.age = age, this.say = function () { alert("這是"+this.name); } } function Student (num) { this.num = num, this.study = function () { alert("我在學習!"); } } Student.prototype = new Person("張三",14); var s = new Student("1234567"); s.say(); s.study(); console.log(s);
將上面代碼運行,在瀏覽器的控制檯咱們能夠看到被打印出來的子類對象:
經過觀察咱們能夠發現,這種使用原型繼承的方法的特色在於子類自身的全部屬性,都是成員屬性;父類繼承過來的屬性,都是原型屬性,可是這種方法的缺點在於仍然沒法經過一步實例化拿到完整的子類對象。
這一種方法裏面涉及到了原型思想,考慮到不少的初學者沒有接觸到這一律念,下面咱們詳細的介紹一下原型與原型鏈以及原型屬性及方法。

3、原型與原型鏈&&原型屬性與原型方法方法

 
   一、prototype:函數的原型對象
               ① 只有函數纔有prototype,並且全部的函數必然有prototype!
               ② prototype自己也是一個對象!
               ③ prototype指向了當前函數所在的引用地址!
               
       二、__proto__:對象的原型
               ① 只有對象纔有__proto__,並且全部的對象必有__proto__;
               ② __proto__也是一個對象,因此也有本身的__proto__,順着這條線向上找的順序,就是原型鏈。
               ③ 數組都是對象,也都有本身的__proto__;
      三、實例化一個類,拿到對象的原理:
                   實例化一個類的時候,其實是將新對象的__proto__,指向構造函數所在的prototype;
                   也就是說:zhangsan.__proto__ ==Person.prototype √
      四、全部對象的__proto__沿着原型鏈向上查找都將指向Object的prototype;
               Object的prototype的原型,指向null;
               
          【原型鏈的指向問題】
               研究原型鏈的指向問題,就是要研究各類 特殊對象的__proto__的指向問題。
         一、經過構造函數,new出的對象。新對象的__proto__指向構造函數的prototype;      
         二、函數的__proto__,指向function()的prototype;
         三、函數的prototype的__proto__指向object的prototype;
               (直接使用{}字面聲明,或使用new Object 拿到的對象的__proto__ 直接指向Object的prototype)
         四、 Object的prototype的__proto__,指向null;
           (Object做爲一個特殊函數,它的__proto__指向function()的prototype;)

 
類中的屬性與方法的聲明方式
        一、【成員屬性和成員方法】
            this.name = "";  this.func = function(){};
            >>>屬於實例化出的新對象,使用對象.屬性調用;
         二、【靜態屬性與靜態方法】
            Person.name = "" Person.func= function(){};
            >>>屬於類的,用類名.屬性調用;
        三、【私有屬性和私有方法】
                在構造函數中,使用var聲明的變量稱爲私有屬性;
                      在構造函數中,使用function聲明的函數,稱爲私有方法;
         四、【原型屬性和原型方法】
               Person.prototype.name = "";
                Person.prototype.func = function(){};
                >>>將屬性或者方法寫到類的prototype上,在實例化的時候,這些屬性和方法就會進入到新對象的__proto__上,就可使用對象名調用;
            也就是說一、4可使用對象名訪問,2使用類名訪問,3只能在函數內部使用
       五、當訪問 對象的屬性或者方法時,會優先使用對象自身上的成員屬性和成員方法,    
              若是沒有找到就使用__proto__上面的原型屬性和原型方法,若是仍然沒有將繼續沿着原型鏈查找,最後返回undefined;
       六、咱們習慣上, 將屬性寫成成員屬性,將方法定義爲原型方法;
function Person (name) { this.name = name;//聲明成員屬性 } Person.prototype.say = function(){};
     緣由:
            ① 原型屬性在定義以後不能改變,沒法在實例化時進行賦值。因此屬性不能使用原型屬性。
                 可是方法,寫完以後基本上不用再須要進行改變,因此,方法可使用原型方法;
            ② 實例化出對象以後,屬性全在對象上,方法全在原型上,結構清晰。
            ③ 使用for-in遍歷對象時會將屬性和方法所有打印出來。
                  而方法每每不須要展現,那麼將方法寫在原型上,就可使用hasOwnProperty將原型方法過濾掉。
            ④ 方法寫在prototype上,將更加節省內存;
            ⑤ 這是官方推薦的寫法;
 示例以下:
function Person (name) { this.name = name;//成員屬性 var num = 1;//私有屬性 } Person.cout = "60億"//靜態屬性 Person.prototype.age =14;//原型屬性 var zhangsan = new Person("張三"); console.log(zhangsan);
瀏覽器控制檯打印以下:
 
 

4、JS中模擬實現封裝

 
                 在上面咱們講完JS的繼承以及繼承實現的方式以後,接下來咱們來看一下另外一大特徵----封裝。
 一、什麼叫封裝?
      ① 方法的封裝: 將類內部的函數進行私有化處理,不對外提供調用接口,沒法在類外部使用的方法,稱爲私有方法,即方法的封裝。
      ② 屬性的封裝: 將類中的屬性進行私有化處理,對外不能直接使用對象名訪問(私有屬性)。 同時,須要提供專門用於設置和讀取私有屬性的set/get方法,讓外部使用咱們提供的方法,對屬性進行操做。 這就叫屬性的封裝。
           在這裏咱們須要注意的地方是:封裝不是拒絕訪問,而是限制訪問。  它要求調用者,必須使用咱們提供的set/get方法進行屬性的操做,而不是直接拒絕操做。所以,單純的屬性私有化,不能稱爲封裝!必需要私有化以後,提供對應的set/get方法!!!
 二、如何實現封裝?
            接下來咱們以實例來看一下JS中如何模擬實現封裝:
function Person(name,age1){ this.name = name; // this.age = age; var age = 0; this.setAge = function(ages){ if(ages>0 && ages<=120){ age = ages; }else{ alert("年齡賦值失敗!"); } } // 當實例化類拿到對象時,能夠直接經過類名的()傳入年齡,設置私有屬性 if(age1 != undefined) this.setAge(age1); this.getAge = function(){ return age; } this.sayTime = function(){ alert("我說當前時間是"+getTime()); } this.writeTime = function (){ alert("我寫了當前時間是"+getTime()); } /* * 私有化的方法,只能在類內部被其餘方法調用,而不能對外提供功能。 這就是方法的封裝! */ function getTime(){ return new Date(); } }

5、閉包

                 咱們都知道在JS中函數存在做用域,函數外聲明的變量爲全局變量,而函數內聲明的變量爲全局變量。同時,在JS中沒有塊級做用域,也就是說,if/for等有{}的結構體,並不能具有本身的做用域;因此,函數外部不能訪問函數內部的局部變量(私有屬性)。由於函數內部的變量,在函數執行完成以後,就會被釋放掉。
                 可是 使用閉包,能夠訪問函數的私有變量!   JS中提供了一種閉包的概念:在函數中,定義一個子函數,子函數能夠訪問父函數的私有變量, 能夠在子函數中進行操做,最後將子函數經過return返回。
咱們舉個栗子來講明一下閉包的概念:
function func1 () { var num =1;//函數內部的局部變量 function func2 () { return num;//定義一個子函數,子函數能夠訪問父函數中聲明的私有變量,並處理後返回。 } return func2();//返回子函數 } var num = func1()();
閉包的做用:
               ① 能夠在函數外部訪問函數的私有變量;
               ② 讓函數內部的變量,能夠始終存在於內存中,不會在函數調用完成以後當即釋放!
接下來咱們看一個典型的案例,來更好的理解閉包的做用:
咱們想要實現點擊一個li,都會彈出其對應的數值
<body> <ul> <li>11111</li> <li>22223</li> <li>33333</li> <li>44444</li> <li>55555</li> </ul> </body>
var lis = document.getElementsByClassName("li"); for(var i=0; i<lis.length; i++){ lis[i].onclick = function(){ alert(i); } }
在運行上面的一段代碼時,發現不管點擊任何一個li都會彈出5.
分析一下錯誤緣由在於
              代碼從上自下,執行完畢後,li的onclick還沒觸發,for循環已經轉完。 而for循環沒有本身的做用域!因此循環5次,用的是同一個全局變量i。也就是說在for循環轉完之後,這個全局變量i已經變成了5. 那麼再點擊li的時候,不管點擊第幾個,i都是5。
接下來咱們提供了三種解決方法,具體以下:
一、【使用閉包解決上述問題】
             解決原理:函數具備本身的做用域,在for循環轉一次建立一個自執行函數,
             在每一個自執行函數中,都有本身獨立的i,而不會被釋放掉。
             因此for循環轉完之後,建立的5個自執行函數的做用域中,分別存儲了5個
             不一樣的i變量,也就解決了問題。
var lis = document.getElementsByClassName("li"); for(var i=0; i<lis.length; i++){ !function(i){ lis[i].onclick = function(){ alert(i); } }(i); }
二、【使用let解決】
                       解決原理:let具備本身的塊級做用域,因此for循環轉一次會建立一個塊級做用域;
var lis = document.getElementsByClassName("li"); for(let i=0; i<lis.length; i++){ lis[i].onclick = function(){ alert(i); } }
  三、【使用this解決原理】
            出錯的緣由在於全局變量i在屢次循環以後被污染。那麼在點擊事件中,就能夠不使用i變量,而是使用 this代替lis[i],這樣不會出現錯誤
var lis = document.getElementsByClassName("li"); for(var i=0; i<lis.length; i++){ lis[i].onclick = function(){ alert(this。innerText); } }
使用修改後的方法,運行文件能夠看到:

好了,有關JS的OOP相關知識就講這麼多吧,過幾天還會爲你們帶來PHP語言的面向對象的介紹,在PHP中面向對象更正統也更加有趣,但願你們保持關注!
共勉,謝謝!
相關文章
相關標籤/搜索