1. 程式人生 > >談一談原生JS中的【面向對象思想】

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

時間 lin 因此 pre defined 成員方法 .... 面向對象的思想 其實在

重點提前說:面向對象的思想很重要!】 最近開始接觸學習後臺的PHP語言,在接觸到PHP中的面向對象相關思想之後,突然想到之前曾接觸的JS中的面向對象思想,無奈記性太差,便去翻了翻資料,花了點時間梳理下以前接觸過的OOP相關知識點,也希望在的PHP的學習中能相互對比,加深理解。 接下來可要進入化冰之路-PHP篇了,過幾天我將會再發一篇PHP中有關OOP的相關知識點梳理學習,希望大家放平心態,面向OOP,共同進步!
一、學習前,你該知道這些基礎知識~
1、語言的分類: 通常情況下我麽所涉及的計算機語言可大致分三類 ①、面向機器:匯編語言。 ②、面向過程:C語言 ③、面向對象:Java、C++、PHP等。 2、區分理解面向對象/面向過程
很多的初學者在剛接觸到面向對象這個概念時,總是感覺其定義很籠統抽象,也比較難理解,我們可以將其和面向過程比較理解記憶。 ①、面向過程:專註於如何去解決一個問題的過程步驟,編程的特點是由一個個的函數去實現每一步的過程步驟,沒有類和對象的概念。 ②、面向對象:專註於由哪一個對象來解決這個問題,編程特點是出現了一個個的類,從類中拿到對象,由這個對象去解決具體問題 舉個可能不是很恰當的例子來理解一下吧,相信很多的園友經歷過家裝吧,新買的房子交付了,就要考慮著手去裝修了。現在擺在我們面前的有兩種方法:一是交給家裝公司來做,在你信得過的情況下,只要告訴他你想要得到什麽效果,剩下的讓他來做即可。二是你自己多費點功夫,去做設計圖,去跑家裝市場,去學習下如何在短期內稱為一個合格的設計師和施工人員。 我們以此類比,第一種方法中我們只需要找個一家合適的家裝公司(一個對象),即面向對象。在第二種方法中我們需要考慮很多解決問題的方法(函數),即面向過程。由此可見二者的區別: 對於調用者來說,面向過程需要調用者自己去實現各種函數。 而面向對象,只需要告訴調用者對象中具體方法的功能,不需要調用者去了解方法中的實現細節。 3、面向對象的三大特征 繼承、封裝、多態
註意:JS可以模擬實現繼承和封裝,但是不能模擬實現多態,故js是基於事件的,基於對象的語言。 4、類和對象的概念 (1)、類:一類具有相同特征(屬性)和行為(方法)的集合; 例如:人類--->: 屬性:身高、姓名、體重 方法:吃、喝、拉、撒 (2)、對象:從類中拿出具有確定屬性值和方法的個體叫做對象:
例如:張三--->:身高:180cm 體重:70kg 方法:說話--->我叫張三 (3)、類和對象的關系: 類是抽象的,對象是具體的 類是對象的抽象化,對象是類的具體化; 當我們對人類的每一個屬性都進行了具體的賦值,那麽就可以說張三是由人類產生的對象. 5、創建一個類並實例化出對象 ①、創建一個類(構造函數):類名必須使用大駝峰法則。即首字母大寫; 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; */ 運行後頁面會自動彈窗輸出: 技術分享 6、類和對象的兩個重要屬性; ①、constructor:返回當前對象的構造函數; calcifer.constructor 返回的是上面聲明的類; ②、instanceof:A instanceof B 檢測一個對象(A)是不是一個類(B)的一個實例; calcifer instanceof Person;√ louver是函數Person的實例化; calcifer instanceof Object;√ 所有對象都是Object的一個實例; Person instanceof Object;√ 函數本身也是一個對象; 7、補充:狹義對象與廣義對象 ①、只有屬性和方法,除此之外,沒有任何其他的內容; 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聲明的字符串是對象類型,可以添加屬性和方法;
二、繼承與實現繼承的方式
1、什麽叫做繼承? 使用一個子類繼承另一個父類,那麽子類可以自動擁有父類中的所有屬性和方法,這個過程叫做繼承; >>>繼承的兩方,發生在兩個類之間; 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()方法 技術分享 第二張圖表示子類同時繼承了父類中的屬性: 技術分享 2、使用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中同時具有了父類對象的屬性以及方法,實現了繼承。 3、使用原型實現繼承 使用原型實現繼承其實就是 將父類對象,賦值給子類的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); 將上面代碼運行,在瀏覽器的控制臺我們可以看到被打印出來的子類對象: 技術分享 通過觀察我們可以發現,這種使用原型繼承的方法的特點在於子類自身的所有屬性,都是成員屬性;父類繼承過來的屬性,都是原型屬性,但是這種方法的缺點在於仍然無法通過一步實例化拿到完整的子類對象。 這一種方法裏面涉及到了原型思想,考慮到很多的初學者沒有接觸到這一概念,下面我們詳細的介紹一下原型與原型鏈以及原型屬性及方法。
三、原型與原型鏈&&原型屬性與原型方法方法
1、prototype:函數的原型對象 ① 只有函數才有prototype,而且所有的函數必然有prototype! ② prototype本身也是一個對象! ③ prototype指向了當前函數所在的引用地址! 2、__proto__:對象的原型 ① 只有對象才有__proto__,而且所有的對象必有__proto__; ② __proto__也是一個對象,所以也有自己的__proto__,順著這條線向上找的順序,就是原型鏈。 ③ 數組都是對象,也都有自己的__proto__; 3、實例化一個類,拿到對象的原理: 實例化一個類的時候,實際上是將新對象的__proto__,指向構造函數所在的prototype; 也就是說:zhangsan.__proto__ ==Person.prototype √ 4、所有對象的__proto__沿著原型鏈向上查找都將指向Object的prototype; Object的prototype的原型,指向null; 【原型鏈的指向問題】 研究原型鏈的指向問題,就是要研究各種特殊對象的__proto__的指向問題。 1、通過構造函數,new出的對象。新對象的__proto__指向構造函數的prototype; 2、函數的__proto__,指向function()的prototype; 3、函數的prototype的__proto__指向object的prototype; (直接使用{}字面聲明,或使用new Object 拿到的對象的__proto__ 直接指向Object的prototype) 4、 Object的prototype的__proto__,指向null; (Object作為一個特殊函數,它的__proto__指向function()的prototype;)
類中的屬性與方法的聲明方式 1、【成員屬性和成員方法】 this.name = ""; this.func = function(){}; >>>屬於實例化出的新對象,使用對象.屬性調用; 2、【靜態屬性與靜態方法】 Person.name = "" Person.func= function(){}; >>>屬於類的,用類名.屬性調用; 3、【私有屬性和私有方法】 在構造函數中,使用var聲明的變量稱為私有屬性; 在構造函數中,使用function聲明的函數,稱為私有方法; 4、【原型屬性和原型方法】 Person.prototype.name = ""; Person.prototype.func = function(){}; >>>將屬性或者方法寫到類的prototype上,在實例化的時候,這些屬性和方法就會進入到新對象的__proto__上,就可以使用對象名調用; 也就是說1、4可以使用對象名訪問,2使用類名訪問,3只能在函數內部使用 5、當訪問 對象的屬性或者方法時,會優先使用對象自身上的成員屬性和成員方法, 如果沒有找到就使用__proto__上面的原型屬性和原型方法,如果仍然沒有將繼續沿著原型鏈查找,最後返回undefined; 6、我們習慣上,將屬性寫成成員屬性,將方法定義為原型方法; 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); 瀏覽器控制臺打印如下: 技術分享
四、JS中模擬實現封裝
在上面我們講完JS的繼承以及繼承實現的方式之後,接下來我們來看一下另一大特征----封裝。 1、什麽叫封裝?方法的封裝: 將類內部的函數進行私有化處理,不對外提供調用接口,無法在類外部使用的方法,稱為私有方法,即方法的封裝。屬性的封裝: 將類中的屬性進行私有化處理,對外不能直接使用對象名訪問(私有屬性)。 同時,需要提供專門用於設置和讀取私有屬性的set/get方法,讓外部使用我們提供的方法,對屬性進行操作。 這就叫屬性的封裝。 在這裏我們需要註意的地方是:封裝不是拒絕訪問,而是限制訪問。 它要求調用者,必須使用我們提供的set/get方法進行屬性的操作,而不是直接拒絕操作。因此,單純的屬性私有化,不能稱為封裝!必須要私有化之後,提供對應的set/get方法!!! 2、如何實現封裝? 接下來我們以實例來看一下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(); } }
五、閉包
我們都知道在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。 接下來我們提供了三種解決方法,具體如下: 1、【使用閉包解決上述問題】 解決原理:函數具有自己的作用域,在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); } 2、【使用let解決】 解決原理:let具有自己的塊級作用域,所以for循環轉一次會創建一個塊級作用域; var lis = document.getElementsByClassName("li"); for(let i=0; i<lis.length; i++){ lis[i].onclick = function(){ alert(i); } } 3、【使用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中面向對象更正統也更加有趣,希望大家保持關註! 共勉,謝謝!

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