【轉載】面向對象編程
原文鏈接:https://blog.csdn.net/sunshine940326/article/details/72872386
什麽是面向對象編程
面向對象的思想主要是以對象為主,將一個問題抽象出具體的對象,並且將抽象出來的對象和對象的屬性和方法封裝成一個類。
面向對象是把構成問題事務分解成各個對象,建立對象的目的不是為了完成一個步驟,而是為了描敘某個事物在整個解決問題的步驟中的行為。
面向對象和面向過程的區別
面向對象和面向過程是兩種不同的編程思想,我們經常會聽到兩者的比較,剛開始編程的時候,大部分應該都是使用的面向過程的編程,但是隨著我們的成長,還是面向對象的編程思想比較好一點~
其實面向對象和面向過程並不是完全相對的,也並不是完全獨立的。
我認為面向對象和面向過程的主要區別是面向過程主要是以動詞為主,解決問題的方式是按照順序一步一步調用不同的函數。
而面向對象主要是以名詞為主,將問題抽象出具體的對象,而這個對象有自己的屬性和方法,在解決問題的時候是將不同的對象組合在一起使用。
所以說面向對象的好處就是可擴展性更強一些,解決了代碼重用性的問題。
- 面向過程就是分析出解決問題所需要的步驟,然後用函數把這些步驟一步一步實現,使用的時候一個一個依次調用就可以了。
- 面向對象是把構成問題事務分解成各個對象,建立對象的目的不是為了完成一個步驟,而是為了描敘某個事物在整個解決問題的步驟中的行為。
有一個知乎的高票回答很有意思,給大家分享一下~
面向對象: 狗.吃(屎)
面向過程: 吃.(狗,屎)
具體的實現我們看一下最經典的“把大象放冰箱”這個問題
面向過程的解決方法
在面向過程的編程方式中實現“把大象放冰箱”這個問題答案是耳熟能詳的,一共分三步:
- 開門(冰箱);
- 裝進(冰箱,大象);
- 關門(冰箱)。
面向對象的解決方法
- 冰箱.開門()
- 冰箱.裝進(大象)
- 冰箱.關門()
可以看出來面向對象和面向過程的側重點是不同的,面向過程是以動詞為主,完成一個事件就是將不同的動作函數按順序調用。
面向對象是以主謂為主。將主謂看成一個一個的對象,然後對象有自己的屬性和方法。比如說,冰箱有自己的id屬性,有開門的方法。然後就可以直接調用冰箱的開門方法給其傳入一個參數大象就可以了。
簡單的例子面向對象和面向過程的好處還不是很明顯。
五子棋例子
下面是一個我認為比較能夠說明兩者區別的一個栗子~:
例如五子棋,面向過程的設計思路就是首先分析問題的步驟:
- 開始遊戲
- 黑子先走
- 繪制畫面
- 判斷輸贏
- 輪到白子
- 繪制畫面
- 判斷輸贏
- 返回步驟2
把上面每個步驟用分別的函數來實現,問題就解決了。
而面向對象的設計則是從另外的思路來解決問題。整個五子棋可以分為
- 黑白雙方,這兩方的行為是一模一樣的
- 棋盤系統,負責繪制畫面
第一類對象(玩家對象)負責接受用戶輸入,並告知第二類對象(棋盤對象)棋子布局的變化,棋盤對象接收到了棋子的i變化就要負責在屏幕上面顯示出這種變化,同時利用第三類對象(規則系統)來對棋局進行判定。
可以明顯地看出,面向對象是以功能來劃分問題,而不是步驟。同樣是繪制棋局,這樣的行為在面向過程的設計中分散在了總多步驟中,很可能出現不同的繪制版本,因為通常設計人員會考慮到實際情況進行各種各樣的簡化。而面向對象的設計中,繪圖只可能在棋盤對象中出現,從而保證了繪圖的統一。
功能上的統一保證了面向對象設計的可擴展性。比如我要加入悔棋的功能,如果要改動面向過程的設計,那麽從輸入到判斷到顯示這一連串的步驟都要改動,甚至步驟之間的循序都要進行大規模調整。如果是面向對象的話,只用改動棋盤對象就行了,棋盤系統保存了黑白雙方的棋譜,簡單回溯就可以了,而顯示和規則判斷則不用顧及,同時整個對對象功能的調用順序都沒有變化,改動只是局部的。
再比如我要把這個五子棋遊戲改為圍棋遊戲,如果你是面向過程設計,那麽五子棋的規則就分布在了你的程序的每一個角落,要改動還不如重寫。但是如果你當初就是面向對象的設計,那麽你只用改動規則對象就可以了,五子棋和圍棋的區別不就是規則嗎?(當然棋盤大小好像也不一樣,但是你會覺得這是一個難題嗎?直接在棋盤對象中進行一番小改動就可以了。)而下棋的大致步驟從面向對象的角度來看沒有任何變化。
當然,要達到改動只是局部的需要設計的人有足夠的經驗,使用對象不能保證你的程序就是面向對象,初學者或者很蹩腳的程序員很可能以面向對象之虛而行面向過程之實,這樣設計出來的所謂面向對象的程序很難有良好的可移植性和可擴展性。
封裝
面向對象有三大特性,封裝、繼承和多態。對於ES5來說,沒有class
的概念,並且由於js的函數級作用域(在函數內部的變量在函數外訪問不到),所以我們就可以模擬 class
的概念,在es5中,類其實就是保存了一個函數的變量,這個函數有自己的屬性和方法。將屬性和方法組成一個類的過程就是封裝。
封裝:把客觀事物封裝成抽象的類,隱藏屬性和方法的實現細節,僅對外公開接口。
通過構造函數添加
javascript提供了一個構造函數(Constructor)模式,用來在創建對象時初始化對象。
構造函數其實就是普通的函數,只不過有以下的特點
- 首字母大寫(建議構造函數首字母大寫,即使用大駝峰命名,非構造函數首字母小寫)
- 內部使用
this
- 使用
new
生成實例
通過構造函數添加屬性和方法實際上也就是通過this添加的屬性和方法。因為this總是指向當前對象的,所以通過this添加的屬性和方法只在當前對象上添加,是該對象自身擁有的。所以我們實例化一個新對象的時候,this指向的屬性和方法都會得到相應的創建,也就是會在內存中復制一份,這樣就造成了內存的浪費。
function Cat(name,color){ this.name = name; this.color = color; this.eat = function () { alert(‘吃老鼠‘) } }
生成實例:
var cat1 = new Cat(‘tom‘,‘red‘)
通過this定義的屬性和方法,我們實例化對象的時候都會重新復制一份
通過原型prototype
在類上通過 this
的方式添加屬性和對象會導致內存浪費的問題,我們就考慮,有什麽方法可以讓實例化的類所使用的方法直接使用指針指向同一個方法。於是,就想到了原型的方式
Javascript規定,每一個構造函數都有一個prototype屬性,指向另一個對象。這個對象的所有屬性和方法,都會被構造函數的實例繼承。
也就是說,對於那些不變的屬性和方法,我們可以直接將其添加在類的prototype
對象上。
function Cat(name,color){ this.name = name; this.color = color; } Cat.prototype.type = "貓科動物"; Cat.prototype.eat = function(){alert("吃老鼠")};
然後生成實例
var cat1 = new Cat("大毛","黃色"); var cat2 = new Cat("二毛","黑色"); alert(cat1.type); // 貓科動物 cat1.eat(); // 吃老鼠
這時所有實例的type
屬性和eat()
方法,其實都是同一個內存地址,指向prototype
對象,因此就提高了運行效率。
在類的外部通過.語法添加
我們還可以在類的外部通過.
語法進行添加,因為在實例化對象的時候,並不會執行到在類外部通過.
語法添加的屬性,所以實例化之後的對象是不能訪問到.
語法所添加的對象和屬性的,只能通過該類訪問。
三者的區別
通過構造函數、原型和.
語法三者都可以在類上添加屬性和方法。但是三者是有一定的區別的。
構造函數:通過this添加的屬性和方法總是指向當前對象的,所以在實例化的時候,通過this添加的屬性和方法都會在內存中復制一份,這樣就會造成內存的浪費。但是這樣創建的好處是即使改變了某一個對象的屬性或方法,不會影響其他的對象(因為每一個對象都是復制的一份)。
原型:通過原型繼承的方法並不是自身的,我們要在原型鏈上一層一層的查找,這樣創建的好處是只在內存中創建一次,實例化的對象都會指向這個prototype
對象,但是這樣做也有弊端,因為實例化的對象的原型都是指向同一內存地址,改動其中的一個對象的屬性可能會影響到其他的對象 .
語法:在類的外部通過.
語法創建的屬性和方法只會創建一次,但是這樣創建的實例化的對象是訪問不到的,只能通過類的自身訪問。
javascript也有private public protected
對於java程序員來說private public protected這三個關鍵字應該是很熟悉的哈,但是在js中,並沒有類似於private public protected這樣的關鍵字,但是我們又希望我們定義的屬性和方法有一定的訪問限制,於是我們就可以模擬private public protected這些訪問權限。
不熟悉java的小夥伴可能不太清楚private public protected概念(其他語言我也不清楚有沒有哈,但是應該都是類似的~),先來科普一下小知識點~
- public:public表明該數據成員、成員函數是對所有用戶開放的,所有用戶都可以直接進行調用
- private:private表示私有,私有的意思就是除了class自己之外,任何人都不可以直接使用,私有財產神聖不可侵犯嘛,即便是子女,朋友,都不可以使用。
- protected:protected對於子女、朋友來說,就是public的,可以自由使用,沒有任何限制,而對於其他的外部class,protected就變成private。
js中的private
因為javascript函數級作用域的特性(在函數中定義的屬性和方法外界訪問不到),所以我們在函數內部直接定義的屬性和方法都是私有的。
js中的public
通過new關鍵詞實例化時,this定義的屬性和變量都會被復制一遍,所以通過this定義的屬性和方法就是公有的。
通過prototype創建的屬性在類的實例化之後類的實例化對象也是可以訪問到的,所以也是公有的。
js中的protected
在函數的內部,我們可以通過this定義的方法訪問到一些類的私有屬性和方法,在實例化的時候就可以初始化對象的一些屬性了。
new的實質
雖然很多人都已經了解了new的實質,那麽我還是要再說一下new 的實質 var o = new Object()
1. 新建一個對象o
2. o. __proto__ = Object.prototype
將新創建的對象的__proto__
屬性指向構造函數的prototype
3. 將this指向新創建的對象
4. 返回新對象,但是這裏需要看構造函數有沒有返回值,如果構造函數的返回值為基本數據類型string,boolean,number,null,undefined
,那麽就返回新對象,如果構造函數的返回值為對象類型,那麽就返回這個對象類型。
舉例:
var Book = function (id, name, price) { //private(在函數內部定義,函數外部訪問不到,實例化之後實例化的對象訪問不到) var num = 1; var id = id; function checkId() { console.log(‘private‘) } //protected(可以訪問到函數內部的私有屬性和私有方法,在實例化之後就可以對實例化的類進行初始化拿到函數的私有屬性) this.getName = function () { console.log(id) } this.getPrice = function () { console.log(price) } //public(實例化的之後,實例化的對象就可以訪問到了~) this.name = name; this.copy = function () { console.log(‘this is public‘) } } //在Book的原型上添加的方法實例化之後可以被實例化對象繼承 Book.prototype.proFunction = function () { console.log(‘this is proFunction‘) } //在函數外部通過.語法創建的屬性和方法,只能通過該類訪問,實例化對象訪問不到 Book.setTime = function () { console.log(‘this is new time‘) } var book1 = new Book(‘111‘,‘悲慘世界‘,‘$99‘) book1.getName(); // 111 getName是protected,可以訪問到類的私有屬性,所以實例化之後也可以訪問到函數的私有屬性 book1.checkId(); //報錯book1.checkId is not a function console.log(book1.id) // undefined id是在函數內部通過定義的,是私有屬性,所以實例化對象訪問不到 console.log(book1.name) //name 是通過this創建的,所以在實例化的時候會在book1中復制一遍name屬性,所以可以訪問到 book1.copy() //this is public book1.proFunction(); //this is proFunction Book.setTime(); //this is new time book1.setTime(); //報錯book1.setTime is not a function
繼承
繼承:子類可以使用父類的所有功能,並且對這些功能進行擴展。繼承的過程,就是從一般到特殊的過程。
其實繼承都是基於以上封裝方法的三個特性來實現的。
類式繼承
所謂的類式繼承就是使用的原型的方式,將方法添加在父類的原型上,然後子類的原型是父類的一個實例化對象。
//聲明父類 var SuperClass = function () { var id = 1; this.name = [‘javascript‘]; this.superValue = function () { console.log(‘superValue is true‘); console.log(id) } }; //為父類添加共有方法 SuperClass.prototype.getSuperValue = function () { return this.superValue(); }; //聲明子類 var SubClass = function () { this.subValue = function () { console.log(‘this is subValue ‘) } }; //繼承父類 SubClass.prototype = new SuperClass() ; //為子類添加共有方法 SubClass.prototype.getSubValue= function () { return this.subValue() }; var sub = new SubClass(); var sub2 = new SubClass(); sub.getSuperValue(); //superValue is true sub.getSubValue(); //this is subValue console.log(sub.id); //undefined console.log(sub.name); //javascript sub.name.push(‘java‘); //["javascript"] console.log(sub2.name) //["javascript", "java"]
其中最核心的一句代碼是SubClass.prototype = new SuperClass() ;
類的原型對象prototype
對象的作用就是為類的原型添加共有方法的,但是類不能直接訪問這些方法,只有將類實例化之後,新創建的對象復制了父類構造函數中的屬性和方法,並將原型__proto__
指向了父類的原型對象。這樣子類就可以訪問父類的public
和protected
的屬性和方法,同時,父類中的private
的屬性和方法不會被子類繼承。
敲黑板,如上述代碼的最後一段,使用類繼承的方法,如果父類的構造函數中有引用類型,就會在子類中被所有實例共用,因此一個子類的實例如果更改了這個引用類型,就會影響到其他子類的實例。
提一個小問題~為什麽一個子類的實例如果更改了這個引用類型,就會影響到其他子類的實例呢,在javascript中,什麽是引用類型呢,引用類型和其他的類型又有什麽區別呢?
構造函數繼承
正式因為有了上述的缺點,才有了構造函數繼承,構造函數繼承的核心思想就是SuperClass.call(this,id)
,直接改變this的指向,使通過this創建的屬性和方法在子類中復制一份,因為是單獨復制的,所以各個實例化的子類互不影響。但是會造成內存浪費的問題
//構造函數繼承 //聲明父類 function SuperClass(id) { var name = ‘javascript‘ this.books=[‘javascript‘,‘html‘,‘css‘]; this.id = id } //聲明父類原型方法 SuperClass.prototype.showBooks = function () { console.log(this.books) } //聲明子類 function SubClass(id) { SuperClass.call(this,id) } //創建第一個子類實例 var subclass1 = new SubClass(10); var subclass2 = new SubClass(11); console.log(subclass1.books); console.log(subclass2.id); console.log(subclass1.name); //undefined subclass2.showBooks();
組合式繼承
我們先來總結一下類繼承和構造函數繼承的優缺點
類繼承 | 構造函數繼承 | |
---|---|---|
核心思想 | 子類的原型是父類實例化的對象 | SuperClass.call(this,id) |
優點 | 子類實例化對象的屬性和方法都指向父類的原型 | 每個實例化的子類互不影響 |
缺點 | 子類之間可能會互相影響 | 內存浪費 |
所以組合式繼承就是汲取兩者的優點,即避免了內存浪費,又使得每個實例化的子類互不影響。
//組合式繼承 //聲明父類 var SuperClass = function (name) { this.name = name; this.books=[‘javascript‘,‘html‘,‘css‘] }; //聲明父類原型上的方法 SuperClass.prototype.showBooks = function () { console.log(this.books) }; //聲明子類 var SubClass = function (name) { SuperClass.call(this, name) }; //子類繼承父類(鏈式繼承) SubClass.prototype = new SuperClass(); //實例化子類 var subclass1 = new SubClass(‘java‘); var subclass2 = new SubClass(‘php‘); subclass2.showBooks(); subclass1.books.push(‘ios‘); //["javascript", "html", "css"] console.log(subclass1.books); //["javascript", "html", "css", "ios"] console.log(subclass2.books); //["javascript", "html", "css"]
寄生組合繼承
那麽問題又來了~組合式繼承的方法固然好,但是會導致一個問題,父類的構造函數會被創建兩次(call()的時候一遍,new的時候又一遍),所以為了解決這個問題,又出現了寄生組合繼承。
剛剛問題的關鍵是父類的構造函數在類繼承和構造函數繼承的組合形式中被創建了兩遍,但是在類繼承中我們並不需要創建父類的構造函數,我們只是要子類繼承父類的原型即可。所以說我們先給父類的原型創建一個副本,然後修改子類constructor
屬性,最後在設置子類的原型就可以了~ constructor
是在創建函數的時候額外添加的一個屬性,該屬性指向創建該實例的構造函數
如果替換了
prototype
對象,那麽下一步必然是為心的prototype
對象加上constructor
屬性,並將這個constructor
屬性指回原來的構造函數
//原型式繼承 //原型式繼承其實就是類式繼承的封裝,實現的功能是返回一個實例,改實例的原型繼承了傳入的o對象 function inheritObject(o) { //聲明一個過渡函數對象 function F() {} //過渡對象的原型繼承父對象 F.prototype = o; //返回一個過渡對象的實例,該實例的原型繼承了父對象 return new F(); } //寄生式繼承 //寄生式繼承就是對原型繼承的第二次封裝,使得子類的原型等於父類的原型。並且在第二次封裝的過程中對繼承的對象進行了擴展 function inheritPrototype(subClass, superClass){ //復制一份父類的原型保存在變量中,使得p的原型等於父類的原型 var p = inheritObject(superClass.prototype); //修正因為重寫子類原型導致子類constructor屬性被修改 p.constructor = subClass; //設置子類的原型 subClass.prototype = p; } //定義父類 var SuperClass = function (name) { this.name = name; this.books = [‘javascript‘,‘html‘,‘css‘] }; //定義父類原型方法 SuperClass.prototype.getBooks = function () { console.log(this.books) }; //定義子類 var SubClass = function (name) { SuperClass.call(this,name) } inheritObject(SubClass,SuperClass); var subclass1 = new SubClass(‘php‘)
【轉載】面向對象編程